@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
@@ -2,12 +2,20 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { readFileSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
5
8
 
6
- vi.mock('@opencode-ai/sdk/v2', () => ({
7
- createOpencodeClient: vi.fn(),
9
+ vi.mock('../opencode/v2-client.js', () => ({
10
+ makeOpenCodeV2Client: vi.fn(),
8
11
  }));
9
12
 
10
- const { createOpencodeClient } = await import('@opencode-ai/sdk/v2');
13
+ vi.mock('../session-goal/objectives.js', () => ({
14
+ writeObjective: vi.fn(async () => ({ content: 'ok' })),
15
+ }));
16
+
17
+ const { makeOpenCodeV2Client } = await import('../opencode/v2-client.js');
18
+ const { writeObjective } = await import('../session-goal/objectives.js');
11
19
  const { createScheduledTaskRunHistoryStore } = await import('./run-history-store.js');
12
20
  const {
13
21
  computeNextRunAt,
@@ -37,6 +45,7 @@ const createRuntime = (updateScheduledTaskState, overrides = {}) => createSchedu
37
45
  waitForOpenCodeReady: vi.fn(async () => {
38
46
  throw new Error('OpenCode unavailable');
39
47
  }),
48
+ persistSessionMetadata: vi.fn(async () => ({})),
40
49
  logger: { info: vi.fn(), warn: vi.fn() },
41
50
  ...overrides,
42
51
  });
@@ -310,10 +319,26 @@ describe('scheduled-tasks project sync isolation', () => {
310
319
  });
311
320
  });
312
321
 
322
+ describe('scheduled-tasks v2 cutover residuals', () => {
323
+ it('runtime source has no 1.x SDK, HeyAPI wrappers, or retired session APIs', () => {
324
+ const source = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'runtime.js'), 'utf8');
325
+ expect(source).not.toContain('@opencode-ai/sdk');
326
+ expect(source).not.toContain('createOpencodeClient');
327
+ expect(source).not.toContain('prompt_async');
328
+ expect(source).not.toContain('session.abort');
329
+ expect(source).not.toContain('session.status({');
330
+ expect(source).not.toContain('session.messages');
331
+ expect(source).not.toContain('session.update');
332
+ expect(source).not.toContain('time: { archived');
333
+ expect(source).not.toContain('result?.error');
334
+ });
335
+ });
336
+
313
337
  describe('scheduled-tasks run history and session lifecycle', () => {
314
338
  beforeEach(() => {
315
339
  vi.restoreAllMocks();
316
- createOpencodeClient.mockReset();
340
+ makeOpenCodeV2Client.mockReset();
341
+ writeObjective.mockClear();
317
342
  });
318
343
 
319
344
  const createHistoryStore = () => {
@@ -341,111 +366,94 @@ describe('scheduled-tasks run history and session lifecycle', () => {
341
366
  };
342
367
 
343
368
  const createSuccessfulClient = ({
344
- updateResult,
345
369
  sessionID = 'ses_1',
346
370
  /** Override session settlement polls after prompt/command admission. */
347
371
  settlement = 'success',
372
+ activeImpl,
373
+ messageListImpl,
348
374
  } = {}) => {
349
- const create = vi.fn(async () => ({ data: { id: sessionID } }));
350
- const update = vi.fn(async () => updateResult ?? { data: { id: sessionID } });
351
- const command = vi.fn(async () => ({ data: {} }));
352
- const abort = vi.fn(async () => ({ data: true }));
375
+ const create = vi.fn(async () => ({ id: sessionID }));
376
+ const prompt = vi.fn(async () => ({ id: 'inbox_1', type: 'user' }));
377
+ const command = vi.fn(async () => ({ id: 'inbox_1', type: 'user' }));
378
+ const interrupt = vi.fn(async () => undefined);
353
379
  const list = vi.fn(async () => ({ data: [] }));
354
- // Default settlement: idle + completed assistant so history records the real
355
- // turn outcome (not prompt_async admission alone).
380
+ // Default settlement: idle (absent from active map) + completed assistant
381
+ // so history records the real turn outcome (not prompt admission alone).
356
382
  let poll = 0;
357
- const status = vi.fn(async () => {
383
+ const active = vi.fn(async () => {
384
+ if (typeof activeImpl === 'function') {
385
+ return activeImpl();
386
+ }
358
387
  poll += 1;
359
388
  if (settlement === 'busy-then-success' && poll < 2) {
360
- return { data: { [sessionID]: { type: 'busy' } } };
389
+ return { [sessionID]: { type: 'running' } };
361
390
  }
362
- return { data: { [sessionID]: { type: 'idle' } } };
391
+ return {};
363
392
  });
364
- const messages = vi.fn(async () => {
393
+ const messageList = vi.fn(async () => {
394
+ if (typeof messageListImpl === 'function') {
395
+ return messageListImpl();
396
+ }
365
397
  if (settlement === 'assistant-error') {
366
398
  return {
367
399
  data: [{
368
- info: {
369
- id: 'msg_err',
370
- role: 'assistant',
371
- error: { name: 'ProviderError', message: 'upstream failed' },
372
- },
400
+ id: 'msg_err',
401
+ type: 'assistant',
402
+ error: { name: 'ProviderError', message: 'upstream failed' },
373
403
  }],
374
404
  };
375
405
  }
376
- if (settlement === 'busy-then-success') {
377
- if (poll < 2) {
378
- return { data: [{ info: { id: 'msg_user', role: 'user' } }] };
379
- }
406
+ if (settlement === 'busy-then-success' && poll < 2) {
407
+ return { data: [{ id: 'msg_user', type: 'user' }] };
380
408
  }
381
409
  return {
382
410
  data: [{
383
- info: {
384
- id: 'msg_ok',
385
- role: 'assistant',
386
- time: { completed: Date.now() },
387
- },
411
+ id: 'msg_ok',
412
+ type: 'assistant',
413
+ time: { created: Date.now(), completed: Date.now() },
414
+ finish: 'stop',
388
415
  }],
389
416
  };
390
417
  });
391
- const get = vi.fn(async () => {
392
- if (settlement === 'goal-complete') {
393
- return {
394
- data: {
395
- id: sessionID,
396
- metadata: {
397
- openchamber: {
398
- goal: { id: 'g1', status: 'complete', objective: 'done' },
399
- },
400
- },
401
- },
402
- };
403
- }
404
- if (settlement === 'goal-blocked') {
405
- return {
406
- data: {
407
- id: sessionID,
408
- metadata: {
409
- openchamber: {
410
- goal: { id: 'g1', status: 'blocked', objective: 'x', note: 'stuck' },
411
- },
412
- },
413
- },
414
- };
415
- }
416
- return { data: { id: sessionID, metadata: {} } };
417
- });
418
- createOpencodeClient.mockReturnValue({
419
- session: { create, update, command, abort, status, messages, get },
418
+ const get = vi.fn(async () => ({
419
+ id: sessionID,
420
+ location: { directory: '/tmp/project-1' },
421
+ }));
422
+ makeOpenCodeV2Client.mockReturnValue({
423
+ session: { create, prompt, command, interrupt, active, get },
424
+ message: { list: messageList },
420
425
  command: { list },
421
426
  });
422
- return { create, update, command, abort, list, status, messages, get };
427
+ return { create, prompt, command, interrupt, list, active, messageList, get };
423
428
  };
424
429
 
425
- const settledSessionApis = (sessionID = 'ses_1') => ({
426
- status: vi.fn(async () => ({ data: { [sessionID]: { type: 'idle' } } })),
427
- messages: vi.fn(async () => ({
428
- data: [{ info: { id: 'msg_ok', role: 'assistant', time: { completed: Date.now() } } }],
429
- })),
430
- get: vi.fn(async () => ({ data: { id: sessionID, metadata: {} } })),
430
+ const completedAssistant = (id = 'msg_ok') => ({
431
+ id,
432
+ type: 'assistant',
433
+ time: { created: 1_000, completed: 1_100 },
434
+ finish: 'stop',
431
435
  });
432
436
 
433
- it('creates session with Scheduled title and scheduledTask metadata, attaches history, archives before prompt', async () => {
437
+ const erroredAssistant = (id = 'msg_err') => ({
438
+ id,
439
+ type: 'assistant',
440
+ time: { created: 2_000, completed: 2_100 },
441
+ error: { name: 'ProviderError', message: 'upstream failed' },
442
+ });
443
+
444
+ it('creates session with Scheduled title and location, attaches history, then prompts via session.prompt', async () => {
434
445
  const history = createHistoryStore();
435
446
  const client = createSuccessfulClient();
436
- const fetchMock = vi.fn(async (url, init) => {
437
- if (String(url).includes('prompt_async')) {
438
- return { ok: true, text: async () => '' };
439
- }
440
- throw new Error(`unexpected fetch ${url} ${init?.method}`);
441
- });
442
- vi.stubGlobal('fetch', fetchMock);
447
+ const persistSessionMetadata = vi.fn(async () => ({}));
448
+ const onSystemSessionPersisted = vi.fn();
443
449
 
444
450
  const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
445
451
  task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
446
452
  }));
447
453
  const runtime = createRuntime(updateScheduledTaskState, {
448
454
  runHistoryStore: history,
455
+ persistSessionMetadata,
456
+ onSystemSessionPersisted,
449
457
  waitForOpenCodeReady: vi.fn(async () => {}),
450
458
  });
451
459
  await runtime.syncProject('project-1');
@@ -463,49 +471,105 @@ describe('scheduled-tasks run history and session lifecycle', () => {
463
471
  });
464
472
  expect(typeof started.id).toBe('string');
465
473
 
474
+ expect(makeOpenCodeV2Client).toHaveBeenCalledWith({
475
+ baseUrl: 'http://127.0.0.1:4096',
476
+ authHeaders: {},
477
+ });
466
478
  expect(client.create).toHaveBeenCalledWith(expect.objectContaining({
467
- directory: '/tmp/project-1',
479
+ location: { directory: '/tmp/project-1' },
468
480
  title: expect.stringMatching(/^\[Scheduled\] Task /),
481
+ model: { id: 'gpt-4.1', providerID: 'openai' },
469
482
  metadata: {
470
483
  openchamber: {
471
- scheduledTask: {
484
+ scheduledTask: expect.objectContaining({
472
485
  projectID: 'project-1',
473
486
  taskID: 'task-1',
474
487
  runID: started.id,
475
488
  name: 'Task',
476
- },
489
+ }),
477
490
  },
478
491
  },
479
492
  }), expect.objectContaining({ signal: expect.any(AbortSignal) }));
480
- expect(history.attachSession).toHaveBeenCalledWith(started.id, 'ses_1');
481
- expect(client.update).toHaveBeenCalledWith(expect.objectContaining({
493
+ expect(client.create.mock.calls[0][0].directory).toBeUndefined();
494
+ expect(persistSessionMetadata).toHaveBeenCalledWith('ses_1', {
495
+ openchamber: {
496
+ scheduledTask: expect.objectContaining({
497
+ projectID: 'project-1',
498
+ taskID: 'task-1',
499
+ runID: started.id,
500
+ name: 'Task',
501
+ }),
502
+ },
503
+ });
504
+ expect(onSystemSessionPersisted).toHaveBeenCalledWith({
482
505
  sessionID: 'ses_1',
483
506
  directory: '/tmp/project-1',
484
- time: { archived: expect.any(Number) },
485
- }), expect.objectContaining({ signal: expect.any(AbortSignal) }));
486
- expect(client.update.mock.invocationCallOrder[0]).toBeLessThan(fetchMock.mock.invocationCallOrder[0]);
507
+ metadata: expect.objectContaining({
508
+ openchamber: expect.objectContaining({
509
+ scheduledTask: expect.objectContaining({ taskID: 'task-1' }),
510
+ }),
511
+ }),
512
+ });
513
+ expect(history.attachSession).toHaveBeenCalledWith(started.id, 'ses_1');
514
+ expect(client.prompt).toHaveBeenCalledWith({
515
+ sessionID: 'ses_1',
516
+ text: 'run',
517
+ }, expect.objectContaining({ signal: expect.any(AbortSignal) }));
487
518
  expect(history.finishRun).toHaveBeenCalledWith(started.id, expect.objectContaining({
488
519
  status: 'success',
489
520
  sessionId: 'ses_1',
490
521
  }));
491
- // Must wait for session settlement, not finish on prompt_async admission alone.
492
- expect(client.status).toHaveBeenCalled();
493
- expect(client.messages).toHaveBeenCalled();
494
- expect(client.status.mock.invocationCallOrder[0]).toBeGreaterThan(fetchMock.mock.invocationCallOrder[0]);
522
+ // Must wait for session settlement, not finish on prompt admission alone.
523
+ expect(client.active).toHaveBeenCalled();
524
+ expect(client.messageList).toHaveBeenCalled();
525
+ expect(client.active.mock.invocationCallOrder[0]).toBeGreaterThan(client.prompt.mock.invocationCallOrder[0]);
526
+ });
495
527
 
496
- vi.unstubAllGlobals();
528
+ it('refuses before session create when Host metadata persist is not configured', async () => {
529
+ const history = createHistoryStore();
530
+ const client = createSuccessfulClient();
531
+ const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
532
+ task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
533
+ }));
534
+ const runtime = createRuntime(updateScheduledTaskState, {
535
+ runHistoryStore: history,
536
+ persistSessionMetadata: null,
537
+ waitForOpenCodeReady: vi.fn(async () => {}),
538
+ });
539
+ await runtime.syncProject('project-1');
540
+ const result = await runtime.runNow('project-1', 'task-1');
541
+
542
+ expect(result.ok).toBe(false);
543
+ expect(result.error).toMatch(/session metadata persist is not configured/);
544
+ expect(client.create).not.toHaveBeenCalled();
545
+ expect(client.prompt).not.toHaveBeenCalled();
546
+ });
547
+
548
+ it('fails the run before prompt when Host metadata persist throws', async () => {
549
+ const history = createHistoryStore();
550
+ const client = createSuccessfulClient();
551
+ const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
552
+ task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
553
+ }));
554
+ const runtime = createRuntime(updateScheduledTaskState, {
555
+ runHistoryStore: history,
556
+ persistSessionMetadata: vi.fn(async () => {
557
+ throw new Error('store unavailable');
558
+ }),
559
+ waitForOpenCodeReady: vi.fn(async () => {}),
560
+ });
561
+ await runtime.syncProject('project-1');
562
+ const result = await runtime.runNow('project-1', 'task-1');
563
+
564
+ expect(result.ok).toBe(false);
565
+ expect(result.error).toMatch(/store unavailable/);
566
+ expect(client.create).toHaveBeenCalled();
567
+ expect(client.prompt).not.toHaveBeenCalled();
497
568
  });
498
569
 
499
570
  it('records error outcome and real duration when the assistant turn fails after admission', async () => {
500
571
  const history = createHistoryStore();
501
572
  const client = createSuccessfulClient({ settlement: 'assistant-error' });
502
- const fetchMock = vi.fn(async (url) => {
503
- if (String(url).includes('prompt_async')) {
504
- return { ok: true, text: async () => '' };
505
- }
506
- throw new Error(`unexpected fetch ${url}`);
507
- });
508
- vi.stubGlobal('fetch', fetchMock);
509
573
 
510
574
  const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
511
575
  task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
@@ -528,22 +592,13 @@ describe('scheduled-tasks run history and session lifecycle', () => {
528
592
  durationMs: expect.any(Number),
529
593
  }),
530
594
  );
531
- expect(client.messages).toHaveBeenCalled();
532
-
533
- vi.unstubAllGlobals();
595
+ expect(client.messageList).toHaveBeenCalled();
534
596
  });
535
597
 
536
- it('keeps the run open while the session is busy and only finalizes after idle settlement', async () => {
598
+ it('keeps the run open while the session is in the active map and only finalizes after idle settlement', async () => {
537
599
  vi.useFakeTimers();
538
600
  const history = createHistoryStore();
539
601
  createSuccessfulClient({ settlement: 'busy-then-success' });
540
- const fetchMock = vi.fn(async (url) => {
541
- if (String(url).includes('prompt_async')) {
542
- return { ok: true, text: async () => '' };
543
- }
544
- throw new Error(`unexpected fetch ${url}`);
545
- });
546
- vi.stubGlobal('fetch', fetchMock);
547
602
 
548
603
  const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
549
604
  task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
@@ -555,7 +610,7 @@ describe('scheduled-tasks run history and session lifecycle', () => {
555
610
  await runtime.syncProject('project-1');
556
611
 
557
612
  const runPromise = runtime.runNow('project-1', 'task-1');
558
- // First settlement poll sees busy and sleeps — history must still be open.
613
+ // First settlement poll sees membership and sleeps — history must still be open.
559
614
  await vi.advanceTimersByTimeAsync(0);
560
615
  expect(history.finishRun).not.toHaveBeenCalled();
561
616
 
@@ -568,17 +623,15 @@ describe('scheduled-tasks run history and session lifecycle', () => {
568
623
  expect.objectContaining({ status: 'success', sessionId: 'ses_1' }),
569
624
  );
570
625
 
571
- vi.unstubAllGlobals();
572
626
  vi.useRealTimers();
573
627
  });
574
628
 
575
- it('does not prompt when archive fails and finalizes the run as error', async () => {
629
+ it('does not prompt when attachSession fails and finalizes the run as error', async () => {
576
630
  const history = createHistoryStore();
577
- createSuccessfulClient({
578
- updateResult: { error: { status: 500, message: 'archive failed' } },
631
+ const client = createSuccessfulClient();
632
+ history.attachSession.mockImplementation(() => {
633
+ throw new Error('attach exploded');
579
634
  });
580
- const fetchMock = vi.fn(async () => ({ ok: true, text: async () => '' }));
581
- vi.stubGlobal('fetch', fetchMock);
582
635
 
583
636
  const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
584
637
  task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
@@ -592,28 +645,22 @@ describe('scheduled-tasks run history and session lifecycle', () => {
592
645
 
593
646
  expect(result.ok).toBe(false);
594
647
  expect(result.status).toBe('error');
595
- expect(fetchMock).not.toHaveBeenCalled();
648
+ expect(result.error).toMatch(/attachSession failed/);
649
+ expect(client.prompt).not.toHaveBeenCalled();
596
650
  expect(history.finishRun).toHaveBeenCalledWith(
597
651
  expect.any(String),
598
652
  expect.objectContaining({ status: 'error' }),
599
653
  );
600
-
601
- vi.unstubAllGlobals();
602
654
  });
603
655
 
604
- it('retries archive once on 404 then prompts on success', async () => {
656
+ it('does not treat session.active failure as idle success', async () => {
605
657
  vi.useFakeTimers();
606
658
  const history = createHistoryStore();
607
- const create = vi.fn(async () => ({ data: { id: 'ses_404' } }));
608
- const update = vi.fn()
609
- .mockResolvedValueOnce({ error: { status: 404 } })
610
- .mockResolvedValueOnce({ data: { id: 'ses_404' } });
611
- createOpencodeClient.mockReturnValue({
612
- session: { create, update, command: vi.fn(), ...settledSessionApis('ses_404') },
613
- command: { list: vi.fn(async () => ({ data: [] })) },
659
+ createSuccessfulClient({
660
+ activeImpl: async () => {
661
+ throw new Error('active unavailable');
662
+ },
614
663
  });
615
- const fetchMock = vi.fn(async () => ({ ok: true, text: async () => '' }));
616
- vi.stubGlobal('fetch', fetchMock);
617
664
 
618
665
  const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
619
666
  task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
@@ -621,63 +668,67 @@ describe('scheduled-tasks run history and session lifecycle', () => {
621
668
  const runtime = createRuntime(updateScheduledTaskState, {
622
669
  runHistoryStore: history,
623
670
  waitForOpenCodeReady: vi.fn(async () => {}),
671
+ maxRunDurationMs: 2_500,
624
672
  });
625
673
  await runtime.syncProject('project-1');
626
674
 
627
675
  const runPromise = runtime.runNow('project-1', 'task-1');
628
- await vi.advanceTimersByTimeAsync(250);
676
+ await vi.advanceTimersByTimeAsync(1_000);
677
+ expect(history.finishRun).not.toHaveBeenCalled();
678
+
679
+ await vi.advanceTimersByTimeAsync(1_500);
629
680
  const result = await runPromise;
630
681
 
631
- expect(result.ok).toBe(true);
632
- expect(update).toHaveBeenCalledTimes(2);
633
- expect(fetchMock).toHaveBeenCalledTimes(1);
682
+ expect(result.ok).toBe(false);
683
+ expect(result.status).toBe('error');
684
+ expect(result.error).toBe('schedule run timed out');
685
+ expect(history.finishRun).toHaveBeenCalledWith(
686
+ expect.any(String),
687
+ expect.objectContaining({
688
+ status: 'error',
689
+ error: 'schedule run timed out',
690
+ }),
691
+ );
634
692
 
635
- vi.unstubAllGlobals();
636
693
  vi.useRealTimers();
637
694
  });
638
695
 
639
- it('watchdog timeout aborts in-flight work, calls session.abort, and does not continue later stages', async () => {
696
+ it('watchdog timeout interrupts in-flight work and does not continue later stages', async () => {
640
697
  vi.useFakeTimers();
641
698
  const history = createHistoryStore();
642
699
  let resolvePrompt;
643
700
  let promptStarted = false;
644
701
  let promptCompleted = false;
645
- const create = vi.fn(async () => ({ data: { id: 'ses_timeout' } }));
646
- const update = vi.fn(async () => ({ data: { id: 'ses_timeout' } }));
647
- const abort = vi.fn(async () => ({ data: true }));
702
+ const create = vi.fn(async () => ({ id: 'ses_timeout' }));
703
+ const interrupt = vi.fn(async () => undefined);
648
704
  const command = vi.fn(async () => {
649
705
  throw new Error('command must not run after hanging prompt path');
650
706
  });
651
707
  const list = vi.fn(async () => ({ data: [] }));
652
- createOpencodeClient.mockReturnValue({
653
- session: { create, update, command, abort },
654
- command: { list },
655
- });
656
-
657
- const fetchMock = vi.fn((url, init) => {
658
- if (String(url).includes('prompt_async')) {
659
- promptStarted = true;
660
- return new Promise((resolve, reject) => {
661
- resolvePrompt = () => {
662
- promptCompleted = true;
663
- resolve({ ok: true, text: async () => '' });
708
+ const prompt = vi.fn((_params, options) => {
709
+ promptStarted = true;
710
+ return new Promise((resolve, reject) => {
711
+ resolvePrompt = () => {
712
+ promptCompleted = true;
713
+ resolve({ id: 'inbox_1', type: 'user' });
714
+ };
715
+ const signal = options?.signal;
716
+ if (signal) {
717
+ const onAbort = () => {
718
+ reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
664
719
  };
665
- const signal = init?.signal;
666
- if (signal) {
667
- const onAbort = () => {
668
- reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
669
- };
670
- if (signal.aborted) {
671
- onAbort();
672
- return;
673
- }
674
- signal.addEventListener('abort', onAbort, { once: true });
720
+ if (signal.aborted) {
721
+ onAbort();
722
+ return;
675
723
  }
676
- });
677
- }
678
- throw new Error(`unexpected fetch ${url}`);
724
+ signal.addEventListener('abort', onAbort, { once: true });
725
+ }
726
+ });
727
+ });
728
+ makeOpenCodeV2Client.mockReturnValue({
729
+ session: { create, prompt, command, interrupt },
730
+ command: { list },
679
731
  });
680
- vi.stubGlobal('fetch', fetchMock);
681
732
 
682
733
  const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
683
734
  task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
@@ -706,9 +757,9 @@ describe('scheduled-tasks run history and session lifecycle', () => {
706
757
  expect(result.ok).toBe(false);
707
758
  expect(result.status).toBe('error');
708
759
  expect(result.error).toBe('schedule run timed out');
709
- expect(abort).toHaveBeenCalledWith({
760
+ expect(interrupt).toHaveBeenCalledWith({
710
761
  sessionID: 'ses_timeout',
711
- directory: '/tmp/project-1',
762
+ continue: false,
712
763
  });
713
764
  expect(promptCompleted).toBe(false);
714
765
  expect(command).not.toHaveBeenCalled();
@@ -731,18 +782,16 @@ describe('scheduled-tasks run history and session lifecycle', () => {
731
782
  } finally {
732
783
  process.off('unhandledRejection', onUnhandled);
733
784
  resolvePrompt?.();
734
- vi.unstubAllGlobals();
735
785
  vi.useRealTimers();
736
786
  }
737
787
  });
738
788
 
739
- it('watchdog timeout during hanging command aborts session and never prompt_async', async () => {
789
+ it('watchdog timeout during hanging command interrupts session and never session.prompt', async () => {
740
790
  vi.useFakeTimers();
741
791
  const history = createHistoryStore();
742
792
  let commandReached = false;
743
- const create = vi.fn(async () => ({ data: { id: 'ses_cmd' } }));
744
- const update = vi.fn(async () => ({ data: { id: 'ses_cmd' } }));
745
- const abort = vi.fn(async () => ({ data: true }));
793
+ const create = vi.fn(async () => ({ id: 'ses_cmd' }));
794
+ const interrupt = vi.fn(async () => undefined);
746
795
  const command = vi.fn((_params, options) => {
747
796
  commandReached = true;
748
797
  return new Promise((_resolve, reject) => {
@@ -761,14 +810,13 @@ describe('scheduled-tasks run history and session lifecycle', () => {
761
810
  });
762
811
  });
763
812
  const list = vi.fn(async () => ({ data: [{ name: 'review' }] }));
764
- createOpencodeClient.mockReturnValue({
765
- session: { create, update, command, abort },
766
- command: { list },
813
+ const prompt = vi.fn(async () => {
814
+ throw new Error('session.prompt must not run when command is selected');
767
815
  });
768
- const fetchMock = vi.fn(async () => {
769
- throw new Error('prompt_async must not run when command is selected');
816
+ makeOpenCodeV2Client.mockReturnValue({
817
+ session: { create, prompt, command, interrupt },
818
+ command: { list },
770
819
  });
771
- vi.stubGlobal('fetch', fetchMock);
772
820
 
773
821
  const taskWithCommand = {
774
822
  ...scheduledTask,
@@ -790,6 +838,7 @@ describe('scheduled-tasks run history and session lifecycle', () => {
790
838
  buildOpenCodeUrl: vi.fn(() => 'http://127.0.0.1:4096'),
791
839
  getOpenCodeAuthHeaders: vi.fn(() => ({})),
792
840
  waitForOpenCodeReady: vi.fn(async () => {}),
841
+ persistSessionMetadata: vi.fn(async () => ({})),
793
842
  logger: { info: vi.fn(), warn: vi.fn() },
794
843
  runHistoryStore: history,
795
844
  maxRunDurationMs: 500,
@@ -808,47 +857,41 @@ describe('scheduled-tasks run history and session lifecycle', () => {
808
857
  status: 'error',
809
858
  error: 'schedule run timed out',
810
859
  });
811
- expect(abort).toHaveBeenCalledWith({
860
+ expect(interrupt).toHaveBeenCalledWith({
812
861
  sessionID: 'ses_cmd',
813
- directory: '/tmp/project-1',
862
+ continue: false,
814
863
  });
815
- expect(fetchMock).not.toHaveBeenCalled();
864
+ expect(command).toHaveBeenCalledWith(expect.objectContaining({
865
+ sessionID: 'ses_cmd',
866
+ command: 'review',
867
+ arguments: 'src',
868
+ model: { id: 'gpt-4.1', providerID: 'openai' },
869
+ }), expect.objectContaining({ signal: expect.any(AbortSignal) }));
870
+ expect(list).toHaveBeenCalledWith({
871
+ location: { directory: '/tmp/project-1' },
872
+ }, expect.objectContaining({ signal: expect.any(AbortSignal) }));
873
+ expect(prompt).not.toHaveBeenCalled();
816
874
 
817
- vi.unstubAllGlobals();
818
875
  vi.useRealTimers();
819
876
  });
820
877
 
821
- it('watchdog timeout during hanging small-model distill returns without waiting and blocks later goal stages', async () => {
822
- vi.useFakeTimers();
878
+ it('goalEnabled without persist refuses before OpenCode ready / distill / session create (no side effects)', async () => {
823
879
  const history = createHistoryStore();
824
- let resolveDistill;
825
- let distillStarted = false;
826
- const create = vi.fn(async () => ({ data: { id: 'ses_distill' } }));
827
- const update = vi.fn(async () => ({ data: { id: 'ses_distill' } }));
828
- const abort = vi.fn(async () => ({ data: true }));
829
- const command = vi.fn(async () => {
830
- throw new Error('command must not run after hanging distill');
880
+ const create = vi.fn(async () => ({ id: 'ses_distill' }));
881
+ const interrupt = vi.fn(async () => undefined);
882
+ const prompt = vi.fn(async () => {
883
+ throw new Error('session.prompt must not run without goal persist');
831
884
  });
832
- const list = vi.fn(async () => ({ data: [] }));
833
- createOpencodeClient.mockReturnValue({
834
- session: { create, update, command, abort },
835
- command: { list },
836
- });
837
-
838
- const fetchMock = vi.fn(async (url, init) => {
839
- throw new Error(`unexpected fetch ${url} ${init?.method}`);
885
+ makeOpenCodeV2Client.mockReturnValue({
886
+ session: { create, prompt, command: vi.fn(), interrupt },
887
+ command: { list: vi.fn(async () => ({ data: [] })) },
840
888
  });
841
- vi.stubGlobal('fetch', fetchMock);
842
889
 
843
- const generateSmallModelText = vi.fn(() => {
844
- distillStarted = true;
845
- return new Promise((resolve) => {
846
- resolveDistill = () => resolve({ text: 'distilled criteria' });
847
- });
848
- });
890
+ const generateSmallModelText = vi.fn(async () => ({ text: 'should not distill' }));
849
891
  const getSmallModelService = vi.fn(async () => ({ generateSmallModelText }));
892
+ const waitForOpenCodeReady = vi.fn(async () => {});
850
893
 
851
- // Oversized prompt forces small-model distill (threshold 5000).
894
+ // Oversized prompt would force distill if goal were admitted — it must not.
852
895
  const largePrompt = `finish the migration ${'x'.repeat(5100)}`;
853
896
  const goalTask = {
854
897
  ...scheduledTask,
@@ -872,78 +915,162 @@ describe('scheduled-tasks run history and session lifecycle', () => {
872
915
  buildOpenCodeUrl: vi.fn(() => 'http://127.0.0.1:4096'),
873
916
  getOpenCodeAuthHeaders: vi.fn(() => ({})),
874
917
  getSmallModelService,
875
- waitForOpenCodeReady: vi.fn(async () => {}),
918
+ waitForOpenCodeReady,
876
919
  logger: { info: vi.fn(), warn: vi.fn() },
877
920
  runHistoryStore: history,
878
- maxRunDurationMs: 1_000,
921
+ // intentionally no persistSessionGoal
879
922
  });
880
923
  await runtime.syncProject('project-1');
924
+ const result = await runtime.runNow('project-1', 'task-1');
881
925
 
882
- const unhandled = [];
883
- const onUnhandled = (reason) => {
884
- unhandled.push(reason);
926
+ expect(result.ok).toBe(false);
927
+ expect(result.error).toMatch(/session goal persist is not configured/);
928
+ expect(waitForOpenCodeReady).not.toHaveBeenCalled();
929
+ expect(getSmallModelService).not.toHaveBeenCalled();
930
+ expect(generateSmallModelText).not.toHaveBeenCalled();
931
+ expect(create).not.toHaveBeenCalled();
932
+ expect(prompt).not.toHaveBeenCalled();
933
+ expect(writeObjective).not.toHaveBeenCalled();
934
+ expect(history.attachSession).not.toHaveBeenCalled();
935
+ expect(history.finishRun).toHaveBeenCalledWith(
936
+ expect.any(String),
937
+ expect.objectContaining({
938
+ status: 'error',
939
+ error: expect.stringMatching(/session goal persist is not configured/),
940
+ }),
941
+ );
942
+ });
943
+
944
+ it('goalEnabled without persist refuses before session create / objective write', async () => {
945
+ const history = createHistoryStore();
946
+ const client = createSuccessfulClient({ sessionID: 'ses_goal' });
947
+
948
+ const goalTask = {
949
+ ...scheduledTask,
950
+ execution: {
951
+ ...scheduledTask.execution,
952
+ prompt: 'finish the migration',
953
+ goalEnabled: true,
954
+ goalTokenBudget: 12_000,
955
+ },
885
956
  };
886
- process.on('unhandledRejection', onUnhandled);
957
+ const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
958
+ task: { ...goalTask, state: { ...goalTask.state, ...state } },
959
+ }));
960
+ const runtime = createScheduledTasksRuntime({
961
+ projectConfigRuntime: {
962
+ listScheduledTasks: vi.fn(async () => [goalTask]),
963
+ updateScheduledTaskState,
964
+ upsertScheduledTask: vi.fn(),
965
+ },
966
+ listProjects: vi.fn(async () => [{ id: 'project-1', path: '/tmp/project-1' }]),
967
+ buildOpenCodeUrl: vi.fn(() => 'http://127.0.0.1:4096'),
968
+ getOpenCodeAuthHeaders: vi.fn(() => ({})),
969
+ waitForOpenCodeReady: vi.fn(async () => {}),
970
+ logger: { info: vi.fn(), warn: vi.fn() },
971
+ runHistoryStore: history,
972
+ // intentionally no persistSessionGoal
973
+ });
974
+ await runtime.syncProject('project-1');
975
+ const result = await runtime.runNow('project-1', 'task-1');
887
976
 
888
- try {
889
- const runPromise = runtime.runNow('project-1', 'task-1');
890
- await vi.advanceTimersByTimeAsync(0);
891
- expect(distillStarted).toBe(true);
892
- expect(generateSmallModelText).toHaveBeenCalled();
977
+ // Missing persist is an explicit run failure before any side effects.
978
+ expect(result.ok).toBe(false);
979
+ expect(result.status).toBe('error');
980
+ expect(result.error).toMatch(/session goal persist is not configured/);
981
+ expect(client.create).not.toHaveBeenCalled();
982
+ expect(client.prompt).not.toHaveBeenCalled();
983
+ expect(writeObjective).not.toHaveBeenCalled();
984
+ expect(history.finishRun).toHaveBeenCalledWith(
985
+ expect.any(String),
986
+ expect.objectContaining({
987
+ status: 'error',
988
+ error: expect.stringMatching(/session goal persist is not configured/),
989
+ }),
990
+ );
991
+ // Task config is preserved (goalEnabled stays on the task); only this run fails.
992
+ const finalPatch = updateScheduledTaskState.mock.calls.at(-1)[2];
993
+ expect(finalPatch.lastStatus).toBe('error');
994
+ expect(Object.hasOwn(finalPatch, 'lastSessionId')).toBe(false);
995
+ });
893
996
 
894
- // Timeout must return while distill is still gated (non-cancellable).
895
- await vi.advanceTimersByTimeAsync(1_000);
896
- const result = await runPromise;
997
+ it('goalEnabled with persist writes objective + active goal then prompts', async () => {
998
+ const history = createHistoryStore();
999
+ const client = createSuccessfulClient({ sessionID: 'ses_goal_ok' });
1000
+ const persistSessionGoal = vi.fn(async (_sessionId, _directory, goal) => ({
1001
+ openchamber: { goal },
1002
+ }));
1003
+ const onGoalPersisted = vi.fn();
897
1004
 
898
- expect(result.ok).toBe(false);
899
- expect(result.status).toBe('error');
900
- expect(result.error).toBe('schedule run timed out');
901
- expect(abort).toHaveBeenCalledWith({
902
- sessionID: 'ses_distill',
903
- directory: '/tmp/project-1',
904
- });
905
- expect(fetchMock).not.toHaveBeenCalled();
906
- expect(command).not.toHaveBeenCalled();
1005
+ const goalTask = {
1006
+ ...scheduledTask,
1007
+ execution: {
1008
+ ...scheduledTask.execution,
1009
+ prompt: 'finish the migration',
1010
+ goalEnabled: true,
1011
+ goalTokenBudget: 12_000,
1012
+ },
1013
+ };
1014
+ const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
1015
+ task: { ...goalTask, state: { ...goalTask.state, ...state } },
1016
+ }));
1017
+ const runtime = createScheduledTasksRuntime({
1018
+ projectConfigRuntime: {
1019
+ listScheduledTasks: vi.fn(async () => [goalTask]),
1020
+ updateScheduledTaskState,
1021
+ upsertScheduledTask: vi.fn(),
1022
+ },
1023
+ listProjects: vi.fn(async () => [{ id: 'project-1', path: '/tmp/project-1' }]),
1024
+ buildOpenCodeUrl: vi.fn(() => 'http://127.0.0.1:4096'),
1025
+ getOpenCodeAuthHeaders: vi.fn(() => ({})),
1026
+ waitForOpenCodeReady: vi.fn(async () => {}),
1027
+ persistSessionMetadata: vi.fn(async () => ({})),
1028
+ logger: { info: vi.fn(), warn: vi.fn() },
1029
+ runHistoryStore: history,
1030
+ persistSessionGoal,
1031
+ onGoalPersisted,
1032
+ });
1033
+ await runtime.syncProject('project-1');
1034
+ const result = await runtime.runNow('project-1', 'task-1');
907
1035
 
908
- const started = history.startRun.mock.calls[0][0];
909
- expect(history.attachSession).toHaveBeenCalledWith(started.id, 'ses_distill');
910
- expect(history.finishRun).toHaveBeenCalledWith(
911
- started.id,
912
- expect.objectContaining({
913
- status: 'error',
914
- error: 'schedule run timed out',
1036
+ expect(result.ok).toBe(true);
1037
+ expect(client.create).toHaveBeenCalled();
1038
+ expect(writeObjective).toHaveBeenCalledWith('ses_goal_ok', 'finish the migration');
1039
+ expect(persistSessionGoal).toHaveBeenCalledTimes(1);
1040
+ expect(persistSessionGoal.mock.calls[0][0]).toBe('ses_goal_ok');
1041
+ expect(persistSessionGoal.mock.calls[0][1]).toBe('/tmp/project-1');
1042
+ expect(persistSessionGoal.mock.calls[0][2]).toMatchObject({
1043
+ status: 'active',
1044
+ objectiveFile: true,
1045
+ objective: '',
1046
+ tokenBudget: 12_000,
1047
+ tokensUsed: 0,
1048
+ turnsUsed: 0,
1049
+ blockedStreak: 0,
1050
+ note: '',
1051
+ statusReason: '',
1052
+ lastAccountedMessageID: '',
1053
+ });
1054
+ expect(typeof persistSessionGoal.mock.calls[0][2].id).toBe('string');
1055
+ expect(persistSessionGoal.mock.calls[0][2].id.length).toBeGreaterThan(0);
1056
+ expect(onGoalPersisted).toHaveBeenCalledWith(expect.objectContaining({
1057
+ sessionID: 'ses_goal_ok',
1058
+ directory: '/tmp/project-1',
1059
+ metadata: expect.objectContaining({
1060
+ openchamber: expect.objectContaining({
1061
+ goal: expect.objectContaining({ status: 'active' }),
915
1062
  }),
916
- );
917
-
918
- // Release distill after finalize; abort gate must block goal PATCH / prompt.
919
- resolveDistill?.();
920
- await vi.advanceTimersByTimeAsync(0);
921
- await Promise.resolve();
922
- await Promise.resolve();
923
- expect(fetchMock).not.toHaveBeenCalled();
924
- expect(unhandled).toEqual([]);
925
- } finally {
926
- process.off('unhandledRejection', onUnhandled);
927
- resolveDistill?.();
928
- vi.unstubAllGlobals();
929
- vi.useRealTimers();
930
- }
1063
+ }),
1064
+ }));
1065
+ expect(client.prompt).toHaveBeenCalled();
931
1066
  });
932
1067
 
933
- it('goalEnabled PATCH preserves scheduledTask marker alongside goal', async () => {
1068
+ it('goalEnabled persist failure aborts before prompt', async () => {
934
1069
  const history = createHistoryStore();
935
- // Goal runs settle on terminal goal metadata, not the first idle turn.
936
- const client = createSuccessfulClient({ sessionID: 'ses_goal', settlement: 'goal-complete' });
937
- const fetchMock = vi.fn(async (url, init) => {
938
- if (String(url).includes('/session/ses_goal') && init?.method === 'PATCH') {
939
- return { ok: true, text: async () => '' };
940
- }
941
- if (String(url).includes('prompt_async')) {
942
- return { ok: true, text: async () => '' };
943
- }
944
- throw new Error(`unexpected fetch ${url} ${init?.method}`);
1070
+ const client = createSuccessfulClient({ sessionID: 'ses_goal_fail' });
1071
+ const persistSessionGoal = vi.fn(async () => {
1072
+ throw new Error('store unavailable');
945
1073
  });
946
- vi.stubGlobal('fetch', fetchMock);
947
1074
 
948
1075
  const goalTask = {
949
1076
  ...scheduledTask,
@@ -951,7 +1078,6 @@ describe('scheduled-tasks run history and session lifecycle', () => {
951
1078
  ...scheduledTask.execution,
952
1079
  prompt: 'finish the migration',
953
1080
  goalEnabled: true,
954
- goalTokenBudget: 12_000,
955
1081
  },
956
1082
  };
957
1083
  const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
@@ -967,44 +1093,228 @@ describe('scheduled-tasks run history and session lifecycle', () => {
967
1093
  buildOpenCodeUrl: vi.fn(() => 'http://127.0.0.1:4096'),
968
1094
  getOpenCodeAuthHeaders: vi.fn(() => ({})),
969
1095
  waitForOpenCodeReady: vi.fn(async () => {}),
1096
+ persistSessionMetadata: vi.fn(async () => ({})),
970
1097
  logger: { info: vi.fn(), warn: vi.fn() },
971
1098
  runHistoryStore: history,
1099
+ persistSessionGoal,
1100
+ });
1101
+ await runtime.syncProject('project-1');
1102
+ const result = await runtime.runNow('project-1', 'task-1');
1103
+
1104
+ expect(result.ok).toBe(false);
1105
+ expect(result.error).toMatch(/store unavailable/);
1106
+ expect(writeObjective).toHaveBeenCalled();
1107
+ expect(persistSessionGoal).toHaveBeenCalled();
1108
+ expect(client.prompt).not.toHaveBeenCalled();
1109
+ });
1110
+
1111
+ it('ordinary non-goal runs still create a session and settle on the assistant tail', async () => {
1112
+ const history = createHistoryStore();
1113
+ const client = createSuccessfulClient({ sessionID: 'ses_plain' });
1114
+ const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
1115
+ task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
1116
+ }));
1117
+ const runtime = createRuntime(updateScheduledTaskState, {
1118
+ runHistoryStore: history,
1119
+ waitForOpenCodeReady: vi.fn(async () => {}),
972
1120
  });
973
1121
  await runtime.syncProject('project-1');
974
1122
  const result = await runtime.runNow('project-1', 'task-1');
975
1123
 
976
1124
  expect(result.ok).toBe(true);
977
- expect(client.get).toHaveBeenCalled();
978
- const patchCall = fetchMock.mock.calls.find(
979
- ([url, init]) => String(url).includes('/session/ses_goal') && init?.method === 'PATCH',
1125
+ expect(client.create).toHaveBeenCalled();
1126
+ expect(client.prompt).toHaveBeenCalled();
1127
+ expect(writeObjective).not.toHaveBeenCalled();
1128
+ });
1129
+
1130
+ it('settles from the latest-side page: >50 older completed + newest error → error', async () => {
1131
+ const history = createHistoryStore();
1132
+ // order=desc page: newest first. Older completed assistants must not win.
1133
+ const latestPage = [
1134
+ erroredAssistant('msg_latest'),
1135
+ ...Array.from({ length: 49 }, (_, index) => completedAssistant(`msg_old_${index}`)),
1136
+ ];
1137
+ createSuccessfulClient({
1138
+ messageListImpl: async () => ({ data: latestPage }),
1139
+ });
1140
+ const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
1141
+ task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
1142
+ }));
1143
+ const runtime = createRuntime(updateScheduledTaskState, {
1144
+ runHistoryStore: history,
1145
+ waitForOpenCodeReady: vi.fn(async () => {}),
1146
+ });
1147
+ await runtime.syncProject('project-1');
1148
+ const result = await runtime.runNow('project-1', 'task-1');
1149
+
1150
+ expect(result.ok).toBe(false);
1151
+ expect(result.status).toBe('error');
1152
+ expect(result.error).toBe('ProviderError');
1153
+ expect(history.finishRun).toHaveBeenCalledWith(
1154
+ expect.any(String),
1155
+ expect.objectContaining({ status: 'error', error: 'ProviderError' }),
980
1156
  );
981
- expect(patchCall).toBeTruthy();
982
- const body = JSON.parse(patchCall[1].body);
983
- expect(body.metadata.openchamber.scheduledTask).toEqual({
984
- projectID: 'project-1',
985
- taskID: 'task-1',
986
- runID: history.startRun.mock.calls[0][0].id,
987
- name: 'Task',
1157
+ });
1158
+
1159
+ it('settles from the latest-side page: >50 older errors + newest completed → success', async () => {
1160
+ const history = createHistoryStore();
1161
+ const latestPage = [
1162
+ completedAssistant('msg_latest'),
1163
+ ...Array.from({ length: 49 }, (_, index) => erroredAssistant(`msg_old_${index}`)),
1164
+ ];
1165
+ createSuccessfulClient({
1166
+ messageListImpl: async () => ({ data: latestPage }),
988
1167
  });
989
- expect(body.metadata.openchamber.goal).toMatchObject({
990
- status: 'active',
991
- tokenBudget: 12_000,
1168
+ const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
1169
+ task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
1170
+ }));
1171
+ const runtime = createRuntime(updateScheduledTaskState, {
1172
+ runHistoryStore: history,
1173
+ waitForOpenCodeReady: vi.fn(async () => {}),
992
1174
  });
993
- expect(typeof body.metadata.openchamber.goal.objectiveFile).toBe('boolean');
994
- expect(typeof body.metadata.openchamber.goal.objective).toBe('string');
995
- if (!body.metadata.openchamber.goal.objectiveFile) {
996
- expect(body.metadata.openchamber.goal.objective.length).toBeGreaterThan(0);
997
- }
998
- expect(client.abort).not.toHaveBeenCalled();
1175
+ await runtime.syncProject('project-1');
1176
+ const result = await runtime.runNow('project-1', 'task-1');
999
1177
 
1000
- vi.unstubAllGlobals();
1178
+ expect(result.ok).toBe(true);
1179
+ expect(history.finishRun).toHaveBeenCalledWith(
1180
+ expect.any(String),
1181
+ expect.objectContaining({ status: 'success' }),
1182
+ );
1001
1183
  });
1002
1184
 
1003
- it('successful runs never call session.abort', async () => {
1185
+ it('does not succeed when the latest message is model-switched', async () => {
1186
+ vi.useFakeTimers();
1187
+ const history = createHistoryStore();
1188
+ createSuccessfulClient({
1189
+ messageListImpl: async () => ({
1190
+ data: [{
1191
+ id: 'msg_switch',
1192
+ type: 'model-switched',
1193
+ time: { created: Date.now() },
1194
+ model: { providerID: 'openai', id: 'gpt-4.1' },
1195
+ }],
1196
+ }),
1197
+ });
1198
+ const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
1199
+ task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
1200
+ }));
1201
+ const runtime = createRuntime(updateScheduledTaskState, {
1202
+ runHistoryStore: history,
1203
+ waitForOpenCodeReady: vi.fn(async () => {}),
1204
+ maxRunDurationMs: 2_500,
1205
+ });
1206
+ await runtime.syncProject('project-1');
1207
+
1208
+ const runPromise = runtime.runNow('project-1', 'task-1');
1209
+ await vi.advanceTimersByTimeAsync(1_000);
1210
+ expect(history.finishRun).not.toHaveBeenCalled();
1211
+ await vi.advanceTimersByTimeAsync(1_500);
1212
+ const result = await runPromise;
1213
+
1214
+ expect(result.ok).toBe(false);
1215
+ expect(result.error).toBe('schedule run timed out');
1216
+ vi.useRealTimers();
1217
+ });
1218
+
1219
+ it('does not succeed when the assistant finish is tool-calls', async () => {
1220
+ vi.useFakeTimers();
1221
+ const history = createHistoryStore();
1222
+ createSuccessfulClient({
1223
+ messageListImpl: async () => ({
1224
+ data: [{
1225
+ id: 'msg_tools',
1226
+ type: 'assistant',
1227
+ time: { created: 1, completed: 2 },
1228
+ finish: 'tool-calls',
1229
+ }],
1230
+ }),
1231
+ });
1232
+ const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
1233
+ task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
1234
+ }));
1235
+ const runtime = createRuntime(updateScheduledTaskState, {
1236
+ runHistoryStore: history,
1237
+ waitForOpenCodeReady: vi.fn(async () => {}),
1238
+ maxRunDurationMs: 2_500,
1239
+ });
1240
+ await runtime.syncProject('project-1');
1241
+
1242
+ const runPromise = runtime.runNow('project-1', 'task-1');
1243
+ await vi.advanceTimersByTimeAsync(1_000);
1244
+ expect(history.finishRun).not.toHaveBeenCalled();
1245
+ await vi.advanceTimersByTimeAsync(1_500);
1246
+ const result = await runPromise;
1247
+
1248
+ expect(result.ok).toBe(false);
1249
+ expect(result.error).toBe('schedule run timed out');
1250
+ vi.useRealTimers();
1251
+ });
1252
+
1253
+ it('does not succeed after two incomplete assistant idle probes', async () => {
1254
+ vi.useFakeTimers();
1255
+ const history = createHistoryStore();
1256
+ createSuccessfulClient({
1257
+ messageListImpl: async () => ({
1258
+ data: [{
1259
+ id: 'msg_incomplete',
1260
+ type: 'assistant',
1261
+ time: { created: 1 },
1262
+ // no time.completed, no finish
1263
+ }],
1264
+ }),
1265
+ });
1266
+ const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
1267
+ task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
1268
+ }));
1269
+ const runtime = createRuntime(updateScheduledTaskState, {
1270
+ runHistoryStore: history,
1271
+ waitForOpenCodeReady: vi.fn(async () => {}),
1272
+ maxRunDurationMs: 3_500,
1273
+ });
1274
+ await runtime.syncProject('project-1');
1275
+
1276
+ const runPromise = runtime.runNow('project-1', 'task-1');
1277
+ // Two idle polls with incomplete assistant must not finalize as success.
1278
+ await vi.advanceTimersByTimeAsync(2_000);
1279
+ expect(history.finishRun).not.toHaveBeenCalled();
1280
+ await vi.advanceTimersByTimeAsync(1_500);
1281
+ const result = await runPromise;
1282
+
1283
+ expect(result.ok).toBe(false);
1284
+ expect(result.error).toBe('schedule run timed out');
1285
+ expect(history.finishRun).toHaveBeenCalledWith(
1286
+ expect.any(String),
1287
+ expect.objectContaining({ status: 'error', error: 'schedule run timed out' }),
1288
+ );
1289
+ vi.useRealTimers();
1290
+ });
1291
+
1292
+ it('lists settlement messages with order=desc (latest-side page)', async () => {
1293
+ const history = createHistoryStore();
1294
+ const client = createSuccessfulClient();
1295
+ const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
1296
+ task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
1297
+ }));
1298
+ const runtime = createRuntime(updateScheduledTaskState, {
1299
+ runHistoryStore: history,
1300
+ waitForOpenCodeReady: vi.fn(async () => {}),
1301
+ });
1302
+ await runtime.syncProject('project-1');
1303
+ await runtime.runNow('project-1', 'task-1');
1304
+
1305
+ expect(client.messageList).toHaveBeenCalledWith(
1306
+ expect.objectContaining({
1307
+ sessionID: 'ses_1',
1308
+ limit: 50,
1309
+ order: 'desc',
1310
+ }),
1311
+ expect.objectContaining({ signal: expect.any(AbortSignal) }),
1312
+ );
1313
+ });
1314
+
1315
+ it('successful runs never call session.interrupt', async () => {
1004
1316
  const history = createHistoryStore();
1005
1317
  const client = createSuccessfulClient();
1006
- const fetchMock = vi.fn(async () => ({ ok: true, text: async () => '' }));
1007
- vi.stubGlobal('fetch', fetchMock);
1008
1318
 
1009
1319
  const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
1010
1320
  task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
@@ -1017,9 +1327,7 @@ describe('scheduled-tasks run history and session lifecycle', () => {
1017
1327
  const result = await runtime.runNow('project-1', 'task-1');
1018
1328
 
1019
1329
  expect(result.ok).toBe(true);
1020
- expect(client.abort).not.toHaveBeenCalled();
1021
-
1022
- vi.unstubAllGlobals();
1330
+ expect(client.interrupt).not.toHaveBeenCalled();
1023
1331
  });
1024
1332
 
1025
1333
  it('persists lastSessionId when an error run still created a session', async () => {
@@ -1114,14 +1422,8 @@ describe('scheduled-tasks run history and session lifecycle', () => {
1114
1422
  sessionID: 'ses_1',
1115
1423
  }));
1116
1424
 
1117
- client.messages.mockImplementation(async () => ({
1118
- data: [{
1119
- info: {
1120
- id: 'msg_ok',
1121
- role: 'assistant',
1122
- time: { completed: Date.now() },
1123
- },
1124
- }],
1425
+ client.messageList.mockImplementation(async () => ({
1426
+ data: [completedAssistant('msg_ok')],
1125
1427
  }));
1126
1428
 
1127
1429
  await runtime.observeSessionEvent({
@@ -1165,14 +1467,8 @@ describe('scheduled-tasks run history and session lifecycle', () => {
1165
1467
  expect(result.status).toBe('error');
1166
1468
  expect(history.finishRun).toHaveBeenCalledTimes(1);
1167
1469
 
1168
- client.messages.mockImplementation(async () => ({
1169
- data: [{
1170
- info: {
1171
- id: 'msg_ok',
1172
- role: 'assistant',
1173
- time: { completed: Date.now() },
1174
- },
1175
- }],
1470
+ client.messageList.mockImplementation(async () => ({
1471
+ data: [completedAssistant('msg_ok')],
1176
1472
  }));
1177
1473
 
1178
1474
  await runtime.observeSessionEvent({
@@ -1303,14 +1599,8 @@ describe('scheduled-tasks run history and session lifecycle', () => {
1303
1599
  const stateWritesAfterRun = updateScheduledTaskState.mock.calls.length;
1304
1600
  const successEmitsAfterRun = emitTaskRunEvent.mock.calls.filter((call) => call[0].status === 'success').length;
1305
1601
 
1306
- client.messages.mockImplementation(async () => ({
1307
- data: [{
1308
- info: {
1309
- id: 'msg_ok',
1310
- role: 'assistant',
1311
- time: { completed: Date.now() },
1312
- },
1313
- }],
1602
+ client.messageList.mockImplementation(async () => ({
1603
+ data: [completedAssistant('msg_ok')],
1314
1604
  }));
1315
1605
 
1316
1606
  await runtime.observeSessionEvent({
@@ -1369,6 +1659,7 @@ describe('scheduled-tasks cross-process slot occupancy', () => {
1369
1659
  waitForOpenCodeReady: vi.fn(async () => {
1370
1660
  throw new Error('OpenCode unavailable');
1371
1661
  }),
1662
+ persistSessionMetadata: vi.fn(async () => ({})),
1372
1663
  logger: { info: vi.fn(), warn: vi.fn() },
1373
1664
  runHistoryStore: store,
1374
1665
  notifyTaskRun,