@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,24 +1,52 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
 
3
- vi.mock('@opencode-ai/sdk/v2', () => ({ createOpencodeClient: vi.fn() }));
4
- const { createOpencodeClient } = await import('@opencode-ai/sdk/v2');
3
+ vi.mock('../opencode/v2-client.js', () => ({ makeOpenCodeV2Client: vi.fn() }));
4
+ const { makeOpenCodeV2Client } = await import('../opencode/v2-client.js');
5
5
  const { createOpenCodeMessageQueueAdapter } = await import('./opencode-adapter.js');
6
6
  const { createSessionTurnGate } = await import('./session-turn-gate.js');
7
7
 
8
+ const notFound = (status = 404) => Object.assign(new Error('not_found'), { status, code: 'not_found' });
9
+ const v2Client = ({ active = {}, list = { data: [] }, inbox = [], message } = {}) => ({
10
+ session: {
11
+ active: vi.fn(async () => active),
12
+ inbox: { list: vi.fn(async () => inbox) },
13
+ message: vi.fn(async ({ messageID }) => {
14
+ if (typeof message === 'function') return message(messageID);
15
+ if (message && typeof message === 'object' && messageID in message) return message[messageID];
16
+ throw notFound();
17
+ }),
18
+ },
19
+ message: { list: vi.fn(async () => list) },
20
+ });
21
+
8
22
  describe('OpenCode message queue adapter', () => {
9
23
  it('waits for readiness without forwarding worker options', async () => {
10
24
  const waitForReady = vi.fn(); const adapter = createOpenCodeMessageQueueAdapter({ waitForReady });
11
25
  await adapter.waitForReady({ signal: new AbortController().signal });
12
26
  expect(waitForReady).toHaveBeenCalledWith();
13
27
  });
14
- it('captures runtime, materializes text and files, and sends the captured configuration', async () => {
15
- const promptAsync = vi.fn(() => ({ data: {}, response: { status: 204 } })); createOpencodeClient.mockReturnValue({ session: { promptAsync, messages: vi.fn(() => ({ data: [] })) } });
28
+ it('captures runtime, materializes text and files, and applies sendConfig before prompt', async () => {
29
+ const fetchMock = vi.fn(async () => ({ ok: true, status: 204, json: async () => ({}) }));
30
+ vi.stubGlobal('fetch', fetchMock);
31
+ makeOpenCodeV2Client.mockReturnValue(v2Client());
16
32
  let generation = 1;
17
33
  const adapter = createOpenCodeMessageQueueAdapter({ waitForReady: vi.fn(), buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({ Authorization: 'secret' }), getRuntimeConfig: () => ({ apiBaseUrl: 'http://open.code' }), getRuntimeGeneration: () => generation, getSessionEligibility: () => ({ idle: true, settled: true }), getLatestMessageID: () => 'old', readAttachment: () => ({ type: 'file', url: 'file:///attachment' }) });
18
34
  const runtime = adapter.captureRuntime(); const scope = { sessionID: 'session', directory: '/repo' }; expect(await adapter.checkEligibility(scope)).toMatchObject({ available: true, idle: true, settled: true, latestMessageID: 'old' });
19
35
  const parts = await adapter.materializeAttachments({ content: 'text', attachments: [{}] }); expect(parts).toEqual([{ type: 'text', text: 'text' }, { type: 'file', url: 'file:///attachment' }]);
20
- await adapter.send({ scope, messageID: 'message', runtime, sendConfig: { providerID: 'p', modelID: 'm', agent: 'a', variant: 'v' }, parts }); expect(promptAsync).toHaveBeenCalledWith(expect.objectContaining({ sessionID: 'session', directory: '/repo', messageID: 'message', model: { providerID: 'p', modelID: 'm' }, agent: 'a', variant: 'v', parts }), expect.any(Object));
36
+ await adapter.send({ scope, messageID: 'message', runtime, sendConfig: { providerID: 'p', modelID: 'm', agent: 'a', variant: 'v' }, parts });
37
+ expect(fetchMock).toHaveBeenCalledTimes(3);
38
+ expect(String(fetchMock.mock.calls[0][0])).toContain('/api/session/session/agent');
39
+ expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ agent: 'a' });
40
+ expect(String(fetchMock.mock.calls[1][0])).toContain('/api/session/session/model');
41
+ expect(JSON.parse(fetchMock.mock.calls[1][1].body)).toEqual({ model: { id: 'm', providerID: 'p', variant: 'v' } });
42
+ expect(String(fetchMock.mock.calls[2][0])).toContain('/api/session/session/prompt');
43
+ const promptBody = JSON.parse(fetchMock.mock.calls[2][1].body);
44
+ expect(promptBody).toEqual(expect.objectContaining({ id: 'message', delivery: 'steer', text: 'text' }));
45
+ expect(promptBody.model).toBeUndefined();
46
+ expect(promptBody.agent).toBeUndefined();
47
+ expect(promptBody.variant).toBeUndefined();
21
48
  generation = 2; expect(await adapter.send({ scope, runtime, sendConfig: { providerID: 'p', modelID: 'm' } })).toMatchObject({ code: 'runtime_stale' });
49
+ vi.unstubAllGlobals();
22
50
  });
23
51
  it('materializes Assistant attachment IDs in delivery order', async () => {
24
52
  const readAttachment = vi.fn((attachment) => ({ type: 'file', url: `file:///${attachment.attachmentID}`, filename: attachment.filename }));
@@ -30,110 +58,352 @@ describe('OpenCode message queue adapter', () => {
30
58
  expect(readAttachment).toHaveBeenCalledWith({ attachmentID: 'image', filename: 'image.png' }, expect.any(Object), expect.any(Object));
31
59
  });
32
60
  it('uses the injected upstream runtime URL and detects its changes', async () => {
33
- const promptAsync = vi.fn(() => ({ data: {}, response: { status: 204 } }));
34
- createOpencodeClient.mockReturnValue({ session: { promptAsync, messages: vi.fn(() => ({ data: [] })) } });
61
+ const fetchMock = vi.fn(async () => ({ ok: true, status: 204, json: async () => ({}) }));
62
+ vi.stubGlobal('fetch', fetchMock);
63
+ makeOpenCodeV2Client.mockReturnValue(v2Client());
35
64
  let upstreamUrl = 'http://opencode-upstream:4096/';
36
65
  const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => upstreamUrl, getOpenCodeAuthHeaders: () => ({}), getRuntimeConfig: () => ({ apiBaseUrl: upstreamUrl }), readAttachment: () => null });
37
66
  const runtime = adapter.captureRuntime();
38
67
  await adapter.send({ scope: { sessionID: 's', directory: '/repo' }, messageID: 'msg_1', runtime, sendConfig: { providerID: 'p', modelID: 'm' }, parts: [] });
39
- expect(createOpencodeClient).toHaveBeenCalledWith(expect.objectContaining({ baseUrl: 'http://opencode-upstream:4096' }));
68
+ const promptCall = fetchMock.mock.calls.find(([url]) => String(url).includes('/prompt'));
69
+ expect(String(promptCall[0])).toContain('http://opencode-upstream:4096/api/session/s/prompt');
40
70
  upstreamUrl = 'http://opencode-upstream:4097/';
41
71
  expect(adapter.isCurrent(runtime)).toBe(false);
72
+ vi.unstubAllGlobals();
42
73
  });
43
74
  it('classifies SDK results and exact reconciliation matches without exposing transport details', async () => {
44
- const messages = vi.fn(() => ({ data: [{ id: 'other' }, { id: 'wanted' }] })); createOpencodeClient.mockReturnValue({ session: { promptAsync: vi.fn(() => ({ error: {}, response: { status: 503 } })), messages } });
75
+ const fetchMock = vi.fn(async () => ({ ok: false, status: 503, json: async () => ({}) }));
76
+ vi.stubGlobal('fetch', fetchMock);
77
+ makeOpenCodeV2Client.mockReturnValue(v2Client({ inbox: [{ id: 'wanted', sessionID: 's' }] }));
45
78
  const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), getSessionEligibility: () => ({ idle: true, settled: true }), getLatestMessageID: () => null, readAttachment: () => null });
46
- expect(await adapter.send({ scope: { sessionID: 's', directory: '/d' }, messageID: 'm', sendConfig: { providerID: 'p', modelID: 'm' } })).toMatchObject({ kind: 'ambiguous', status: 503 }); expect(await adapter.findMessage({ sessionID: 's', directory: '/d' }, 'wanted')).toEqual({ found: true });
79
+ // Prompt-only path (no sendConfig) classifies transport status on the POST itself.
80
+ expect(await adapter.send({ scope: { sessionID: 's', directory: '/d' }, messageID: 'm', parts: [] })).toMatchObject({ kind: 'ambiguous', status: 503 });
81
+ expect(await adapter.findMessage({ sessionID: 's', directory: '/d' }, 'wanted')).toEqual({ found: true });
82
+ vi.unstubAllGlobals();
47
83
  });
48
84
  it('treats explicit 2xx empty bodies as ok and never treats undefined/malformed results as success', async () => {
49
- const promptAsync = vi.fn()
50
- .mockReturnValueOnce({ data: undefined, error: null, response: { status: 204 } })
51
- .mockReturnValueOnce({ data: undefined, error: undefined, response: { status: 202 } })
52
- .mockReturnValueOnce({ data: {}, error: null, response: { status: 200 } })
53
- .mockReturnValueOnce(undefined)
54
- .mockReturnValueOnce(null)
55
- .mockReturnValueOnce({});
56
- createOpencodeClient.mockReturnValue({ session: { promptAsync, messages: vi.fn(() => ({ data: [] })) } });
85
+ const fetchMock = vi.fn()
86
+ .mockResolvedValueOnce({ ok: true, status: 204, json: async () => undefined })
87
+ .mockResolvedValueOnce({ ok: true, status: 202, json: async () => undefined })
88
+ .mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({}) })
89
+ .mockResolvedValueOnce(undefined)
90
+ .mockResolvedValueOnce(null)
91
+ .mockResolvedValueOnce({});
92
+ vi.stubGlobal('fetch', fetchMock);
57
93
  const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), getRuntimeConfig: () => ({ apiBaseUrl: 'http://open.code' }), readAttachment: () => null });
58
- const base = { scope: { sessionID: 's', directory: '/d' }, messageID: 'm', sendConfig: { providerID: 'p', modelID: 'm' }, parts: [] };
94
+ const base = { scope: { sessionID: 's', directory: '/d' }, messageID: 'm', parts: [] };
59
95
  await expect(adapter.send(base)).resolves.toMatchObject({ ok: true, status: 204 });
60
96
  await expect(adapter.send(base)).resolves.toMatchObject({ ok: true, status: 202 });
61
97
  await expect(adapter.send(base)).resolves.toMatchObject({ ok: true, status: 200 });
62
98
  await expect(adapter.send(base)).resolves.toMatchObject({ ok: false, kind: 'ambiguous', code: 'malformed_result' });
63
99
  await expect(adapter.send(base)).resolves.toMatchObject({ ok: false, kind: 'ambiguous', code: 'malformed_result' });
64
100
  await expect(adapter.send(base)).resolves.toMatchObject({ ok: false, kind: 'ambiguous', code: 'malformed_result' });
101
+ vi.unstubAllGlobals();
65
102
  });
66
103
  it('reads failure status from error.status and classifies definitive 4xx failures', async () => {
67
- const promptAsync = vi.fn()
68
- .mockReturnValueOnce({ error: { status: 400 }, response: undefined })
69
- .mockReturnValueOnce({ error: {}, response: { status: 422 } })
70
- .mockReturnValueOnce({ error: { status: 429 } })
71
- .mockReturnValueOnce({ error: { status: 408 } });
72
- createOpencodeClient.mockReturnValue({ session: { promptAsync, messages: vi.fn(() => ({ data: [] })) } });
104
+ const fetchMock = vi.fn()
105
+ .mockResolvedValueOnce({ ok: false, status: 400, json: async () => ({}) })
106
+ .mockResolvedValueOnce({ ok: false, status: 422, json: async () => ({}) })
107
+ .mockResolvedValueOnce({ ok: false, status: 429, json: async () => ({}) })
108
+ .mockResolvedValueOnce({ ok: false, status: 408, json: async () => ({}) })
109
+ .mockResolvedValueOnce({ ok: false, status: 409, json: async () => ({ _tag: 'SessionBusyError' }) });
110
+ vi.stubGlobal('fetch', fetchMock);
73
111
  const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), readAttachment: () => null });
74
- const base = { scope: { sessionID: 's', directory: '/d' }, messageID: 'm', sendConfig: { providerID: 'p', modelID: 'm' }, parts: [] };
112
+ const base = { scope: { sessionID: 's', directory: '/d' }, messageID: 'm', parts: [] };
75
113
  await expect(adapter.send(base)).resolves.toMatchObject({ ok: false, kind: 'failed', status: 400 });
76
114
  await expect(adapter.send(base)).resolves.toMatchObject({ ok: false, kind: 'failed', status: 422 });
77
115
  await expect(adapter.send(base)).resolves.toMatchObject({ ok: false, kind: 'ambiguous', status: 429 });
78
116
  await expect(adapter.send(base)).resolves.toMatchObject({ ok: false, kind: 'ambiguous', status: 408 });
117
+ await expect(adapter.send(base)).resolves.toMatchObject({ ok: false, kind: 'retry', code: 'session_busy' });
118
+ vi.unstubAllGlobals();
79
119
  });
80
120
  it('prefers client.v2.session.message exact lookup and treats 404 as found:false', async () => {
81
- const exact = vi.fn()
82
- .mockReturnValueOnce({ data: { info: { id: 'wanted' } } })
83
- .mockReturnValueOnce({ error: { status: 404 }, response: { status: 404 } });
84
- const messages = vi.fn(() => ({ data: [{ id: 'other' }] }));
85
- createOpencodeClient.mockReturnValue({ v2: { session: { message: exact } }, session: { messages, promptAsync: vi.fn() } });
121
+ const api = v2Client({
122
+ inbox: [],
123
+ message: { wanted: { id: 'wanted', type: 'user' } },
124
+ });
125
+ makeOpenCodeV2Client.mockReturnValue(api);
86
126
  const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), readAttachment: () => null });
87
127
  await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'wanted')).resolves.toEqual({ found: true });
88
128
  await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'missing')).resolves.toEqual({ found: false });
89
- expect(messages).not.toHaveBeenCalled();
90
- expect(exact).toHaveBeenCalledWith({ sessionID: 's', messageID: 'wanted', directory: '/d' }, expect.any(Object));
91
- });
92
- it('falls back from unsupported exact lookup to legacy session.message then bounded messages', async () => {
93
- const legacyMessage = vi.fn()
94
- .mockReturnValueOnce({ data: { info: { id: 'legacy-hit' } } })
95
- .mockReturnValueOnce({ error: { status: 404 }, response: { status: 404 } });
96
- const messages = vi.fn(() => ({ data: [{ id: 'bounded-hit' }] }));
97
- createOpencodeClient
98
- .mockReturnValueOnce({ session: { message: legacyMessage, messages, promptAsync: vi.fn() } })
99
- .mockReturnValueOnce({ session: { message: legacyMessage, messages, promptAsync: vi.fn() } })
100
- .mockReturnValueOnce({ session: { messages, promptAsync: vi.fn() } });
129
+ expect(api.session.inbox.list).toHaveBeenCalled();
130
+ expect(api.session.message).toHaveBeenCalledWith({ sessionID: 's', messageID: 'wanted' }, expect.any(Object));
131
+ });
132
+ it('reconciles inbox then session.message without session.messages or 1.x fallback', async () => {
133
+ const api = v2Client({
134
+ inbox: [{ id: 'legacy-hit' }],
135
+ message: { 'bounded-hit': { id: 'bounded-hit', type: 'user' } },
136
+ });
137
+ makeOpenCodeV2Client.mockReturnValue(api);
101
138
  const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), readAttachment: () => null });
102
139
  await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'legacy-hit')).resolves.toEqual({ found: true });
103
140
  await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'missing')).resolves.toEqual({ found: false });
104
141
  await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'bounded-hit')).resolves.toEqual({ found: true });
105
- expect(messages).toHaveBeenCalledTimes(1);
142
+ expect(api.session.status).toBeUndefined();
143
+ expect(api.session.messages).toBeUndefined();
144
+ expect(api.session.promptAsync).toBeUndefined();
106
145
  });
107
- it('falls back when v2 exact lookup returns 405/501 unsupported', async () => {
108
- const exact = vi.fn().mockReturnValueOnce({ error: { status: 405 }, response: { status: 405 } });
109
- const messages = vi.fn(() => ({ data: [{ info: { id: 'wanted' } }] }));
110
- createOpencodeClient.mockReturnValue({ v2: { session: { message: exact } }, session: { messages, promptAsync: vi.fn() } });
146
+ it('marks non-404 inbox/message failures unavailable instead of falling back', async () => {
147
+ const api = v2Client();
148
+ api.session.inbox.list.mockRejectedValue(Object.assign(new Error('unsupported'), { status: 405 }));
149
+ makeOpenCodeV2Client.mockReturnValue(api);
111
150
  const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), readAttachment: () => null });
112
- await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'wanted')).resolves.toEqual({ found: true });
113
- expect(messages).toHaveBeenCalledTimes(1);
151
+ await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'wanted')).resolves.toEqual({ unavailable: true });
114
152
  });
115
- it('uses absent session status as idle and derives settlement from the message tail', async () => {
116
- const messages = vi.fn(() => ({ data: [] })); createOpencodeClient.mockReturnValue({ session: { messages, status: vi.fn(() => ({ data: {} })) } });
153
+ it('uses absent session.active membership as idle and derives settlement from type and time.completed', async () => {
154
+ const list = vi.fn(async () => ({ data: [] }));
155
+ const active = vi.fn(async () => ({}));
156
+ makeOpenCodeV2Client.mockReturnValue({ session: { active, inbox: { list: vi.fn() }, message: vi.fn() }, message: { list } });
117
157
  const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), readAttachment: () => null });
118
158
  expect(await adapter.checkEligibility({ sessionID: 's', directory: '/d' })).toMatchObject({ idle: true, settled: true });
119
- messages.mockReturnValueOnce({ data: [{ info: { id: 'user', role: 'user' } }] }); expect((await adapter.checkEligibility({ sessionID: 's', directory: '/d' })).settled).toBe(false);
120
- messages.mockReturnValueOnce({ data: [{ info: { id: 'assistant', role: 'assistant', time: { completed: 1 } } }] }); expect((await adapter.checkEligibility({ sessionID: 's', directory: '/d' })).settled).toBe(true);
159
+ list.mockResolvedValueOnce({ data: [{ id: 'user', type: 'user', time: { created: 1 } }] });
160
+ expect((await adapter.checkEligibility({ sessionID: 's', directory: '/d' })).settled).toBe(false);
161
+ list.mockResolvedValueOnce({ data: [{ id: 'assistant', type: 'assistant', time: { completed: 1 } }] });
162
+ expect((await adapter.checkEligibility({ sessionID: 's', directory: '/d' })).settled).toBe(true);
163
+ expect(list).toHaveBeenCalledWith({ sessionID: 's', limit: 1, order: 'desc' }, expect.any(Object));
164
+ });
165
+ it('treats a sessionID present in session.active as not idle', async () => {
166
+ makeOpenCodeV2Client.mockReturnValue(v2Client({
167
+ active: { s: { type: 'running' } },
168
+ list: { data: [{ id: 'assistant', type: 'assistant', time: { completed: 1 } }] },
169
+ }));
170
+ const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), readAttachment: () => null });
171
+ expect(await adapter.checkEligibility({ sessionID: 's', directory: '/d' })).toMatchObject({ available: true, idle: false });
121
172
  });
122
173
  it('recovers an idle incomplete assistant tail without treating fetch failure as empty', async () => {
123
174
  let now = 0; const turnGate = createSessionTurnGate({ clock: () => now });
124
- const messages = vi.fn(() => ({ data: [{ info: { id: 'assistant', role: 'assistant', time: { created: 1 } } }] }));
125
- const status = vi.fn(() => ({ data: {} })); createOpencodeClient.mockReturnValue({ session: { messages, status } });
175
+ const list = vi.fn(async () => ({ data: [{ id: 'assistant', type: 'assistant', time: { created: 1 } }] }));
176
+ const active = vi.fn(async () => ({}));
177
+ makeOpenCodeV2Client.mockReturnValue({ session: { active, inbox: { list: vi.fn() }, message: vi.fn() }, message: { list } });
126
178
  const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), readAttachment: () => null, turnGate });
127
179
  expect(await adapter.checkEligibility({ sessionID: 's', directory: '/d' })).toMatchObject({ settled: false, settlementReason: 'tail_unsettled' });
128
- status.mockReturnValueOnce({ error: {} }); now = 3_000;
180
+ active.mockRejectedValueOnce(Object.assign(new Error('upstream'), { status: 500 })); now = 3_000;
129
181
  await expect(adapter.checkEligibility({ sessionID: 's', directory: '/d' })).resolves.toEqual({ available: false, idle: false, settled: false });
130
182
  expect(await adapter.checkEligibility({ sessionID: 's', directory: '/d' })).toMatchObject({ settled: false, settlementReason: 'tail_unsettled' });
131
183
  now = 6_000;
132
184
  expect(await adapter.checkEligibility({ sessionID: 's', directory: '/d' })).toMatchObject({ settled: true, settlementReason: 'stopped_assistant' });
133
185
  });
134
186
  it('marks malformed or failed authoritative eligibility reads unavailable', async () => {
135
- createOpencodeClient.mockReturnValue({ session: { messages: vi.fn(() => ({ data: [] })), status: vi.fn(() => ({ error: {} })) } });
187
+ makeOpenCodeV2Client.mockReturnValue({
188
+ session: { active: vi.fn(async () => { throw Object.assign(new Error('upstream'), { status: 500 }); }), inbox: { list: vi.fn() }, message: vi.fn() },
189
+ message: { list: vi.fn(async () => ({ data: [] })) },
190
+ });
136
191
  const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), readAttachment: () => null });
137
192
  await expect(adapter.checkEligibility({ sessionID: 's', directory: '/d' })).resolves.toEqual({ available: false, idle: false, settled: false });
138
193
  });
139
194
  });
195
+
196
+ describe('sendConfig serial boundary (v2 prompt contract)', () => {
197
+ it('applies different models in agent→model→prompt order per item', async () => {
198
+ const calls = [];
199
+ const fetchMock = vi.fn(async (url, init) => {
200
+ calls.push({ url: String(url), body: init?.body ? JSON.parse(init.body) : null });
201
+ return { ok: true, status: 204, json: async () => ({}) };
202
+ });
203
+ vi.stubGlobal('fetch', fetchMock);
204
+ const adapter = createOpenCodeMessageQueueAdapter({
205
+ buildOpenCodeUrl: () => 'http://open.code/',
206
+ getOpenCodeAuthHeaders: () => ({}),
207
+ readAttachment: () => null,
208
+ });
209
+ const scope = { sessionID: 's', directory: '/d' };
210
+ await adapter.send({
211
+ scope, messageID: 'msg_a', parts: [{ type: 'text', text: 'first' }],
212
+ sendConfig: { providerID: 'openai', modelID: 'gpt-a', agent: 'build', variant: 'fast' },
213
+ });
214
+ await adapter.send({
215
+ scope, messageID: 'msg_b', parts: [{ type: 'text', text: 'second' }],
216
+ sendConfig: { providerID: 'anthropic', modelID: 'claude-b', agent: 'plan' },
217
+ });
218
+ expect(calls.map((c) => c.url.replace(/^.*\/api\/session\/s\//, ''))).toEqual([
219
+ 'agent?directory=%2Fd',
220
+ 'model?directory=%2Fd',
221
+ 'prompt?directory=%2Fd',
222
+ 'agent?directory=%2Fd',
223
+ 'model?directory=%2Fd',
224
+ 'prompt?directory=%2Fd',
225
+ ]);
226
+ expect(calls[0].body).toEqual({ agent: 'build' });
227
+ expect(calls[1].body).toEqual({ model: { id: 'gpt-a', providerID: 'openai', variant: 'fast' } });
228
+ expect(calls[2].body).toEqual(expect.objectContaining({ id: 'msg_a', text: 'first', delivery: 'steer' }));
229
+ expect(calls[2].body.model).toBeUndefined();
230
+ expect(calls[3].body).toEqual({ agent: 'plan' });
231
+ expect(calls[4].body).toEqual({ model: { id: 'claude-b', providerID: 'anthropic' } });
232
+ expect(calls[5].body).toEqual(expect.objectContaining({ id: 'msg_b', text: 'second' }));
233
+ vi.unstubAllGlobals();
234
+ });
235
+
236
+ it('does not POST prompt when model switch fails', async () => {
237
+ const fetchMock = vi.fn(async (url) => {
238
+ if (String(url).includes('/model')) return { ok: false, status: 400, json: async () => ({ _tag: 'InvalidRequestError' }) };
239
+ return { ok: true, status: 204, json: async () => ({}) };
240
+ });
241
+ vi.stubGlobal('fetch', fetchMock);
242
+ const adapter = createOpenCodeMessageQueueAdapter({
243
+ buildOpenCodeUrl: () => 'http://open.code/',
244
+ getOpenCodeAuthHeaders: () => ({}),
245
+ readAttachment: () => null,
246
+ });
247
+ const result = await adapter.send({
248
+ scope: { sessionID: 's', directory: '/d' },
249
+ messageID: 'msg_fail',
250
+ parts: [{ type: 'text', text: 'nope' }],
251
+ sendConfig: { providerID: 'p', modelID: 'm', agent: 'a' },
252
+ });
253
+ expect(result).toMatchObject({ ok: false, kind: 'failed', code: 'config_switch_failed', status: 400 });
254
+ expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/prompt'))).toBe(false);
255
+ expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/agent'))).toBe(true);
256
+ expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/model'))).toBe(true);
257
+ vi.unstubAllGlobals();
258
+ });
259
+
260
+ it('treats session busy on switch as retry without prompt', async () => {
261
+ const fetchMock = vi.fn(async (url) => {
262
+ if (String(url).includes('/agent')) return { ok: false, status: 409, json: async () => ({ _tag: 'SessionBusyError' }) };
263
+ return { ok: true, status: 200, json: async () => ({}) };
264
+ });
265
+ vi.stubGlobal('fetch', fetchMock);
266
+ const adapter = createOpenCodeMessageQueueAdapter({
267
+ buildOpenCodeUrl: () => 'http://open.code/',
268
+ getOpenCodeAuthHeaders: () => ({}),
269
+ readAttachment: () => null,
270
+ });
271
+ const result = await adapter.send({
272
+ scope: { sessionID: 's', directory: '/d' },
273
+ messageID: 'msg_busy',
274
+ parts: [{ type: 'text', text: 'wait' }],
275
+ sendConfig: { providerID: 'p', modelID: 'm', agent: 'a' },
276
+ });
277
+ expect(result).toMatchObject({ ok: false, kind: 'retry', code: 'session_busy' });
278
+ expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/prompt'))).toBe(false);
279
+ vi.unstubAllGlobals();
280
+ });
281
+
282
+ it('maps MessageNotFoundError _tag from projection to found:false', async () => {
283
+ const api = v2Client({ inbox: [] });
284
+ api.session.message.mockRejectedValue({
285
+ _tag: 'MessageNotFoundError',
286
+ sessionID: 's',
287
+ messageID: 'missing',
288
+ message: 'not found',
289
+ });
290
+ makeOpenCodeV2Client.mockReturnValue(api);
291
+ const adapter = createOpenCodeMessageQueueAdapter({
292
+ buildOpenCodeUrl: () => 'http://open.code/',
293
+ getOpenCodeAuthHeaders: () => ({}),
294
+ readAttachment: () => null,
295
+ });
296
+ await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'missing')).resolves.toEqual({ found: false });
297
+ });
298
+
299
+ it('awaits projection rejections so non-404 stays unavailable instead of escaping', async () => {
300
+ const api = v2Client({ inbox: [] });
301
+ api.session.message.mockRejectedValue(Object.assign(new Error('boom'), { status: 500 }));
302
+ makeOpenCodeV2Client.mockReturnValue(api);
303
+ const adapter = createOpenCodeMessageQueueAdapter({
304
+ buildOpenCodeUrl: () => 'http://open.code/',
305
+ getOpenCodeAuthHeaders: () => ({}),
306
+ readAttachment: () => null,
307
+ });
308
+ await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'wanted')).resolves.toEqual({ unavailable: true });
309
+ });
310
+ });
311
+
312
+ describe('intent queue prompt/inbox authority (ticket 12)', () => {
313
+ const jsonResponse = (status, body) => ({
314
+ ok: status >= 200 && status < 300,
315
+ status,
316
+ json: async () => body,
317
+ text: async () => JSON.stringify(body),
318
+ headers: { get: () => 'application/json' },
319
+ });
320
+
321
+ it('sends via v2 prompt/inbox and does not treat local parts as the body of record', async () => {
322
+ const fetchMock = vi.fn(async () => jsonResponse(200, {
323
+ id: 'msg_inbox',
324
+ sessionID: 's',
325
+ delivery: 'steer',
326
+ payload: { text: 'from-projection' },
327
+ }));
328
+ vi.stubGlobal('fetch', fetchMock);
329
+ const adapter = createOpenCodeMessageQueueAdapter({
330
+ buildOpenCodeUrl: (pathname) => `http://open.code${pathname}`,
331
+ getOpenCodeAuthHeaders: () => ({ Authorization: 'Basic secret' }),
332
+ readAttachment: () => ({ type: 'file', url: 'file:///staged' }),
333
+ });
334
+ const localParts = [{ type: 'text', text: 'stale-local-parts' }];
335
+ const result = await adapter.send({
336
+ scope: { sessionID: 's', directory: '/d' },
337
+ messageID: 'msg_inbox',
338
+ sendConfig: { providerID: 'p', modelID: 'm' },
339
+ parts: localParts,
340
+ content: 'stale-local-parts',
341
+ });
342
+ expect(result).toMatchObject({ ok: true, status: 200 });
343
+ const promptCall = fetchMock.mock.calls.find(([url]) => String(url).includes('/prompt'));
344
+ expect(promptCall).toBeTruthy();
345
+ const [url, init] = promptCall;
346
+ expect(String(url)).toContain('/api/session/s/prompt');
347
+ const body = JSON.parse(init.body);
348
+ expect(body.delivery).toMatch(/steer|queue/);
349
+ expect(body.id).toBe('msg_inbox');
350
+ expect(body.model).toBeUndefined();
351
+ expect(init.method).toBe('POST');
352
+ vi.unstubAllGlobals();
353
+ });
354
+
355
+ it('reconciles only against OpenCode inbox and projection, never a local parts scan', async () => {
356
+ const list = vi.fn(async () => ({ data: [{ id: 'wanted', type: 'user', text: 'local-scan' }] }));
357
+ const api = v2Client({
358
+ inbox: [{ id: 'wanted', sessionID: 's', delivery: 'steer', payload: { text: 'inbox' } }],
359
+ message: { wanted: { id: 'wanted', type: 'user' } },
360
+ });
361
+ api.message.list = list;
362
+ makeOpenCodeV2Client.mockReturnValue(api);
363
+ const adapter = createOpenCodeMessageQueueAdapter({
364
+ buildOpenCodeUrl: (pathname) => `http://open.code${pathname}`,
365
+ getOpenCodeAuthHeaders: () => ({}),
366
+ readAttachment: () => null,
367
+ });
368
+ await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'wanted')).resolves.toEqual({ found: true });
369
+ expect(list).not.toHaveBeenCalled();
370
+ expect(api.session.inbox.list).toHaveBeenCalledWith({ sessionID: 's' }, expect.any(Object));
371
+ });
372
+
373
+ it('keeps staged attachment files until prompt is accepted, then projection wins', async () => {
374
+ const fetchMock = vi.fn(async () => jsonResponse(200, {
375
+ id: 'msg_att',
376
+ sessionID: 's',
377
+ delivery: 'steer',
378
+ payload: { text: 'accepted' },
379
+ }));
380
+ vi.stubGlobal('fetch', fetchMock);
381
+ const readAttachment = vi.fn(() => ({ type: 'file', url: 'file:///staged-disk', filename: 'note.txt' }));
382
+ const adapter = createOpenCodeMessageQueueAdapter({
383
+ buildOpenCodeUrl: (pathname) => `http://open.code${pathname}`,
384
+ getOpenCodeAuthHeaders: () => ({}),
385
+ readAttachment,
386
+ });
387
+ const parts = await adapter.materializeAttachments({
388
+ content: 'caption',
389
+ attachments: [{ attachmentID: 'att-1', filename: 'note.txt' }],
390
+ });
391
+ expect(parts).toEqual([
392
+ { type: 'text', text: 'caption' },
393
+ { type: 'file', url: 'file:///staged-disk', filename: 'note.txt' },
394
+ ]);
395
+ const result = await adapter.send({
396
+ scope: { sessionID: 's', directory: '/d' },
397
+ messageID: 'msg_att',
398
+ sendConfig: { providerID: 'p', modelID: 'm' },
399
+ parts,
400
+ });
401
+ expect(result.ok).toBe(true);
402
+ expect(readAttachment).toHaveBeenCalled();
403
+ const promptCall = fetchMock.mock.calls.find(([url]) => String(url).includes('/prompt'));
404
+ const body = JSON.parse(promptCall[1].body);
405
+ expect(body.files?.length || body.parts).toBeTruthy();
406
+ expect(body.model).toBeUndefined();
407
+ vi.unstubAllGlobals();
408
+ });
409
+ });
@@ -35,13 +35,26 @@ export const createMessageQueueRuntime = ({ dbPath, attachmentRoot, service, att
35
35
  const observeSessionEvent = (event) => {
36
36
  if (!isCurrentRuntimeEvent(event)) return false;
37
37
  const payload = event?.payload?.payload ?? event?.payload;
38
- const properties = payload?.properties;
38
+ // Accept bridge `{ properties }` and native v2 `{ data }` envelopes.
39
+ const properties = (payload?.properties && typeof payload.properties === 'object')
40
+ ? payload.properties
41
+ : ((payload?.data && typeof payload.data === 'object') ? payload.data : null);
39
42
  const directory = typeof event?.directory === 'string' && event.directory !== 'global' ? event.directory : '';
40
43
  const sessionID = typeof properties?.sessionID === 'string' ? properties.sessionID : '';
41
44
  if (!directory || !sessionID) return false;
45
+ // Ticket 07: shutdown interrupt keeps the upstream claim — do not treat as
46
+ // idle/error settlement for queue wake; wait for authoritative terminal.
47
+ if (payload.type === 'session.execution.interrupted' && properties?.reason === 'shutdown') {
48
+ return false;
49
+ }
42
50
  const phase = payload.type === 'session.status'
43
51
  ? properties?.status?.type ?? properties?.info?.type
44
- : payload.type === 'session.idle' ? 'idle' : payload.type === 'session.error' ? 'error' : '';
52
+ : payload.type === 'session.idle' ? 'idle'
53
+ : payload.type === 'session.error' ? 'error'
54
+ : payload.type === 'session.execution.succeeded' ? 'idle'
55
+ : payload.type === 'session.execution.failed' ? 'error'
56
+ : payload.type === 'session.execution.interrupted' ? 'idle'
57
+ : '';
45
58
  if (typeof phase !== 'string' || !phase) return false;
46
59
  openCodeAdapter.observeSessionEvent?.({ directory, sessionID }, phase);
47
60
  return true;
@@ -4,24 +4,24 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { describe, expect, it, vi } from 'vitest';
6
6
 
7
- vi.mock('@opencode-ai/sdk/v2', () => ({ createOpencodeClient: vi.fn() }));
8
- const { createOpencodeClient } = await import('@opencode-ai/sdk/v2');
7
+ vi.mock('../opencode/v2-client.js', () => ({ makeOpenCodeV2Client: vi.fn() }));
8
+ const { makeOpenCodeV2Client } = await import('../opencode/v2-client.js');
9
9
  import { createMessageQueueRuntime } from './runtime.js';
10
10
 
11
11
  describe('message queue runtime ownership', () => {
12
12
  it('keeps the durable queue identity separate from the upstream OpenCode adapter URL', async () => {
13
13
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'message-queue-runtime-'));
14
14
  const uiUrl = 'http://127.0.0.1:57123'; const upstreamUrl = 'http://127.0.0.1:4096/';
15
- const status = vi.fn(() => ({ data: {} })); const messages = vi.fn(() => ({ data: [] }));
16
- createOpencodeClient.mockReturnValue({ session: { status, messages, promptAsync: vi.fn() } });
15
+ const active = vi.fn(() => ({})); const list = vi.fn(() => ({ data: [] }));
16
+ makeOpenCodeV2Client.mockReturnValue({ session: { active, prompt: vi.fn() }, message: { list } });
17
17
  const runtime = createMessageQueueRuntime({ dbPath: path.join(root, 'queue.sqlite'), getRuntimeConfig: () => ({ apiBaseUrl: uiUrl }), getOpenCodeRuntimeConfig: () => ({ apiBaseUrl: upstreamUrl }), buildOpenCodeUrl: () => upstreamUrl, getOpenCodeAuthHeaders: () => ({}) });
18
18
  try {
19
19
  const adapterRuntime = runtime.adapter.captureRuntime();
20
20
  await runtime.adapter.checkEligibility({ directory: '/repo', sessionID: 'session' }, adapterRuntime);
21
21
  expect(runtime.service.getRuntimeKey()).toBe(crypto.createHash('sha256').update(`url:${uiUrl}`).digest('hex'));
22
22
  expect(adapterRuntime.config.apiBaseUrl).toBe(upstreamUrl);
23
- expect(createOpencodeClient).toHaveBeenCalledWith(expect.objectContaining({ baseUrl: 'http://127.0.0.1:4096' }));
24
- expect(createOpencodeClient.mock.calls.map(([options]) => options.baseUrl)).not.toContain(uiUrl);
23
+ expect(makeOpenCodeV2Client).toHaveBeenCalledWith(expect.objectContaining({ baseUrl: 'http://127.0.0.1:4096' }));
24
+ expect(makeOpenCodeV2Client.mock.calls.map(([options]) => options.baseUrl)).not.toContain(uiUrl);
25
25
  } finally {
26
26
  await runtime.stop(); fs.rmSync(root, { recursive: true, force: true });
27
27
  }
@@ -109,7 +109,13 @@ export const createMessageQueueWorker = ({ service, adapter, workerID, concurren
109
109
  };
110
110
  const run = async () => {
111
111
  if (paused || stopping) return status();
112
- const runtimeKey = service.getRuntimeKey(); const runtime = adapter.captureRuntime(); const authority = service.getAuthority({ runtimeKey });
112
+ const runtimeKey = service.getRuntimeKey();
113
+ // The managed OpenCode service may still be booting when the worker starts;
114
+ // captureRuntime throws until its port is detected. Leave queued work
115
+ // pending and retry on the next tick instead of failing the flight.
116
+ let runtime;
117
+ try { runtime = adapter.captureRuntime(); } catch { return status(); }
118
+ const authority = service.getAuthority({ runtimeKey });
113
119
  if (authority?.authority !== 'active') return status();
114
120
  service.recoverExpiredSending?.({ runtimeKey });
115
121
  // Start dispatch probes before awaiting reconciling work so a hung findMessage
@@ -412,6 +412,11 @@ export const createNotificationTriggerRuntime = (deps) => {
412
412
  }
413
413
  }
414
414
  }
415
+ // Ticket 07: shutdown interrupt is not a completion terminal — no ready push.
416
+ if (payload.type === 'session.execution.interrupted') {
417
+ const reason = payload.properties?.reason ?? payload.data?.reason;
418
+ if (reason === 'shutdown') return;
419
+ }
415
420
  if ((payload.type === 'session.idle' || payload.type === 'session.error') && sessionId) {
416
421
  const error = payload.properties?.error;
417
422
  const errorText = typeof error?.message === 'string'