@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,36 +1,32 @@
1
- import { createOpencodeClient } from '@opencode-ai/sdk/v2';
1
+ import { OpenCode } from '@opencode/client';
2
+ import { buildLlmSessionMetadata } from '../session-metadata/system-session.js';
3
+ import { ensureLlmTempDirectory } from './temp-directory.js';
2
4
 
3
5
  const LLM_AGENT_NAME = 'openchamber-llm';
6
+ const TEXT_AGENT_NAME = 'openchamber-text';
4
7
  const GENERATE_TIMEOUT_MS = 90_000;
5
- const SETTLE_POLL_MS = 250;
6
- const INCOMPLETE_ASSISTANT_SETTLE_PROBES = 2;
7
- const EMPTY_IDLE_PROBES = 5;
8
+ const AGENT_READY_WAIT_MS = 5_000;
9
+ const AGENT_READY_POLL_MS = 250;
10
+ /** Single attachment payload cap (decoded bytes). Composer UI allows larger drafts; gateway enforces this. */
11
+ export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
12
+ const MAX_INLINE_FILE_CHARS = 100_000;
13
+ const TEXT_FILE_MIME = /^(text\/|application\/(json|javascript|xml|sql|yaml|x-yaml|toml))/i;
8
14
 
9
15
  const positiveMs = (value, fallback) => (
10
16
  Number.isFinite(value) && value > 0 ? Math.trunc(value) : fallback
11
17
  );
12
18
 
13
19
  /**
14
- * Session-level deny-all. OpenCode 1.18 session.create serializes this
15
- * PermissionRuleset shape (permission/pattern/action). Confirmed on local
16
- * 1.18.23/1.18.29: create returns the same rule on the session object.
17
- */
18
- export const LLM_SESSION_DENY_PERMISSION = Object.freeze([
19
- Object.freeze({ permission: '*', pattern: '*', action: 'deny' }),
20
- ]);
21
-
22
- /**
23
- * Agent frontmatter for OpenCode 1.18. Runtime /agent metadata uses
24
- * permission:[{permission,pattern,action}]. Markdown config accepts the
25
- * PermissionActionConfig map (`"*": deny`) / scalar `deny` / tools:"*":false —
26
- * not the legacy action/resource/effect list (that loads but does not add
27
- * a terminal * deny, so build-like allows and skill guidance stay visible).
20
+ * Agent frontmatter for official client agent permissions (action/resource/effect).
21
+ * The final rule must be deny-all; assertLlmAgentDenyAll verifies that before prompt.
28
22
  */
29
23
  const AGENT_MARKDOWN = `---
30
24
  mode: primary
31
25
  hidden: true
32
- permission:
33
- "*": deny
26
+ permissions:
27
+ - action: "*"
28
+ resource: "*"
29
+ effect: deny
34
30
  ---
35
31
 
36
32
  You generate responses for an application-owned assistant. Follow the supplied system instructions and response format.
@@ -40,51 +36,46 @@ Report execution and failures from supplied tool results only.
40
36
  Do not call native OpenCode tools, MCP tools, or skill tools. Native permissions are denied for this generator session.
41
37
  `;
42
38
 
43
- const isMissing = (result) =>
44
- result?.error?.status === 404
45
- || result?.error?.statusCode === 404
46
- || result?.error?.code === 'not_found'
47
- || result?.status === 404;
48
-
49
- const promptAdmitted = (result) =>
50
- !result?.error
51
- && (result?.response?.status === 204
52
- || result?.status === 204
53
- || result?.data !== undefined
54
- || result?.response?.ok === true);
55
-
56
- const sdkErrorMessage = (result, fallback) => {
57
- const status = result?.error?.status ?? result?.error?.statusCode ?? result?.status;
58
- const message = result?.error?.message || result?.error?.data?.message || fallback;
59
- return status ? `${message} (${status})` : message;
60
- };
39
+ /** Neutral deny-all agent for text-only `session.generate`; the caller's instructions travel in the prompt. */
40
+ const TEXT_AGENT_MARKDOWN = `---
41
+ mode: primary
42
+ hidden: true
43
+ permissions:
44
+ - action: "*"
45
+ resource: "*"
46
+ effect: deny
47
+ ---
61
48
 
62
- const failGenerate = (message) => {
49
+ Follow the instructions in the request and respond in the requested format.
50
+ Native OpenCode, MCP, and skill tools are denied for this generator session.
51
+ When the request defines an application tool-call format (such as fenced JSON), write it as plain text in your reply; the application executes it.
52
+ `;
53
+
54
+ const failGenerate = (message, code = 'upstream_error') => {
63
55
  const error = new Error(message);
64
- error.code = 'upstream_error';
56
+ error.code = code;
65
57
  throw error;
66
58
  };
67
59
 
68
- const sleep = (ms, signal) => new Promise((resolve, reject) => {
69
- if (signal?.aborted) {
70
- reject(signal.reason instanceof Error ? signal.reason : new Error('aborted'));
71
- return;
72
- }
73
- const timer = setTimeout(() => {
74
- signal?.removeEventListener?.('abort', onAbort);
75
- resolve();
76
- }, ms);
77
- const onAbort = () => {
78
- clearTimeout(timer);
79
- reject(signal.reason instanceof Error ? signal.reason : new Error('aborted'));
80
- };
81
- signal?.addEventListener?.('abort', onAbort, { once: true });
82
- });
60
+ const clientErrorMessage = (error, fallback) => {
61
+ if (typeof error?.message === 'string' && error.message.trim()) return error.message;
62
+ if (typeof error === 'string' && error.trim()) return error;
63
+ return fallback;
64
+ };
65
+
66
+ const isAbortLike = (error, signal) => (
67
+ signal?.aborted
68
+ || error?.name === 'AbortError'
69
+ || error?.code === 'ABORT_ERR'
70
+ || (typeof error?.message === 'string' && /aborted|This operation was aborted/i.test(error.message))
71
+ );
83
72
 
84
- const readMessageInfo = (message) => {
85
- if (!message || typeof message !== 'object') return null;
86
- if (message.info && typeof message.info === 'object') return message.info;
87
- return message;
73
+ const rethrowIfAborted = (error, signal) => {
74
+ if (!isAbortLike(error, signal)) return;
75
+ if (error instanceof Error) throw error;
76
+ const next = new Error(clientErrorMessage(error, 'aborted'));
77
+ next.name = 'AbortError';
78
+ throw next;
88
79
  };
89
80
 
90
81
  const assistantErrorDetail = (error) => {
@@ -100,179 +91,6 @@ const assistantErrorDetail = (error) => {
100
91
  return '';
101
92
  };
102
93
 
103
- const assistantTextFromPrompt = (data) => {
104
- const parts = Array.isArray(data?.parts) ? data.parts : Array.isArray(data?.data?.parts) ? data.data.parts : [];
105
- const text = parts
106
- .filter((part) => part?.type === 'text' && typeof part.text === 'string')
107
- .map((part) => part.text)
108
- .join('');
109
- if (text.trim()) return text;
110
- const info = data?.info ?? data?.data?.info;
111
- if (info?.error) {
112
- const detail = assistantErrorDetail(info.error);
113
- if (detail) failGenerate(detail);
114
- }
115
- return '';
116
- };
117
-
118
- const assistantTextFromMessages = (messages) => {
119
- if (!Array.isArray(messages) || messages.length === 0) return '';
120
- for (let i = messages.length - 1; i >= 0; i -= 1) {
121
- const message = messages[i];
122
- const info = readMessageInfo(message);
123
- if (info?.role !== 'assistant') continue;
124
- if (info.error) {
125
- const detail = assistantErrorDetail(info.error);
126
- failGenerate(detail || 'OpenCode assistant error');
127
- }
128
- const parts = Array.isArray(message?.parts) ? message.parts : [];
129
- return parts
130
- .map((part) => (part?.type === 'text' && typeof part.text === 'string' ? part.text : ''))
131
- .filter(Boolean)
132
- .join('');
133
- }
134
- return '';
135
- };
136
-
137
- const deniedTools = (ids) => {
138
- const tools = Object.create(null);
139
- for (const id of ids) {
140
- if (typeof id === 'string' && id.trim()) tools[id.trim()] = false;
141
- }
142
- return tools;
143
- };
144
-
145
- const waitForIdleAssistant = async ({
146
- client,
147
- sessionID,
148
- directory,
149
- signal,
150
- onProgress = null,
151
- settlePollMs = SETTLE_POLL_MS,
152
- }) => {
153
- let incompleteAssistantProbes = 0;
154
- let emptyIdleProbes = 0;
155
- const pollMs = positiveMs(settlePollMs, SETTLE_POLL_MS);
156
-
157
- for (;;) {
158
- signal?.throwIfAborted?.();
159
-
160
- let sessionBusy = false;
161
- try {
162
- const statusResult = await client.session.status({ directory }, { signal });
163
- if (!statusResult?.error && statusResult?.data && typeof statusResult.data === 'object') {
164
- const statusValue = statusResult.data[sessionID];
165
- const type = statusValue?.type ?? statusValue?.status;
166
- sessionBusy = type === 'busy' || type === 'retry';
167
- }
168
- } catch (error) {
169
- if (signal?.aborted) throw error;
170
- }
171
-
172
- if (sessionBusy) {
173
- incompleteAssistantProbes = 0;
174
- emptyIdleProbes = 0;
175
- onProgress?.();
176
- await sleep(pollMs, signal);
177
- continue;
178
- }
179
-
180
- try {
181
- const messagesResult = await client.session.messages({
182
- sessionID,
183
- directory,
184
- limit: 20,
185
- }, { signal });
186
- if (messagesResult?.error) {
187
- // Deterministic upstream failures (e.g. 400) must not look like "still settling".
188
- failGenerate(`OpenCode LLM session.messages failed: ${sdkErrorMessage(messagesResult, 'messages failed')}`);
189
- }
190
- if (Array.isArray(messagesResult?.data)) {
191
- const lastInfo = readMessageInfo(messagesResult.data.at(-1));
192
- if (lastInfo?.role === 'assistant') {
193
- emptyIdleProbes = 0;
194
- if (lastInfo.error) {
195
- const detail = assistantErrorDetail(lastInfo.error);
196
- failGenerate(detail || 'OpenCode assistant error');
197
- }
198
- if (lastInfo.time?.completed) {
199
- return messagesResult.data;
200
- }
201
- incompleteAssistantProbes += 1;
202
- onProgress?.();
203
- if (incompleteAssistantProbes >= INCOMPLETE_ASSISTANT_SETTLE_PROBES) {
204
- return messagesResult.data;
205
- }
206
- } else {
207
- incompleteAssistantProbes = 0;
208
- emptyIdleProbes += 1;
209
- if (emptyIdleProbes >= EMPTY_IDLE_PROBES && lastInfo?.role === 'user') {
210
- failGenerate('OpenCode LLM generator ended without assistant text');
211
- }
212
- }
213
- }
214
- } catch (error) {
215
- if (signal?.aborted) throw error;
216
- if (error?.code === 'upstream_error') throw error;
217
- }
218
-
219
- await sleep(pollMs, signal);
220
- }
221
- };
222
-
223
- const isJsonContentType = (response) => /json/i.test(response?.headers?.get?.('content-type') || '');
224
-
225
- const looksLikeJsonObject = (text) => {
226
- const trimmed = String(text || '').trim();
227
- if (!trimmed || trimmed.startsWith('<')) return false;
228
- try {
229
- const parsed = JSON.parse(trimmed);
230
- return parsed !== null && typeof parsed === 'object';
231
- } catch {
232
- return false;
233
- }
234
- };
235
-
236
- /**
237
- * Detect a sessionless generate endpoint on the running OpenCode.
238
- * Bundled 1.18.4 does not expose POST /generate; keep the probe so a later
239
- * OpenCode that does can be used without changing the public completions API.
240
- *
241
- * SPA / OpenCode HTML often answers 200 text/html `<!doctype html>` for
242
- * unknown paths. That is not generate — only JSON (Content-Type or body)
243
- * counts as available.
244
- */
245
- export async function detectSessionlessGenerate({ fetchImpl, baseUrl, headers }) {
246
- const clientShape = typeof arguments[0]?.client?.generate === 'function'
247
- || typeof arguments[0]?.client?.v2?.generate === 'function';
248
- if (clientShape) {
249
- return { available: true, mode: 'sdk' };
250
- }
251
- const root = String(baseUrl || '').replace(/\/$/, '');
252
- const candidates = [`${root}/generate`, `${root}/api/generate`];
253
- for (const url of candidates) {
254
- try {
255
- const response = await fetchImpl(url, {
256
- method: 'POST',
257
- headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...headers },
258
- body: JSON.stringify({ probe: true }),
259
- signal: AbortSignal.timeout(4_000),
260
- });
261
- if (response.status === 404 || response.status === 405) continue;
262
- const body = await response.text().catch(() => '');
263
- if (isJsonContentType(response) || looksLikeJsonObject(body)) {
264
- return { available: true, mode: 'http', url };
265
- }
266
- } catch {
267
- // Probe failure is not a generate capability.
268
- }
269
- }
270
- return { available: false, mode: 'throwaway-session' };
271
- }
272
-
273
- const TEXT_FILE_MIME = /^(text\/|application\/(json|javascript|xml|sql|yaml|x-yaml|toml))/i;
274
- const MAX_INLINE_FILE_CHARS = 100_000;
275
-
276
94
  const messageText = (message) => {
277
95
  if (typeof message?.content === 'string') return message.content;
278
96
  if (Array.isArray(message?.content)) {
@@ -294,21 +112,51 @@ const messageFileParts = (message) => {
294
112
  }));
295
113
  };
296
114
 
297
- const decodeDataUrlText = (url, mime) => {
298
- if (!TEXT_FILE_MIME.test(mime) || typeof url !== 'string' || !url.startsWith('data:')) return null;
115
+ /**
116
+ * Strict data-URL parse. Returns null when the URI is not a well-formed data URL.
117
+ * Decoded size is checked against MAX_ATTACHMENT_BYTES.
118
+ */
119
+ export function parseDataUrl(url) {
120
+ if (typeof url !== 'string' || !url.startsWith('data:')) return null;
299
121
  const comma = url.indexOf(',');
300
- if (comma < 0) return null;
122
+ if (comma < 5) return null;
301
123
  const meta = url.slice(5, comma);
302
124
  const payload = url.slice(comma + 1);
125
+ if (!meta || payload.length === 0) return null;
126
+ const parts = meta.split(';');
127
+ const mime = parts[0] && /^[\w.+-]+\/[\w.+-]+$/i.test(parts[0]) ? parts[0] : null;
128
+ if (!mime) return null;
129
+ const isBase64 = parts.some((part) => part.toLowerCase() === 'base64');
130
+ let bytes;
303
131
  try {
304
- const bytes = meta.includes('base64')
132
+ bytes = isBase64
305
133
  ? Buffer.from(payload, 'base64')
306
134
  : Buffer.from(decodeURIComponent(payload), 'utf8');
307
- const text = bytes.toString('utf8');
308
- return text.length > MAX_INLINE_FILE_CHARS ? `${text.slice(0, MAX_INLINE_FILE_CHARS)}\n…` : text;
309
135
  } catch {
310
136
  return null;
311
137
  }
138
+ // Reject corrupt base64 that decodes to empty while payload was non-empty.
139
+ if (isBase64 && bytes.length === 0 && payload.replace(/\s/g, '').length > 0) return null;
140
+ if (bytes.length > MAX_ATTACHMENT_BYTES) {
141
+ const error = new Error(`Attachment exceeds ${MAX_ATTACHMENT_BYTES} byte limit`);
142
+ error.code = 'validation_error';
143
+ throw error;
144
+ }
145
+ return { mime, bytes, isBase64, byteLength: bytes.length };
146
+ }
147
+
148
+ const decodeDataUrlText = (url, mime) => {
149
+ if (!TEXT_FILE_MIME.test(mime)) return null;
150
+ let parsed;
151
+ try {
152
+ parsed = parseDataUrl(url);
153
+ } catch (error) {
154
+ if (error?.code === 'validation_error') throw error;
155
+ return null;
156
+ }
157
+ if (!parsed) return null;
158
+ const text = parsed.bytes.toString('utf8');
159
+ return text.length > MAX_INLINE_FILE_CHARS ? `${text.slice(0, MAX_INLINE_FILE_CHARS)}\n…` : text;
312
160
  };
313
161
 
314
162
  const describeContactFilePart = (part) => {
@@ -320,6 +168,25 @@ const describeContactFilePart = (part) => {
320
168
  return `[file: ${name} (${mime})]`;
321
169
  };
322
170
 
171
+ /**
172
+ * Validate every file part URL before generate. Non-data URLs are rejected
173
+ * (gateway only accepts contact data-URL attachments).
174
+ */
175
+ export function assertValidAttachmentParts(files) {
176
+ for (const part of files) {
177
+ if (!part || typeof part.url !== 'string') {
178
+ failGenerate('Attachment is missing a data URL', 'validation_error');
179
+ }
180
+ if (!part.url.startsWith('data:')) {
181
+ failGenerate('Attachment URL must be a data URL', 'validation_error');
182
+ }
183
+ const parsed = parseDataUrl(part.url);
184
+ if (!parsed) {
185
+ failGenerate('Attachment data URL is malformed', 'validation_error');
186
+ }
187
+ }
188
+ }
189
+
323
190
  const flattenMessages = (messages) => {
324
191
  const system = [];
325
192
  const turns = [];
@@ -345,50 +212,36 @@ const flattenMessages = (messages) => {
345
212
  };
346
213
  };
347
214
 
215
+ /** Image bytes only when the catalog model is vision-capable. */
348
216
  const filesForPrompt = (files, forwardImageParts) => {
349
217
  if (forwardImageParts) return files;
350
218
  return files.filter((part) => !String(part?.mime || '').startsWith('image/'));
351
219
  };
352
220
 
353
- async function generateViaSessionless({ fetchImpl, url, headers, providerID, modelID, variant, messages, signal }) {
354
- const response = await fetchImpl(url, {
355
- method: 'POST',
356
- headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...headers },
357
- body: JSON.stringify({
358
- model: { providerID, modelID },
359
- ...(variant ? { variant } : {}),
360
- messages,
361
- }),
362
- signal,
363
- });
364
- if (!response.ok) {
365
- const body = await response.text().catch(() => '');
366
- failGenerate(`OpenCode generate failed (${response.status})${body ? `: ${body.slice(0, 200)}` : ''}`);
367
- }
368
- const payload = await response.json();
369
- const text = typeof payload?.text === 'string'
370
- ? payload.text
371
- : typeof payload?.message === 'string'
372
- ? payload.message
373
- : '';
374
- if (!text.trim()) failGenerate('OpenCode generate returned no text');
375
- return { text: text.trim(), source: 'generate' };
376
- }
221
+ const imageFilesForSession = (files, forwardImageParts) => {
222
+ if (!forwardImageParts) return [];
223
+ return files.filter((part) => String(part?.mime || '').startsWith('image/'));
224
+ };
225
+
226
+ const modelRef = (providerID, modelID, variant) => ({
227
+ id: modelID,
228
+ providerID,
229
+ ...(variant ? { variant } : {}),
230
+ });
377
231
 
378
232
  const eventPayload = (event) => event?.payload?.payload ?? event?.payload ?? event;
379
233
 
380
- const eventDeltaProperties = (payload) => {
234
+ const eventDeltaData = (payload) => {
381
235
  if (!payload || typeof payload !== 'object') return null;
382
- if (payload.properties && typeof payload.properties === 'object') return payload.properties;
383
236
  if (payload.data && typeof payload.data === 'object') return payload.data;
237
+ if (payload.properties && typeof payload.properties === 'object') return payload.properties;
384
238
  return null;
385
239
  };
386
240
 
387
241
  /**
388
- * Forward real OpenCode `message.part.delta` text tokens for one throwaway
389
- * session. Returns an unsubscribe fn, or null when deltas cannot be observed
390
- * (no callback, no hub, or hub without subscribeEvent). Never fabricates
391
- * typewriter chunks from a completed string.
242
+ * Forward real OpenCode `session.text.delta` tokens for one throwaway session.
243
+ * Consumes data.sessionID / assistantMessageID / ordinal / delta.
244
+ * Returns unsubscribe, or null when deltas cannot be observed.
392
245
  *
393
246
  * @param {{ globalEventHub?: { subscribeEvent?: Function } | null, sessionID: string, onTextDelta?: ((text: string) => void) | null }} args
394
247
  * @returns {(() => void) | null}
@@ -398,22 +251,19 @@ export function subscribeThrowawayTextDeltas({ globalEventHub, sessionID, onText
398
251
  if (typeof sessionID !== 'string' || !sessionID) return null;
399
252
  if (typeof globalEventHub?.subscribeEvent !== 'function') return null;
400
253
 
401
- // Lock to the first assistant messageID seen for this throwaway session so
402
- // concurrent sessions on the shared hub cannot leak tokens into this generate.
403
254
  let assistantMessageID = null;
255
+ let lastOrdinal = -1;
404
256
 
405
257
  const unsubscribe = globalEventHub.subscribeEvent((event) => {
406
258
  const payload = eventPayload(event);
407
- if (payload?.type !== 'message.part.delta') return;
408
- const props = eventDeltaProperties(payload);
409
- if (!props) return;
410
- if (props.sessionID !== sessionID) return;
411
- // Text field only — reasoning/other fields stay out of completion tokens.
412
- if (typeof props.field === 'string' && props.field !== 'text') return;
413
- if (typeof props.delta !== 'string' || props.delta.length === 0) return;
414
-
415
- const messageID = typeof props.messageID === 'string' && props.messageID
416
- ? props.messageID
259
+ if (payload?.type !== 'session.text.delta') return;
260
+ const data = eventDeltaData(payload);
261
+ if (!data) return;
262
+ if (data.sessionID !== sessionID) return;
263
+ if (typeof data.delta !== 'string' || data.delta.length === 0) return;
264
+
265
+ const messageID = typeof data.assistantMessageID === 'string' && data.assistantMessageID
266
+ ? data.assistantMessageID
417
267
  : null;
418
268
  if (assistantMessageID) {
419
269
  if (messageID && messageID !== assistantMessageID) return;
@@ -421,8 +271,13 @@ export function subscribeThrowawayTextDeltas({ globalEventHub, sessionID, onText
421
271
  assistantMessageID = messageID;
422
272
  }
423
273
 
274
+ if (typeof data.ordinal === 'number' && Number.isFinite(data.ordinal)) {
275
+ if (data.ordinal <= lastOrdinal) return;
276
+ lastOrdinal = data.ordinal;
277
+ }
278
+
424
279
  try {
425
- onTextDelta(props.delta);
280
+ onTextDelta(data.delta);
426
281
  } catch {
427
282
  // Caller callback failures must not abort generate or leave the hub broken.
428
283
  }
@@ -432,19 +287,323 @@ export function subscribeThrowawayTextDeltas({ globalEventHub, sessionID, onText
432
287
  }
433
288
 
434
289
  /**
435
- * Generate assistant text through OpenCode's connected providers.
436
- * Sessionless generate if the binary exposes it; otherwise a throwaway
437
- * archived session used only as a tools-denied text generator.
290
+ * OpenCode loads a new location's config lazily: the first requests against a
291
+ * fresh temp directory can report no agents at all. Poll `agent.get` within a
292
+ * bounded window instead of failing on that first read.
293
+ */
294
+ const readGeneratorAgent = async ({ client, location, signal, agentID, waitMs, pollMs }) => {
295
+ const deadline = Date.now() + waitMs;
296
+ for (;;) {
297
+ try {
298
+ return await client.agent.get({
299
+ agentID,
300
+ ...(location ? { location } : {}),
301
+ }, { signal });
302
+ } catch (error) {
303
+ rethrowIfAborted(error, signal);
304
+ if (Date.now() + pollMs > deadline) throw error;
305
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
306
+ }
307
+ }
308
+ };
309
+
310
+ /**
311
+ * Verify the generator agent ends with deny-all permissions.
312
+ * Title alone is never treated as isolation. Failure must happen before prompt.
313
+ */
314
+ export async function assertLlmAgentDenyAll({
315
+ client,
316
+ location,
317
+ signal,
318
+ agentID = LLM_AGENT_NAME,
319
+ waitMs = AGENT_READY_WAIT_MS,
320
+ pollMs = AGENT_READY_POLL_MS,
321
+ }) {
322
+ let result;
323
+ try {
324
+ result = await readGeneratorAgent({ client, location, signal, agentID, waitMs, pollMs });
325
+ } catch (error) {
326
+ rethrowIfAborted(error, signal);
327
+ failGenerate(
328
+ `OpenCode LLM agent is unavailable (${clientErrorMessage(error, 'agent.get failed')}); generation blocked`,
329
+ 'llm_attachment_generation_unavailable',
330
+ );
331
+ }
332
+ const permissions = result?.data?.permissions;
333
+ if (!Array.isArray(permissions) || permissions.length === 0) {
334
+ failGenerate(
335
+ 'OpenCode LLM agent permissions are missing; generation blocked',
336
+ 'llm_attachment_generation_unavailable',
337
+ );
338
+ }
339
+ const last = permissions[permissions.length - 1];
340
+ if (
341
+ !last
342
+ || last.action !== '*'
343
+ || last.resource !== '*'
344
+ || last.effect !== 'deny'
345
+ ) {
346
+ failGenerate(
347
+ 'OpenCode LLM agent final permission is not deny-all; generation blocked',
348
+ 'llm_attachment_generation_unavailable',
349
+ );
350
+ }
351
+ return result.data;
352
+ }
353
+
354
+ const assistantTextFromMessages = (messages) => {
355
+ if (!Array.isArray(messages) || messages.length === 0) return '';
356
+ for (const message of messages) {
357
+ // message.list order:desc — first assistant is the newest.
358
+ const role = message?.type ?? message?.role ?? message?.info?.role;
359
+ if (role !== 'assistant') continue;
360
+ const error = message?.error ?? message?.info?.error;
361
+ if (error) {
362
+ const detail = assistantErrorDetail(error);
363
+ failGenerate(detail || 'OpenCode assistant error');
364
+ }
365
+ const finish = message?.finish ?? message?.info?.finish;
366
+ if (finish === 'error') {
367
+ failGenerate(assistantErrorDetail(message?.error) || 'OpenCode assistant finish=error');
368
+ }
369
+ const content = Array.isArray(message?.content)
370
+ ? message.content
371
+ : Array.isArray(message?.parts)
372
+ ? message.parts
373
+ : [];
374
+ const text = content
375
+ .map((part) => (part?.type === 'text' && typeof part.text === 'string' ? part.text : ''))
376
+ .filter(Boolean)
377
+ .join('');
378
+ return text;
379
+ }
380
+ return '';
381
+ };
382
+
383
+ const makeClient = ({ baseUrl, headers, clientFactory }) => {
384
+ if (typeof clientFactory === 'function') return clientFactory();
385
+ return OpenCode.make({ baseUrl, headers });
386
+ };
387
+
388
+ const prepareGeneratorDirectory = async ({ ensureTempDirectory, agentName, agentMarkdown }) => {
389
+ let workingDirectory;
390
+ try {
391
+ workingDirectory = await ensureTempDirectory({ agentName, agentMarkdown });
392
+ } catch (error) {
393
+ failGenerate(
394
+ `LLM temp directory failed (${clientErrorMessage(error, 'ensure failed')})`,
395
+ 'llm_attachment_generation_unavailable',
396
+ );
397
+ }
398
+ if (typeof workingDirectory !== 'string' || !workingDirectory.trim()) {
399
+ failGenerate(
400
+ 'LLM temp directory is empty; generation blocked',
401
+ 'llm_attachment_generation_unavailable',
402
+ );
403
+ }
404
+ return workingDirectory.trim();
405
+ };
406
+
407
+ /**
408
+ * Run `work(sessionID)` inside a throwaway OpenCode session owned by a
409
+ * verified deny-all agent. The session is always removed afterwards;
410
+ * cancellation interrupts it first.
411
+ */
412
+ async function withThrowawaySession({
413
+ client,
414
+ workingDirectory,
415
+ agentName,
416
+ providerID,
417
+ modelID,
418
+ variant,
419
+ signal,
420
+ persistSessionMetadata,
421
+ onSystemSessionPersisted,
422
+ }, work) {
423
+ const location = { directory: workingDirectory };
424
+
425
+ // Permission isolation must be verified before any prompt. Never use title as isolation.
426
+ await assertLlmAgentDenyAll({ client, location, signal, agentID: agentName });
427
+
428
+ let sessionID = null;
429
+ try {
430
+ let created;
431
+ const isolationMetadata = buildLlmSessionMetadata();
432
+ try {
433
+ created = await client.session.create({
434
+ title: '[openchamber-llm] generate',
435
+ agent: agentName,
436
+ model: modelRef(providerID, modelID, variant),
437
+ location,
438
+ metadata: isolationMetadata,
439
+ }, { signal });
440
+ } catch (error) {
441
+ // Temp workspace invisible to OpenCode must fail explicitly.
442
+ failGenerate(
443
+ `OpenCode LLM session create failed (${clientErrorMessage(error, 'create failed')})`,
444
+ 'llm_attachment_generation_unavailable',
445
+ );
446
+ }
447
+ sessionID = created?.id ?? created?.data?.id;
448
+ if (!sessionID) {
449
+ failGenerate('OpenCode LLM session create returned no id', 'llm_attachment_generation_unavailable');
450
+ }
451
+ if (typeof persistSessionMetadata === 'function') {
452
+ try {
453
+ await persistSessionMetadata(sessionID, isolationMetadata);
454
+ onSystemSessionPersisted?.({
455
+ sessionID,
456
+ directory: workingDirectory,
457
+ metadata: isolationMetadata,
458
+ });
459
+ } catch (error) {
460
+ console.warn('[llm] failed to persist system session metadata:', error?.message || error);
461
+ }
462
+ }
463
+ return await work(sessionID);
464
+ } finally {
465
+ if (sessionID) {
466
+ if (signal?.aborted) {
467
+ try {
468
+ await client.session.interrupt({ sessionID, continue: false });
469
+ } catch {
470
+ // Best-effort interrupt on timeout/abort.
471
+ }
472
+ }
473
+ try {
474
+ await client.session.remove({ sessionID });
475
+ } catch (error) {
476
+ console.warn('[llm] failed to remove throwaway OpenCode session:', error?.message || error);
477
+ }
478
+ }
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Text-only generation through `session.generate`: one model call with the
484
+ * session's provider context (so session-scoped providers such as OpenCode Go
485
+ * work), no tool loop, and no messages written to the session.
486
+ */
487
+ async function generateViaSessionGenerate({ client, prompt, signal, ...sessionOptions }) {
488
+ return withThrowawaySession({ client, signal, ...sessionOptions }, async (sessionID) => {
489
+ let result;
490
+ try {
491
+ result = await client.session.generate({ sessionID, prompt }, { signal });
492
+ } catch (error) {
493
+ rethrowIfAborted(error, signal);
494
+ failGenerate(clientErrorMessage(error, 'OpenCode session.generate failed'));
495
+ }
496
+ const text = typeof result?.text === 'string'
497
+ ? result.text.trim()
498
+ : typeof result?.data?.text === 'string'
499
+ ? result.data.text.trim()
500
+ : '';
501
+ if (!text) failGenerate('OpenCode session.generate returned no text');
502
+ return { text, source: 'session.generate' };
503
+ });
504
+ }
505
+
506
+ async function generateViaAttachmentSession({
507
+ client,
508
+ system,
509
+ prompt,
510
+ imageFiles,
511
+ signal,
512
+ onTextDelta,
513
+ globalEventHub,
514
+ ...sessionOptions
515
+ }) {
516
+ return withThrowawaySession({ client, signal, ...sessionOptions }, async (sessionID) => {
517
+ if (system) {
518
+ try {
519
+ await client.session.instructions.entry.put({
520
+ sessionID,
521
+ key: 'system',
522
+ value: system,
523
+ }, { signal });
524
+ } catch (error) {
525
+ failGenerate(clientErrorMessage(error, 'instructions.entry.put failed'));
526
+ }
527
+ }
528
+
529
+ const unsubscribeDeltas = subscribeThrowawayTextDeltas({
530
+ globalEventHub,
531
+ sessionID,
532
+ onTextDelta,
533
+ });
534
+ try {
535
+ return await promptAttachmentSession({ client, sessionID, prompt, imageFiles, signal });
536
+ } finally {
537
+ try {
538
+ unsubscribeDeltas?.();
539
+ } catch {
540
+ // Unsubscribe must not mask generate errors or block session remove.
541
+ }
542
+ }
543
+ });
544
+ }
545
+
546
+ async function promptAttachmentSession({ client, sessionID, prompt, imageFiles, signal }) {
547
+ const files = imageFiles.map((part) => ({
548
+ uri: part.url,
549
+ ...(part.filename ? { name: part.filename } : {}),
550
+ }));
551
+
552
+ try {
553
+ await client.session.prompt({
554
+ sessionID,
555
+ text: prompt || '[attachment]',
556
+ ...(files.length > 0 ? { files } : {}),
557
+ delivery: 'steer',
558
+ }, { signal });
559
+ } catch (error) {
560
+ rethrowIfAborted(error, signal);
561
+ failGenerate(clientErrorMessage(error, 'session.prompt failed'));
562
+ }
563
+
564
+ try {
565
+ await client.session.wait({ sessionID }, { signal });
566
+ } catch (error) {
567
+ rethrowIfAborted(error, signal);
568
+ failGenerate(clientErrorMessage(error, 'session.wait failed'));
569
+ }
570
+
571
+ let listed;
572
+ try {
573
+ listed = await client.message.list({
574
+ sessionID,
575
+ limit: 20,
576
+ order: 'desc',
577
+ }, { signal });
578
+ } catch (error) {
579
+ rethrowIfAborted(error, signal);
580
+ failGenerate(clientErrorMessage(error, 'message.list failed'));
581
+ }
582
+
583
+ const messages = Array.isArray(listed?.data) ? listed.data : [];
584
+ const text = assistantTextFromMessages(messages);
585
+ if (!text.trim()) {
586
+ failGenerate('OpenCode LLM generator returned no assistant text');
587
+ }
588
+ return { text: text.trim(), source: 'attachment-session' };
589
+ }
590
+
591
+ /**
592
+ * Generate assistant text through OpenCode's connected providers via
593
+ * `@opencode/client`.
594
+ *
595
+ * Both paths run in a throwaway session owned by a verified deny-all agent in
596
+ * the LLM temp directory, and the session is removed afterwards.
597
+ * - Pure text: `session.generate` (single model call, nothing written).
598
+ * - Vision image attachments: `session.prompt` + wait + message list.
599
+ * - Non-vision: keep `[image: …]` descriptions; never forward image bytes.
438
600
  *
439
- * V2 session.prompt only forwards { id, prompt, delivery, resume } and drops
440
- * model/parts/tools. Use promptAsync (body still has model, parts, tools,
441
- * system, agent) then wait for idle and read session.messages.
601
+ * Optional internal streaming: `onTextDelta` + `globalEventHub` forwards real
602
+ * `session.text.delta` tokens on the attachment-session path only.
603
+ * `session.generate` cannot emit live deltas (no fake typewriter).
442
604
  *
443
- * Optional internal streaming: pass `onTextDelta(text)` plus a live
444
- * `globalEventHub` (same hub as UI SSE). On the throwaway path only, real
445
- * `message.part.delta` tokens for this session are forwarded. Sessionless
446
- * `/generate` JSON cannot emit deltas — onTextDelta is skipped (no fake
447
- * typewriter). Public HTTP completions stay non-streaming.
605
+ * Optional `signal` from the contact continuation is combined with the generator
606
+ * deadline. Cancellation still runs throwaway-session cleanup.
448
607
  */
449
608
  export async function generateOpenCodeText({
450
609
  buildOpenCodeUrl,
@@ -453,26 +612,29 @@ export async function generateOpenCodeText({
453
612
  modelID,
454
613
  messages,
455
614
  variant,
456
- fetchImpl = globalThis.fetch.bind(globalThis),
457
615
  clientFactory,
458
- ensureTempDirectory,
459
- detect = detectSessionlessGenerate,
616
+ ensureTempDirectory = ensureLlmTempDirectory,
460
617
  forwardImageParts = false,
461
618
  onTextDelta = null,
462
619
  globalEventHub = null,
620
+ persistSessionMetadata = null,
621
+ onSystemSessionPersisted = null,
463
622
  signal: parentSignal = null,
464
623
  timeoutMs = GENERATE_TIMEOUT_MS,
465
- settlePollMs = SETTLE_POLL_MS,
466
624
  }) {
467
- parentSignal?.throwIfAborted();
625
+ parentSignal?.throwIfAborted?.();
468
626
  if (!providerID || !modelID) {
469
627
  const error = new Error('providerID and modelID are required');
470
628
  error.code = 'validation_error';
471
629
  throw error;
472
630
  }
473
631
  const flattened = flattenMessages(messages);
474
- const promptFiles = filesForPrompt(flattened.files, forwardImageParts);
475
- if (!flattened.prompt && promptFiles.length === 0) {
632
+ const allFiles = flattened.files;
633
+ assertValidAttachmentParts(allFiles);
634
+
635
+ const imageFiles = imageFilesForSession(allFiles, forwardImageParts);
636
+ // Non-image files stay as prompt descriptions (and inlined text when possible).
637
+ if (!flattened.prompt && imageFiles.length === 0 && allFiles.length === 0) {
476
638
  const error = new Error('messages must include a user turn');
477
639
  error.code = 'validation_error';
478
640
  throw error;
@@ -480,180 +642,81 @@ export async function generateOpenCodeText({
480
642
 
481
643
  const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
482
644
  const headers = getOpenCodeAuthHeaders() || {};
483
- const probe = await detect({ fetchImpl, baseUrl, headers, client: clientFactory?.() });
484
- parentSignal?.throwIfAborted();
645
+ const client = makeClient({ baseUrl, headers, clientFactory });
646
+
485
647
  const controller = new AbortController();
486
- const requestSignal = parentSignal ? AbortSignal.any([parentSignal, controller.signal]) : controller.signal;
487
- const stallMs = positiveMs(timeoutMs, GENERATE_TIMEOUT_MS);
488
- const abortGenerate = (message) => {
489
- if (controller.signal.aborted) return;
490
- controller.abort(new Error(message));
491
- };
492
- // Idle deadline only: live busy/delta/incomplete assistant progress
493
- // must not look like "the model hung". There is no wall-clock cap.
494
- let stallTimer = setTimeout(() => {
495
- abortGenerate(`OpenCode LLM generate timed out after ${stallMs}ms without progress`);
496
- }, stallMs);
497
- const bumpTimeout = () => {
498
- if (controller.signal.aborted) return;
499
- clearTimeout(stallTimer);
500
- stallTimer = setTimeout(() => {
501
- abortGenerate(`OpenCode LLM generate timed out after ${stallMs}ms without progress`);
502
- }, stallMs);
648
+ const deadlineMs = positiveMs(timeoutMs, GENERATE_TIMEOUT_MS);
649
+ const requestSignal = parentSignal
650
+ ? AbortSignal.any([parentSignal, controller.signal])
651
+ : controller.signal;
652
+ const timeout = setTimeout(() => {
653
+ controller.abort(new Error(`OpenCode LLM generate timed out after ${deadlineMs}ms`));
654
+ }, deadlineMs);
655
+ timeout.unref?.();
656
+
657
+ const sessionOptions = {
658
+ client,
659
+ providerID,
660
+ modelID,
661
+ variant,
662
+ signal: requestSignal,
663
+ persistSessionMetadata,
664
+ onSystemSessionPersisted,
503
665
  };
504
666
 
505
667
  try {
506
- if (probe.available && probe.mode === 'http' && probe.url) {
507
- return await generateViaSessionless({
508
- fetchImpl,
509
- url: probe.url,
510
- headers,
511
- providerID,
512
- modelID,
513
- variant,
514
- messages: forwardImageParts
515
- ? messages
516
- : messages.map((message) => {
517
- if (!Array.isArray(message?.parts)) return message;
518
- const parts = message.parts.filter((part) => part?.type !== 'file' || !String(part.mime || '').startsWith('image/'));
519
- return parts.length === message.parts.length ? message : { ...message, parts };
520
- }),
521
- signal: requestSignal,
668
+ if (imageFiles.length > 0) {
669
+ const workingDirectory = await prepareGeneratorDirectory({
670
+ ensureTempDirectory,
671
+ agentName: LLM_AGENT_NAME,
672
+ agentMarkdown: AGENT_MARKDOWN,
522
673
  });
523
- }
524
-
525
- const workingDirectory = await ensureTempDirectory({
526
- agentName: LLM_AGENT_NAME,
527
- agentMarkdown: AGENT_MARKDOWN,
528
- });
529
- const client = clientFactory
530
- ? clientFactory()
531
- : createOpencodeClient({ baseUrl, directory: workingDirectory, headers });
532
-
533
- let toolIds;
534
- try {
535
- toolIds = await client.tool.ids({ directory: workingDirectory });
536
- } catch (error) {
537
- failGenerate(`OpenCode tool.ids failed: ${error?.message || 'tool.ids failed'}`);
538
- }
539
- if (toolIds?.error) {
540
- failGenerate(`OpenCode tool.ids failed: ${sdkErrorMessage(toolIds, 'tool.ids failed')}`);
541
- }
542
- const tools = deniedTools(Array.isArray(toolIds?.data) ? toolIds.data : []);
543
-
544
- const created = await client.session.create({
545
- directory: workingDirectory,
546
- title: '[openchamber-llm] generate',
547
- agent: LLM_AGENT_NAME,
548
- // Session fence: deny every native OpenCode/MCP/skill tool for this throwaway.
549
- permission: LLM_SESSION_DENY_PERMISSION,
550
- metadata: { openchamber: { llm: { purpose: 'chat-completions' } } },
551
- }, { signal: requestSignal });
552
- const sessionID = created?.data?.id;
553
- if (created?.error || !sessionID) {
554
- failGenerate(`OpenCode LLM session create failed: ${sdkErrorMessage(created, 'create failed')}`);
555
- }
556
-
557
- let unsubscribeDeltas = null;
558
- try {
559
- const archiveAt = Date.now();
560
- let archived = await client.session.update({
561
- sessionID,
562
- directory: workingDirectory,
563
- time: { archived: archiveAt },
564
- });
565
- if (isMissing(archived)) {
566
- archived = await client.session.update({
567
- sessionID,
568
- directory: workingDirectory,
569
- time: { archived: archiveAt },
570
- });
571
- }
572
- if (archived?.error) {
573
- failGenerate(`OpenCode LLM session archive failed: ${sdkErrorMessage(archived, 'archive failed')}`);
574
- }
575
-
576
- // Subscribe before promptAsync so early message.part.delta tokens are not missed.
577
- unsubscribeDeltas = subscribeThrowawayTextDeltas({
674
+ return await generateViaAttachmentSession({
675
+ ...sessionOptions,
676
+ workingDirectory,
677
+ agentName: LLM_AGENT_NAME,
678
+ system: flattened.system,
679
+ prompt: flattened.prompt,
680
+ imageFiles,
681
+ onTextDelta,
578
682
  globalEventHub,
579
- sessionID,
580
- onTextDelta: (text) => {
581
- bumpTimeout();
582
- if (typeof onTextDelta === 'function') onTextDelta(text);
583
- },
584
683
  });
585
-
586
- // promptAsync still forwards model/parts/tools. v2 session.prompt does not.
587
- const prompted = await client.session.promptAsync({
588
- sessionID,
589
- directory: workingDirectory,
590
- agent: LLM_AGENT_NAME,
591
- model: { providerID, modelID },
592
- ...(variant ? { variant } : {}),
593
- ...(flattened.system ? { system: flattened.system } : {}),
594
- tools,
595
- parts: [
596
- { type: 'text', text: flattened.prompt, synthetic: false },
597
- ...promptFiles,
598
- ],
599
- }, { signal: requestSignal });
600
- if (!promptAdmitted(prompted)) {
601
- failGenerate(`OpenCode LLM promptAsync failed: ${sdkErrorMessage(prompted, 'promptAsync failed')}`);
602
- }
603
- bumpTimeout();
604
-
605
- let text = assistantTextFromPrompt(prompted?.data ?? prompted);
606
- if (!text.trim()) {
607
- const settled = await waitForIdleAssistant({
608
- client,
609
- sessionID,
610
- directory: workingDirectory,
611
- signal: requestSignal,
612
- onProgress: bumpTimeout,
613
- settlePollMs,
614
- });
615
- text = assistantTextFromMessages(settled);
616
- }
617
- if (!text.trim()) {
618
- failGenerate('OpenCode LLM generator returned no assistant text');
619
- }
620
- return { text: text.trim(), source: 'throwaway-session' };
621
- } finally {
622
- try {
623
- unsubscribeDeltas?.();
624
- } catch {
625
- // Unsubscribe must not mask generate errors or block session delete.
626
- }
627
- unsubscribeDeltas = null;
628
- try {
629
- const deleted = await client.session.delete({ sessionID, directory: workingDirectory });
630
- if (deleted?.error) {
631
- console.warn(
632
- '[llm] failed to delete throwaway OpenCode session:',
633
- sdkErrorMessage(deleted, 'delete failed'),
634
- );
635
- }
636
- } catch (error) {
637
- console.warn('[llm] failed to delete throwaway OpenCode session:', error?.message || error);
638
- }
639
684
  }
685
+
686
+ // Text path: the system prompt is prepended into the single prompt string.
687
+ const workingDirectory = await prepareGeneratorDirectory({
688
+ ensureTempDirectory,
689
+ agentName: TEXT_AGENT_NAME,
690
+ agentMarkdown: TEXT_AGENT_MARKDOWN,
691
+ });
692
+ const prompt = [flattened.system, flattened.prompt].filter((part) => String(part || '').trim()).join('\n\n');
693
+ return await generateViaSessionGenerate({
694
+ ...sessionOptions,
695
+ workingDirectory,
696
+ agentName: TEXT_AGENT_NAME,
697
+ prompt,
698
+ });
640
699
  } finally {
641
- clearTimeout(stallTimer);
700
+ clearTimeout(timeout);
642
701
  }
643
702
  }
644
703
 
645
704
  export const _test = {
646
705
  flattenMessages,
647
706
  filesForPrompt,
707
+ imageFilesForSession,
648
708
  describeContactFilePart,
649
- deniedTools,
650
- assistantTextFromPrompt,
651
709
  assistantTextFromMessages,
652
710
  eventPayload,
653
- eventDeltaProperties,
711
+ eventDeltaData,
654
712
  subscribeThrowawayTextDeltas,
713
+ assertLlmAgentDenyAll,
714
+ parseDataUrl,
715
+ assertValidAttachmentParts,
655
716
  LLM_AGENT_NAME,
656
717
  AGENT_MARKDOWN,
657
- LLM_SESSION_DENY_PERMISSION,
718
+ TEXT_AGENT_NAME,
719
+ TEXT_AGENT_MARKDOWN,
720
+ MAX_ATTACHMENT_BYTES,
658
721
  GENERATE_TIMEOUT_MS,
659
722
  };