@openchambery/web 2.0.0-beta.1 → 2.0.0-beta.4

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 (287) hide show
  1. package/README.md +15 -11
  2. package/bin/cli-opencode.test.js +4 -4
  3. package/bin/cli.js +1 -1
  4. package/bin/lib/cli-startup.js +1 -1
  5. package/bin/lib/cli-startup.test.js +18 -4
  6. package/dist/assets/{AgentAvatar-CLWWNu_Q.js → AgentAvatar-Q1SmFRx2.js} +1 -1
  7. package/dist/assets/AssistantView-CwXkiCAd.js +3 -0
  8. package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-BW92schO.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-BZmwl-zE.js} +1 -1
  10. package/dist/assets/{Icon-DD-BuMfZ.js → Icon-AQPvxMiU.js} +1 -1
  11. package/dist/assets/{MarkdownRenderer-bc_p1H7l.js → MarkdownRenderer-Bv05iDZZ.js} +2 -2
  12. package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-C81xqYib.js} +3 -3
  13. package/dist/assets/MarkstreamRendererImpl-CXUCQuKJ.js +3 -0
  14. package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-BMgIu7zc.js} +1 -1
  15. package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-BOXGNHmC.js} +2 -2
  16. package/dist/assets/MobileSurface-xuc_z0_G.js +2 -0
  17. package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-B5At2Z95.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-bsPLgz4r.js} +1 -1
  19. package/dist/assets/OverlayScrollbar-4wp4migh.js +2 -0
  20. package/dist/assets/SettingsView-CJ6tcH5_.js +25 -0
  21. package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-CMtX2bMl.js} +1 -1
  22. package/dist/assets/{TerminalView-Dh9MWCpS.js → TerminalView-Dvr-ysTS.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-uH1ZZDSS.js} +2 -2
  24. package/dist/assets/appThemeRegistry-DK7q2juD.js +1 -0
  25. package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
  26. package/dist/assets/{button-D-rATN7w.js → button-CcNJUxUd.js} +1 -1
  27. package/dist/assets/collapsible-ZmZ6rLWs.js +3 -0
  28. package/dist/assets/context-menu-B2LXJgXL.js +1 -0
  29. package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-C6HrP0jq.js} +1 -1
  30. package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DdVBp45U.js} +1 -1
  31. package/dist/assets/{diagram-DQf7Y_uR.js → diagram-CxTs9hCQ.js} +1 -1
  32. package/dist/assets/{dialog-bfbvR4VH.js → dialog-KMVL9QKF.js} +1 -1
  33. package/dist/assets/es-CN3E_zXj.js +15 -0
  34. package/dist/assets/{fileMentionTouchSelection-BvrNCroG.js → fileMentionTouchSelection-7RIwlpkB.js} +1 -1
  35. package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-CjdBRy8y.js} +1 -1
  36. package/dist/assets/fr-9zZvQZD2.js +13 -0
  37. package/dist/assets/gitApi-yuWg9BN4.js +10 -0
  38. package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-9vQBq3zp.js} +1 -1
  39. package/dist/assets/{input-8r6F_Fza.js → input-CrIvk3HZ.js} +1 -1
  40. package/dist/assets/insertionBoundaries-BiUCSkx-.js +1 -0
  41. package/dist/assets/ja-Dnjk_82N.js +15 -0
  42. package/dist/assets/ko-BxCEE99C.js +15 -0
  43. package/dist/assets/{main-CCE_4y5F.js → main-C9mIBo77.js} +2 -2
  44. package/dist/assets/main-DiL6w6KF.js +169 -0
  45. package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
  46. package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-BmXVY_gc.js} +1 -1
  47. package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
  48. package/dist/assets/mcpQueries-C7jknHV8.js +1 -0
  49. package/dist/assets/miniChat-CwtUaElU.js +2 -0
  50. package/dist/assets/mobile-DB-6--ws.js +2 -0
  51. package/dist/assets/multirun-TAsxFI8e.js +4 -0
  52. package/dist/assets/{persistence-Ce3AnpEr.js → persistence-56kq8Z4V.js} +2 -2
  53. package/dist/assets/pl-CNNsWnPM.js +15 -0
  54. package/dist/assets/platform-C13fMVEs.js +6 -0
  55. package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
  56. package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
  57. package/dist/assets/renderElectronMiniChatApp-CGj0MNRU.js +2 -0
  58. package/dist/assets/renderMobileApp-CGMMqbsO.js +28 -0
  59. package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
  60. package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-BEANkvOd.js} +1 -1
  61. package/dist/assets/runtimeEndpointReset-A_ZBUJFS.js +147 -0
  62. package/dist/assets/sessionChangeSummary-qOLrP9fw.js +2 -0
  63. package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-34XFmP9C.js} +1 -1
  64. package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-DyEV0Heg.js} +1 -1
  65. package/dist/assets/src-ubCWXZhR.css +2 -0
  66. package/dist/assets/store-DLCiZaA6.js +15 -0
  67. package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-CXaBEoa_.js} +1 -1
  68. package/dist/assets/textarea-BBOlRI9O.js +1 -0
  69. package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-CPI4fr1a.js} +1 -1
  70. package/dist/assets/uk-rwQrxeYM.js +15 -0
  71. package/dist/assets/{url-_z7evzdK.js → url-DRDQXjyX.js} +1 -1
  72. package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-CiDe42I5.js} +57 -43
  73. package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
  74. package/dist/assets/useDesktopWindowControlsLayout-BlizAlOt.js +1 -0
  75. package/dist/assets/useEffectiveDirectory-C_CouL5v.js +1 -0
  76. package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-B0oUZAiB.js} +1 -1
  77. package/dist/assets/useMobileNavigationStore-DfPClaW-.js +414 -0
  78. package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
  79. package/dist/assets/useSessionAutoCleanup-DtrfbxAp.js +2 -0
  80. package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
  81. package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
  82. package/dist/assets/{useWorkerHighlightedLines-Bqn4AiBL.js → useWorkerHighlightedLines-CPNYz6Wf.js} +2 -2
  83. package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
  84. package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
  85. package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
  86. package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
  87. package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
  88. package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
  89. package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
  90. package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
  91. package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
  92. package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
  93. package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
  94. package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
  95. package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
  96. package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
  97. package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
  98. package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
  99. package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
  100. package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
  101. package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
  102. package/dist/index.html +13 -13
  103. package/dist/mini-chat.html +13 -13
  104. package/dist/mobile.html +13 -13
  105. package/package.json +1 -1
  106. package/server/index.js +327 -62
  107. package/server/lib/assistants/DOCUMENTATION.md +14 -9
  108. package/server/lib/assistants/contracts.js +11 -1
  109. package/server/lib/assistants/projection-history.test.js +265 -18
  110. package/server/lib/assistants/routes.js +6 -0
  111. package/server/lib/assistants/service.js +417 -231
  112. package/server/lib/assistants/service.test.js +392 -148
  113. package/server/lib/conversations/DOCUMENTATION.md +1 -1
  114. package/server/lib/conversations/service.js +1 -0
  115. package/server/lib/conversations/service.test.js +16 -0
  116. package/server/lib/conversations/validation.js +11 -1
  117. package/server/lib/event-stream/DOCUMENTATION.md +3 -1
  118. package/server/lib/event-stream/directory-ws-bridge.js +13 -2
  119. package/server/lib/event-stream/global-hub.js +49 -4
  120. package/server/lib/event-stream/reasoning-projection.js +16 -1
  121. package/server/lib/event-stream/reasoning-projection.test.js +15 -0
  122. package/server/lib/event-stream/runtime.js +57 -20
  123. package/server/lib/event-stream/runtime.test.js +112 -0
  124. package/server/lib/llm/DOCUMENTATION.md +33 -9
  125. package/server/lib/llm/catalog.js +1 -1
  126. package/server/lib/llm/completions.js +5 -1
  127. package/server/lib/llm/completions.test.js +3 -3
  128. package/server/lib/llm/generate.js +237 -145
  129. package/server/lib/llm/generate.test.js +201 -46
  130. package/server/lib/llm/routes.js +2 -0
  131. package/server/lib/message-queue/runtime.js +15 -2
  132. package/server/lib/notifications/runtime.js +23 -5
  133. package/server/lib/notifications/runtime.test.js +11 -0
  134. package/server/lib/opencode/DOCUMENTATION.md +85 -17
  135. package/server/lib/opencode/agent-document.d.ts +28 -0
  136. package/server/lib/opencode/agent-document.js +280 -0
  137. package/server/lib/opencode/agent-document.test.js +137 -0
  138. package/server/lib/opencode/agents.js +96 -0
  139. package/server/lib/opencode/auth-state-runtime.js +5 -0
  140. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  141. package/server/lib/opencode/config-entity-routes.js +42 -99
  142. package/server/lib/opencode/config-entity-routes.test.js +107 -0
  143. package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
  144. package/server/lib/opencode/ensure-cli.js +1 -2
  145. package/server/lib/opencode/env-runtime.js +40 -26
  146. package/server/lib/opencode/env-runtime.test.js +10 -17
  147. package/server/lib/opencode/feature-routes-runtime.js +43 -4
  148. package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
  149. package/server/lib/opencode/http-server-connections.js +98 -0
  150. package/server/lib/opencode/http-server-connections.test.js +321 -0
  151. package/server/lib/opencode/lifecycle.js +349 -39
  152. package/server/lib/opencode/lifecycle.test.js +322 -0
  153. package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
  154. package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
  155. package/server/lib/opencode/openchamber-routes.js +7 -1
  156. package/server/lib/opencode/opencode2-pin.js +4 -4
  157. package/server/lib/opencode/opencode2-pin.test.js +1 -0
  158. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +127 -4
  159. package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
  160. package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
  161. package/server/lib/opencode/plugin-routes.js +3 -18
  162. package/server/lib/opencode/plugin-routes.test.js +9 -9
  163. package/server/lib/opencode/provider-catalog.js +58 -12
  164. package/server/lib/opencode/provider-catalog.test.js +48 -0
  165. package/server/lib/opencode/proxy.js +252 -46
  166. package/server/lib/opencode/proxy.test.js +235 -2
  167. package/server/lib/opencode/routes.js +296 -62
  168. package/server/lib/opencode/routes.test.js +278 -12
  169. package/server/lib/opencode/runtime-contract.js +401 -0
  170. package/server/lib/opencode/runtime-contract.test.js +214 -0
  171. package/server/lib/opencode/server-opencode-fetch.js +242 -0
  172. package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
  173. package/server/lib/opencode/server-utils-runtime.js +33 -0
  174. package/server/lib/opencode/shared-service.integration.test.js +66 -0
  175. package/server/lib/opencode/shared-service.js +147 -0
  176. package/server/lib/opencode/shared-service.test.js +93 -0
  177. package/server/lib/opencode/shared.js +3 -3
  178. package/server/lib/opencode/shutdown-runtime.js +15 -6
  179. package/server/lib/opencode/shutdown-runtime.test.js +49 -1
  180. package/server/lib/opencode/skill-routes.js +20 -33
  181. package/server/lib/opencode/skill-routes.test.js +35 -2
  182. package/server/lib/opencode/skills.test.js +15 -0
  183. package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
  184. package/server/lib/opencode/static-routes-runtime.js +3 -1
  185. package/server/lib/opencode/static-routes-runtime.test.js +33 -0
  186. package/server/lib/opencode/v2-client.js +14 -3
  187. package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
  188. package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
  189. package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
  190. package/server/lib/permission-auto-accept/runtime.js +14 -7
  191. package/server/lib/permission-auto-accept/runtime.test.js +27 -12
  192. package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
  193. package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -0
  194. package/server/lib/scheduled-tasks/runtime.js +52 -4
  195. package/server/lib/scheduled-tasks/runtime.test.js +80 -1
  196. package/server/lib/session-goal/DOCUMENTATION.md +113 -13
  197. package/server/lib/session-goal/routes.js +9 -0
  198. package/server/lib/session-goal/runtime.js +1579 -117
  199. package/server/lib/session-goal/runtime.test.js +1453 -84
  200. package/server/lib/session-index/DOCUMENTATION.md +18 -15
  201. package/server/lib/session-index/event-ingest.js +89 -16
  202. package/server/lib/session-index/event-ingest.test.js +73 -0
  203. package/server/lib/session-index/host-authority-snapshot.js +92 -0
  204. package/server/lib/session-index/routes.host-archive.test.js +201 -0
  205. package/server/lib/session-index/routes.js +63 -10
  206. package/server/lib/session-index/routes.test.js +8 -8
  207. package/server/lib/session-index/service.js +7 -1
  208. package/server/lib/session-index/sync-runtime.js +161 -34
  209. package/server/lib/session-index/sync-runtime.test.js +155 -1
  210. package/server/lib/session-metadata/DOCUMENTATION.md +61 -16
  211. package/server/lib/session-metadata/routes.js +108 -9
  212. package/server/lib/session-metadata/routes.test.js +47 -2
  213. package/server/lib/session-metadata/session-archive.d.ts +73 -0
  214. package/server/lib/session-metadata/session-archive.js +654 -0
  215. package/server/lib/session-metadata/session-archive.test.js +359 -0
  216. package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
  217. package/server/lib/session-metadata/session-metadata-store.js +212 -72
  218. package/server/lib/session-metadata/session-metadata-store.test.js +205 -10
  219. package/server/lib/session-metadata/session-projection.d.ts +41 -0
  220. package/server/lib/session-metadata/session-projection.js +255 -0
  221. package/server/lib/session-metadata/session-projection.test.js +64 -0
  222. package/server/lib/session-metadata/system-session.js +55 -0
  223. package/server/lib/session-metadata/system-session.test.js +55 -0
  224. package/server/lib/session-title/DOCUMENTATION.md +40 -7
  225. package/server/lib/session-title/native-runtime.test.js +167 -0
  226. package/server/lib/session-title/runtime.js +67 -42
  227. package/server/lib/session-title/session-access.js +46 -0
  228. package/server/lib/session-title/session-access.test.js +91 -0
  229. package/server/lib/session-turn-pages/DOCUMENTATION.md +9 -8
  230. package/server/lib/session-turn-pages/routes.js +17 -102
  231. package/server/lib/session-turn-pages/routes.test.js +281 -0
  232. package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
  233. package/server/lib/session-turn-pages/session-message-projection.js +281 -0
  234. package/server/lib/small-model/DOCUMENTATION.md +101 -147
  235. package/server/lib/small-model/catalog.js +31 -51
  236. package/server/lib/small-model/catalog.test.js +65 -10
  237. package/server/lib/small-model/custom-api.js +68 -0
  238. package/server/lib/small-model/custom-api.test.js +64 -0
  239. package/server/lib/small-model/index.js +124 -240
  240. package/server/lib/small-model/resolve.js +35 -120
  241. package/server/lib/small-model/resolve.test.js +20 -110
  242. package/server/lib/small-model/routes.js +1 -3
  243. package/server/lib/small-model/summary-settings.test.js +157 -253
  244. package/server/opencode-proxy.test.js +2 -0
  245. package/dist/assets/AssistantView-BTCkm8yk.js +0 -3
  246. package/dist/assets/MarkstreamRendererImpl-CnkodGJW.js +0 -3
  247. package/dist/assets/MobileSurface-BXLeiZ4d.js +0 -2
  248. package/dist/assets/OverlayScrollbar-pMwxaCca.js +0 -2
  249. package/dist/assets/SettingsView-B5I0KDTZ.js +0 -25
  250. package/dist/assets/appThemeRegistry-COGfxTFV.js +0 -1
  251. package/dist/assets/collapsible-BXRd5_Hr.js +0 -1
  252. package/dist/assets/context-menu-jGtH6fR9.js +0 -1
  253. package/dist/assets/es-c52LTHHe.js +0 -15
  254. package/dist/assets/fr-PisFbUE8.js +0 -13
  255. package/dist/assets/gitApi-ButwzQ0N.js +0 -9
  256. package/dist/assets/insertionBoundaries-DGs7S5zv.js +0 -1
  257. package/dist/assets/ja-C8Ikun3Z.js +0 -15
  258. package/dist/assets/ko-ClFys5o2.js +0 -15
  259. package/dist/assets/main-Dn9rpsgl.js +0 -169
  260. package/dist/assets/markdown-worker-CptoICmq.js +0 -2
  261. package/dist/assets/miniChat-BpjxVqgg.js +0 -2
  262. package/dist/assets/mobile-nMJCaNq1.js +0 -2
  263. package/dist/assets/multirun-BvSG94mW.js +0 -4
  264. package/dist/assets/pl-CnaKWN5F.js +0 -15
  265. package/dist/assets/platform-CWTIp02q.js +0 -4
  266. package/dist/assets/projectMeta-DOe49dM_.js +0 -3
  267. package/dist/assets/pt-BR-s8OpfRI6.js +0 -15
  268. package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +0 -2
  269. package/dist/assets/renderMobileApp-y-S8y8v_.js +0 -28
  270. package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
  271. package/dist/assets/runtimeEndpointReset-B6BxhvR0.js +0 -147
  272. package/dist/assets/sessionChangeSummary-VVBlEXuh.js +0 -2
  273. package/dist/assets/src-BzX5UI-c.css +0 -2
  274. package/dist/assets/store-DJczmczv.js +0 -15
  275. package/dist/assets/textarea-3XR8sUfy.js +0 -1
  276. package/dist/assets/uk-CaX0iqkB.js +0 -15
  277. package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +0 -1
  278. package/dist/assets/useEffectiveDirectory-CN6JoCFw.js +0 -1
  279. package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +0 -414
  280. package/dist/assets/useSessionAutoCleanup-BLjymaFt.js +0 -2
  281. package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
  282. package/dist/assets/zh-CN-20MYEjha.js +0 -15
  283. package/dist/assets/zh-TW-DiN04qTw.js +0 -15
  284. package/server/lib/small-model/call.js +0 -427
  285. package/server/lib/small-model/call.test.js +0 -382
  286. package/server/lib/small-model/opencode-session.js +0 -183
  287. package/server/lib/small-model/opencode-session.test.js +0 -147
@@ -56,20 +56,37 @@ const projectModalities = (source) => {
56
56
  return ownKeys(projected).length > 0 ? projected : undefined;
57
57
  };
58
58
 
59
+ // v2 ModelCapabilities.input/output are modality name arrays, not boolean maps.
60
+ const projectModalityList = (source) => {
61
+ if (!Array.isArray(source)) return undefined;
62
+ const projected = createDictionary();
63
+ for (const item of source) {
64
+ if (typeof item !== 'string' || !MODALITIES.has(item) || !isSafeIdentifier(item)) continue;
65
+ projected[item] = true;
66
+ }
67
+ return ownKeys(projected).length > 0 ? projected : undefined;
68
+ };
69
+
59
70
  const projectCapabilities = (source) => {
60
71
  if (!isRecord(source)) return undefined;
61
72
  const capabilities = createDictionary();
62
73
  const flags = projectBooleanFields(source, ['temperature', 'reasoning', 'attachment', 'toolcall']);
63
74
  if (flags) Object.assign(capabilities, flags);
75
+ // v2 names tool support `tools`. Keep an explicit catalog `toolcall` flag when both exist.
76
+ if (!Object.hasOwn(capabilities, 'toolcall') && typeof source.tools === 'boolean') {
77
+ capabilities.toolcall = source.tools;
78
+ }
64
79
  for (const field of ['input', 'output']) {
65
80
  if (!Object.hasOwn(source, field)) continue;
66
- const modalities = projectModalities(source[field]);
81
+ const modalities = Array.isArray(source[field])
82
+ ? projectModalityList(source[field])
83
+ : projectModalities(source[field]);
67
84
  if (modalities) capabilities[field] = modalities;
68
85
  }
69
86
  return ownKeys(capabilities).length > 0 ? capabilities : undefined;
70
87
  };
71
88
 
72
- const projectCost = (source) => {
89
+ const projectCostEntry = (source) => {
73
90
  if (!isRecord(source)) return undefined;
74
91
  const cost = createDictionary();
75
92
  for (const field of ['input', 'output']) {
@@ -87,6 +104,30 @@ const projectCost = (source) => {
87
104
  return ownKeys(cost).length > 0 ? cost : undefined;
88
105
  };
89
106
 
107
+ // v2 ModelInfo.cost is a tier array. The untiered entry is the base price.
108
+ const projectCost = (source) => {
109
+ if (Array.isArray(source)) {
110
+ const base = source.find((item) => isRecord(item) && !isRecord(item.tier))
111
+ || source.find((item) => isRecord(item));
112
+ return projectCostEntry(base);
113
+ }
114
+ return projectCostEntry(source);
115
+ };
116
+
117
+ // v2 ModelInfo.variants is `{ id, settings?, headers?, body? }[]`. Catalog consumers
118
+ // still read a name→{} record. Request payloads stay server-side.
119
+ const projectVariantNames = (source) => {
120
+ if (Array.isArray(source)) {
121
+ return source.map((item) => {
122
+ if (typeof item === 'string') return item;
123
+ if (isRecord(item) && typeof item.id === 'string') return item.id;
124
+ return undefined;
125
+ });
126
+ }
127
+ if (isRecord(source)) return ownKeys(source);
128
+ return null;
129
+ };
130
+
90
131
  function projectModel(source) {
91
132
  if (!isRecord(source) || !isSafeIdentifier(source.id) || !isSafeDisplayName(source.name)) return null;
92
133
  // partial is structural only (e.g. variant truncation). Soft allowlist stripping of optional
@@ -113,18 +154,23 @@ function projectModel(source) {
113
154
  if (Object.hasOwn(source, 'release_date') && source.release_date !== '' && source.release_date !== null) {
114
155
  if (isSafeReleaseDate(source.release_date)) model.release_date = source.release_date;
115
156
  }
116
- if (Object.hasOwn(source, 'variants') && isRecord(source.variants)) {
117
- const variants = createDictionary();
118
- let variantCount = 0;
119
- for (const variantName of ownKeys(source.variants)) {
120
- if (variantCount >= MAX_VARIANTS_PER_MODEL) {
121
- partial = true;
122
- break;
157
+ if (Object.hasOwn(source, 'variants')) {
158
+ const names = projectVariantNames(source.variants);
159
+ if (names) {
160
+ const variants = createDictionary();
161
+ let variantCount = 0;
162
+ for (const variantName of names) {
163
+ if (variantCount >= MAX_VARIANTS_PER_MODEL) {
164
+ partial = true;
165
+ break;
166
+ }
167
+ variantCount += 1;
168
+ if (typeof variantName === 'string' && isSafeIdentifier(variantName) && !Object.hasOwn(variants, variantName)) {
169
+ variants[variantName] = createDictionary();
170
+ }
123
171
  }
124
- variantCount += 1;
125
- if (isSafeIdentifier(variantName)) variants[variantName] = createDictionary();
172
+ if (ownKeys(variants).length > 0) model.variants = variants;
126
173
  }
127
- if (ownKeys(variants).length > 0) model.variants = variants;
128
174
  }
129
175
  return { value: model, partial };
130
176
  }
@@ -204,6 +204,54 @@ describe('projectProviderCatalog', () => {
204
204
  expect(result.value.partial).toBe(true);
205
205
  });
206
206
 
207
+ it('projects v2 variant arrays, capability lists, and tiered cost without leaking request payloads', () => {
208
+ const result = projectProviderCatalog({
209
+ providers: [{
210
+ id: 'openai',
211
+ name: 'OpenAI',
212
+ models: {
213
+ 'gpt-5': {
214
+ id: 'gpt-5',
215
+ name: 'GPT 5',
216
+ capabilities: {
217
+ tools: true,
218
+ input: ['text', 'image', 'secret-modality'],
219
+ output: ['text'],
220
+ },
221
+ cost: [
222
+ { tier: { type: 'context', size: 200000 }, input: 9, output: 9, cache: { read: 1, write: 1 } },
223
+ { input: 1, output: 2, cache: { read: 0.1, write: 0.2 }, secret: 'cost-sentinel' },
224
+ ],
225
+ variants: [
226
+ { id: 'low', settings: { reasoningEffort: 'low', apiKey: 'variant-settings-sentinel' }, headers: { Authorization: 'variant-headers-sentinel' } },
227
+ { id: 'high', body: { store: false } },
228
+ { id: 'low' },
229
+ { id: 'constructor' },
230
+ 'max',
231
+ { settings: { token: 'variant-missing-id-sentinel' } },
232
+ ],
233
+ },
234
+ },
235
+ }],
236
+ default: {},
237
+ });
238
+
239
+ expect(result.ok).toBe(true);
240
+ expect(result.value.partial).toBe(false);
241
+ expect(result.value.providers[0].models['gpt-5']).toEqual({
242
+ id: 'gpt-5',
243
+ name: 'GPT 5',
244
+ capabilities: {
245
+ toolcall: true,
246
+ input: { text: true, image: true },
247
+ output: { text: true },
248
+ },
249
+ cost: { input: 1, output: 2, cache: { read: 0.1, write: 0.2 } },
250
+ variants: { low: {}, high: {}, max: {} },
251
+ });
252
+ expect(JSON.stringify(result.value)).not.toContain('sentinel');
253
+ });
254
+
207
255
  it('treats empty/null release_date as absent without marking the catalog partial', () => {
208
256
  const catalog = safeCatalog();
209
257
  catalog.providers[0].models['gpt-5'].release_date = '';
@@ -11,6 +11,10 @@ import {
11
11
  extractDirectoryFromRequest,
12
12
  isDirectoryTurnAdmissionPath,
13
13
  } from './instance-recovery-runtime.js';
14
+ import {
15
+ runtimeContractExecutionBlockedBody,
16
+ shouldBlockRuntimeContractExecution,
17
+ } from './runtime-contract.js';
14
18
  import {
15
19
  createReasoningOutboundFilter,
16
20
  createSseBlockSplitter,
@@ -20,6 +24,11 @@ import {
20
24
  readIncludeReasoningQuery,
21
25
  stripIncludeReasoningParam,
22
26
  } from '../event-stream/reasoning-projection.js';
27
+ import {
28
+ isSessionLifecycleEventType,
29
+ projectSessionLifecyclePayload,
30
+ projectSessionWithStoredMap,
31
+ } from '../session-metadata/session-projection.js';
23
32
 
24
33
  /**
25
34
  * v2 protocol mounts every JSON/SSE route under `/api`
@@ -244,6 +253,8 @@ export const registerOpenCodeProxy = (app, deps) => {
244
253
  SSE_UPSTREAM_CONNECT_TIMEOUT_MS,
245
254
  SSE_UPSTREAM_STALL_TIMEOUT_MS,
246
255
  getRuntime,
256
+ /** Optional DI: `{ allowMissingContract: true }` for unit factories only. */
257
+ getRuntimeContractGateOptions = null,
247
258
  getOpenCodeAuthHeaders,
248
259
  buildOpenCodeUrl,
249
260
  ensureOpenCodeApiPrefix,
@@ -253,50 +264,137 @@ export const registerOpenCodeProxy = (app, deps) => {
253
264
  // wired, fold its per-session metadata onto list/detail responses so
254
265
  // clients keep reading `session.metadata` where they always did.
255
266
  getStoredSessionMetadata = null,
267
+ /** Sync committed snapshot; null = not ready / unknown when store is wired. */
268
+ getStoredSessionMetadataSync = null,
269
+ /** Await Host metadata load before SSE fan-out (resolves ok/false, does not reject). */
270
+ ensureSessionMetadataReady = null,
271
+ /** Best-effort Host metadata cleanup after successful upstream session delete. */
272
+ onSessionDeleted = null,
256
273
  } = deps;
257
274
 
275
+ const storeConfigured = typeof getStoredSessionMetadata === 'function';
276
+
277
+ const waitForHostMetadataReady = async () => {
278
+ if (!storeConfigured) return true;
279
+ if (typeof ensureSessionMetadataReady === 'function') {
280
+ try {
281
+ return await ensureSessionMetadataReady() === true;
282
+ } catch {
283
+ return false;
284
+ }
285
+ }
286
+ // Fallback: one getAll attempt.
287
+ try {
288
+ await getStoredSessionMetadata();
289
+ return true;
290
+ } catch {
291
+ return false;
292
+ }
293
+ };
294
+
258
295
  /**
259
- * `{ [sessionID]: metadata }` for the current instance, or `null` when the
260
- * store cannot answer. `null` means "unknown", and an unknown answer leaves
261
- * the upstream record untouched — never rewrite a session as empty metadata.
296
+ * When the Host store is wired, read failures are retryable errors — never
297
+ * silent upstream-without-overlay (that would look like authoritative empty
298
+ * Host state). When the store is not wired, leave upstream untouched.
299
+ * @returns {{ status: 'unconfigured' | 'ok' | 'unavailable', stored?: object, error?: string }}
262
300
  */
263
301
  const readStoredSessionMetadata = async () => {
264
- if (typeof getStoredSessionMetadata !== 'function') return null;
302
+ if (!storeConfigured) return { status: 'unconfigured' };
265
303
  try {
266
304
  const stored = await getStoredSessionMetadata();
267
- return stored && typeof stored === 'object' ? stored : null;
305
+ if (!stored || typeof stored !== 'object') {
306
+ return { status: 'unavailable', error: 'session metadata is unavailable' };
307
+ }
308
+ return { status: 'ok', stored };
268
309
  } catch (error) {
269
- console.warn('[proxy] session metadata unavailable:', error?.message ?? error);
310
+ const message = error instanceof Error ? error.message : 'session metadata is unavailable';
311
+ console.warn('[proxy] session metadata unavailable:', message);
312
+ return { status: 'unavailable', error: message };
313
+ }
314
+ };
315
+
316
+ const readStoredSessionMetadataSync = () => {
317
+ if (typeof getStoredSessionMetadataSync !== 'function') {
318
+ return storeConfigured ? null : undefined;
319
+ }
320
+ try {
321
+ const stored = getStoredSessionMetadataSync();
322
+ if (stored && typeof stored === 'object') return stored;
323
+ return null;
324
+ } catch {
270
325
  return null;
271
326
  }
272
327
  };
273
328
 
274
- /**
275
- * OpenChamber's stored metadata wins per key: OpenCode only ever saw what was
276
- * set at create time, and everything written since lives on our side.
277
- */
278
- const withStoredMetadata = (session, stored) => {
279
- if (!session || typeof session !== 'object' || typeof session.id !== 'string') return session;
280
- const ours = stored[session.id];
281
- if (!ours || typeof ours !== 'object' || Array.isArray(ours)) return session;
282
- const theirs = session.metadata && typeof session.metadata === 'object' && !Array.isArray(session.metadata)
283
- ? session.metadata
284
- : {};
285
- return { ...session, metadata: { ...theirs, ...ours } };
329
+ const metadataUnavailableResponse = (res, error) => {
330
+ res.status(503).json({
331
+ error: error || 'session metadata is unavailable',
332
+ code: 'session_metadata_unavailable',
333
+ retryable: true,
334
+ });
286
335
  };
287
336
 
337
+ /**
338
+ * OpenChamber's stored metadata deep-merges onto upstream create-time
339
+ * metadata, and Host archive authority projects onto `time.archived`.
340
+ */
288
341
  const overlaySession = (session, stored) => {
289
342
  if (!stored) return session;
290
- return withStoredMetadata(session, stored);
343
+ return projectSessionWithStoredMap(session, stored);
291
344
  };
292
345
 
346
+ /**
347
+ * @returns {Promise<{ ok: true, payload: unknown } | { ok: false, error: string }>}
348
+ */
293
349
  const overlayOwnedStateOnList = async (payload) => {
294
350
  const records = sessionListRecords(payload);
295
- if (!records) return payload;
296
- const stored = await readStoredSessionMetadata();
297
- if (!stored) return payload;
298
- const overlaid = records.map((session) => overlaySession(session, stored));
299
- return Array.isArray(payload) ? overlaid : { ...payload, data: overlaid };
351
+ if (!records) return { ok: true, payload };
352
+ const meta = await readStoredSessionMetadata();
353
+ if (meta.status === 'unconfigured') return { ok: true, payload };
354
+ if (meta.status === 'unavailable') return { ok: false, error: meta.error || 'session metadata is unavailable' };
355
+ const overlaid = records.map((session) => overlaySession(session, meta.stored));
356
+ // Preserve `{ data, cursor }` (and any other list envelope fields).
357
+ return {
358
+ ok: true,
359
+ payload: Array.isArray(payload) ? overlaid : { ...payload, data: overlaid },
360
+ };
361
+ };
362
+
363
+ const createHostSessionSseFilter = () => ({
364
+ projectEvent: (payload) => {
365
+ // Transcript/message hot path stays open. Only session lifecycle frames
366
+ // require Host authority; until the store is ready they are suppressed.
367
+ const stored = readStoredSessionMetadataSync();
368
+ if (storeConfigured && stored == null) {
369
+ const type = payload?.type || payload?.payload?.type;
370
+ if (isSessionLifecycleEventType(type)) return null;
371
+ return payload;
372
+ }
373
+ if (!stored) return payload;
374
+ return projectSessionLifecyclePayload(payload, (sessionId) => {
375
+ const host = stored[sessionId];
376
+ return host && typeof host === 'object' && !Array.isArray(host) ? host : null;
377
+ }, { hostReady: true });
378
+ },
379
+ });
380
+
381
+ const composeSseFilters = (...filters) => {
382
+ const active = filters.filter(Boolean);
383
+ if (active.length === 0) return null;
384
+ if (active.length === 1) return active[0];
385
+ return {
386
+ projectEvent: (payload) => {
387
+ let current = payload;
388
+ for (const filter of active) {
389
+ current = filter.projectEvent(current);
390
+ if (current == null) return null;
391
+ }
392
+ return current;
393
+ },
394
+ dispose: () => {
395
+ for (const filter of active) filter.dispose?.();
396
+ },
397
+ };
300
398
  };
301
399
 
302
400
  if (app.get('opencodeProxyConfigured')) {
@@ -449,6 +547,15 @@ export const registerOpenCodeProxy = (app, deps) => {
449
547
  };
450
548
 
451
549
  const forwardSseRequest = async (req, res) => {
550
+ // Gate session-lifecycle Host authority before attaching upstream SSE so we
551
+ // never drop the only lifecycle frame while metadata is still unknown.
552
+ if (storeConfigured) {
553
+ const ready = await waitForHostMetadataReady();
554
+ if (!ready) {
555
+ return metadataUnavailableResponse(res, 'session metadata is unavailable');
556
+ }
557
+ }
558
+
452
559
  const abortController = new AbortController();
453
560
  const closeUpstream = () => abortController.abort();
454
561
  let upstream = null;
@@ -466,7 +573,13 @@ export const registerOpenCodeProxy = (app, deps) => {
466
573
  const includeReasoning = readIncludeReasoningQuery(req.query)
467
574
  && readIncludeReasoningFromUrl(requestUrl);
468
575
  const reasoningFilter = includeReasoning ? null : createReasoningOutboundFilter();
469
- const sseSplitter = includeReasoning ? null : createSseBlockSplitter();
576
+ const hostSessionFilter = typeof getStoredSessionMetadataSync === 'function'
577
+ ? createHostSessionSseFilter()
578
+ : null;
579
+ const outboundSseFilter = composeSseFilters(reasoningFilter, hostSessionFilter);
580
+ // Parse SSE blocks when any Host projection filter is active (reasoning
581
+ // strip and/or session archive authority). Otherwise byte-identical passthrough.
582
+ const sseSplitter = outboundSseFilter ? createSseBlockSplitter() : null;
470
583
 
471
584
  req.on('close', closeUpstream);
472
585
 
@@ -582,20 +695,20 @@ export const registerOpenCodeProxy = (app, deps) => {
582
695
  }
583
696
  if (value && value.length > 0) {
584
697
  resetStallTimer();
585
- if (includeReasoning) {
586
- // Enabled path: byte-identical passthrough (existing behavior).
698
+ if (!outboundSseFilter || !sseSplitter) {
699
+ // No Host projection: byte-identical passthrough.
587
700
  sseBoundary.observe(value);
588
701
  const canContinue = await enqueueSseWrite(value);
589
702
  if (!canContinue) {
590
703
  break;
591
704
  }
592
705
  } else {
593
- // Disabled path: parse SSE blocks, drop reasoning, re-serialize.
594
- // Preserves id/event lines and GlobalEvent wrap when unchanged drop-only.
706
+ // Parse SSE blocks, apply reasoning strip + Host archive authority.
707
+ // Preserves id/event lines and GlobalEvent wrap when unchanged.
595
708
  const blocks = sseSplitter.push(value);
596
709
  let canContinue = true;
597
710
  for (const block of blocks) {
598
- const filtered = filterSseBlock(block, reasoningFilter);
711
+ const filtered = filterSseBlock(block, outboundSseFilter);
599
712
  if (!filtered) {
600
713
  continue;
601
714
  }
@@ -610,9 +723,9 @@ export const registerOpenCodeProxy = (app, deps) => {
610
723
  }
611
724
  }
612
725
 
613
- if (!includeReasoning && sseSplitter) {
726
+ if (outboundSseFilter && sseSplitter) {
614
727
  for (const block of sseSplitter.finish()) {
615
- const filtered = filterSseBlock(block, reasoningFilter);
728
+ const filtered = filterSseBlock(block, outboundSseFilter);
616
729
  if (filtered) {
617
730
  sseBoundary.observe(filtered);
618
731
  await enqueueSseWrite(filtered);
@@ -641,7 +754,7 @@ export const registerOpenCodeProxy = (app, deps) => {
641
754
  } finally {
642
755
  clearConnectTimer();
643
756
  clearStallTimer();
644
- reasoningFilter?.dispose?.();
757
+ outboundSseFilter?.dispose?.();
645
758
  if (heartbeatTimer) {
646
759
  clearTimeout(heartbeatTimer);
647
760
  heartbeatTimer = null;
@@ -659,7 +772,7 @@ export const registerOpenCodeProxy = (app, deps) => {
659
772
  }
660
773
  };
661
774
 
662
- const fetchSessionListPayload = async (upstreamPath, { req = null, timeoutMs = null } = {}) => {
775
+ const fetchSessionListPayload = async (upstreamPath, { req = null, timeoutMs = null, method = 'GET' } = {}) => {
663
776
  const headers = req
664
777
  ? {
665
778
  ...normalizeForwardedDirectoryHeaders(collectForwardProxyHeaders(req.headers, getOpenCodeAuthHeaders())),
@@ -672,7 +785,7 @@ export const registerOpenCodeProxy = (app, deps) => {
672
785
  'accept-encoding': 'identity',
673
786
  };
674
787
  const upstream = await fetch(buildOpenCodeUrl(upstreamPath, ''), {
675
- method: 'GET',
788
+ method: method || 'GET',
676
789
  headers,
677
790
  ...(typeof timeoutMs === 'number' ? { signal: AbortSignal.timeout(timeoutMs) } : {}),
678
791
  });
@@ -720,7 +833,11 @@ export const registerOpenCodeProxy = (app, deps) => {
720
833
  }
721
834
 
722
835
  res.setHeader('content-type', result.contentType);
723
- res.json(await overlayOwnedStateOnList(sanitizeSessionListPayload(result.payload)));
836
+ const overlaid = await overlayOwnedStateOnList(sanitizeSessionListPayload(result.payload));
837
+ if (!overlaid.ok) {
838
+ return metadataUnavailableResponse(res, overlaid.error);
839
+ }
840
+ res.json(overlaid.payload);
724
841
  } catch (error) {
725
842
  if (isAbortError(error)) {
726
843
  return;
@@ -835,12 +952,42 @@ export const registerOpenCodeProxy = (app, deps) => {
835
952
  req.path.startsWith('/config/settings') ||
836
953
  req.path.startsWith('/config/skills') ||
837
954
  req.path === '/config/reload' ||
838
- req.path === '/health'
955
+ req.path === '/health' ||
956
+ req.path === '/opencode/health' ||
957
+ req.path === '/opencode/version' ||
958
+ req.path === '/opencode/contract' ||
959
+ req.path === '/opencode/upgrade-status' ||
960
+ req.path === '/opencode/upgrade'
839
961
  ) {
840
962
  return next();
841
963
  }
842
964
 
965
+ // Ticket 11: limit execution when the running serve fails contract admission.
966
+ // Diagnostics, reads, and stop-task stay available; health success alone is not full semantics.
967
+ // Require explicit executionAllowed === true for the *current* instance (null/pending blocks).
968
+ // Gate again after readiness hold so initial-unknown → ready-incompatible cannot slip through.
969
+ const pathForGate = req.originalUrl || req.url || req.path;
970
+ const gateOptions = typeof getRuntimeContractGateOptions === 'function'
971
+ ? getRuntimeContractGateOptions()
972
+ : {};
973
+ const blockedByContract = () => {
974
+ const contract = getRuntime()?.runtimeContract;
975
+ if (shouldBlockRuntimeContractExecution(req.method, pathForGate, contract, gateOptions)) {
976
+ return runtimeContractExecutionBlockedBody(contract);
977
+ }
978
+ return null;
979
+ };
980
+
981
+ const earlyBlock = blockedByContract();
982
+ if (earlyBlock) {
983
+ return res.status(409).json(earlyBlock);
984
+ }
985
+
843
986
  if (!shouldHoldProxiedRequest(getRuntime())) {
987
+ const lateBlock = blockedByContract();
988
+ if (lateBlock) {
989
+ return res.status(409).json(lateBlock);
990
+ }
844
991
  return next();
845
992
  }
846
993
 
@@ -850,6 +997,11 @@ export const registerOpenCodeProxy = (app, deps) => {
850
997
  if (res.writableEnded || req.aborted) return;
851
998
  const runtimeState = getRuntime();
852
999
  if (!shouldHoldProxiedRequest(runtimeState)) {
1000
+ // Re-evaluate after wait: startup may have published incompatible/pending.
1001
+ const afterHoldBlock = blockedByContract();
1002
+ if (afterHoldBlock) {
1003
+ return res.status(409).json(afterHoldBlock);
1004
+ }
853
1005
  return next();
854
1006
  }
855
1007
  if (!isMigrationBlockingTranscript(runtimeState) && Date.now() >= processDeadline) {
@@ -938,7 +1090,11 @@ export const registerOpenCodeProxy = (app, deps) => {
938
1090
  return bTime - aTime;
939
1091
  });
940
1092
  console.log(`[SessionMerge] ${globalSessions?.length || 0} global + ${extraSessions.length} extra = ${merged.length} total`);
941
- return res.json(await overlayOwnedStateOnList(sanitizeSessionListPayload(merged)));
1093
+ const overlaid = await overlayOwnedStateOnList(sanitizeSessionListPayload(merged));
1094
+ if (!overlaid.ok) {
1095
+ return metadataUnavailableResponse(res, overlaid.error);
1096
+ }
1097
+ return res.json(overlaid.payload);
942
1098
  } catch (error) {
943
1099
  console.log(`[SessionMerge] Error: ${error.message}`);
944
1100
  return res.status(500).json({ error: error.message || 'Failed to merge Windows sessions' });
@@ -954,7 +1110,7 @@ export const registerOpenCodeProxy = (app, deps) => {
954
1110
  // with the list it came from. Everything else about the record is forwarded
955
1111
  // untouched. Without a store, fall through to the generic proxy.
956
1112
  app.get('/api/session/:sessionID', async (req, res, next) => {
957
- if (typeof getStoredSessionMetadata !== 'function') return next();
1113
+ if (!storeConfigured) return next();
958
1114
  // Nested routes (message, children, …) are registered separately; Express
959
1115
  // still matches this pattern for bare session ids only when no further
960
1116
  // segment is present — but be defensive if a child path slips through.
@@ -965,26 +1121,33 @@ export const registerOpenCodeProxy = (app, deps) => {
965
1121
  const upstreamPath = await getRequestUpstreamPath(req);
966
1122
  const result = await fetchSessionListPayload(upstreamPath, { req });
967
1123
 
968
- res.status(result.upstream.status);
969
- applyForwardProxyResponseHeaders(result.upstream.headers, res);
970
- res.setHeader('content-type', result.contentType);
971
-
972
1124
  const record = result.isJson && !result.parseError ? result.payload : null;
973
1125
  const session = record && typeof record === 'object' && !Array.isArray(record)
974
1126
  ? (record.data && typeof record.data === 'object' && !Array.isArray(record.data) ? record.data : record)
975
1127
  : null;
976
1128
  if (!session || typeof session.id !== 'string') {
1129
+ res.status(result.upstream.status);
1130
+ applyForwardProxyResponseHeaders(result.upstream.headers, res);
1131
+ res.setHeader('content-type', result.contentType);
977
1132
  res.end(result.bodyText);
978
1133
  return;
979
1134
  }
980
1135
 
981
- const stored = await readStoredSessionMetadata();
982
- if (!stored) {
1136
+ const meta = await readStoredSessionMetadata();
1137
+ if (meta.status === 'unavailable') {
1138
+ return metadataUnavailableResponse(res, meta.error);
1139
+ }
1140
+
1141
+ res.status(result.upstream.status);
1142
+ applyForwardProxyResponseHeaders(result.upstream.headers, res);
1143
+ res.setHeader('content-type', result.contentType);
1144
+
1145
+ if (meta.status === 'unconfigured') {
983
1146
  res.end(result.bodyText);
984
1147
  return;
985
1148
  }
986
1149
 
987
- const overlaid = overlaySession(session, stored);
1150
+ const overlaid = overlaySession(session, meta.stored);
988
1151
  res.json(record.data && typeof record.data === 'object' && !Array.isArray(record.data)
989
1152
  ? { ...record, data: overlaid }
990
1153
  : overlaid);
@@ -999,11 +1162,54 @@ export const registerOpenCodeProxy = (app, deps) => {
999
1162
  }
1000
1163
  });
1001
1164
 
1165
+ // Successful upstream delete → idempotent Host metadata cleanup. Failures are
1166
+ // observable (logged) and compensated by session.deleted event ingest.
1167
+ app.delete('/api/session/:sessionID', async (req, res, next) => {
1168
+ if (typeof onSessionDeleted !== 'function') return next();
1169
+ const sessionID = typeof req.params?.sessionID === 'string' ? req.params.sessionID.trim() : '';
1170
+ if (!sessionID || sessionID.includes('/')) return next();
1171
+ try {
1172
+ const upstreamPath = await getRequestUpstreamPath(req);
1173
+ const result = await fetchSessionListPayload(upstreamPath, {
1174
+ req,
1175
+ method: 'DELETE',
1176
+ });
1177
+ res.status(result.upstream.status);
1178
+ applyForwardProxyResponseHeaders(result.upstream.headers, res);
1179
+ res.setHeader('content-type', result.contentType || 'application/json');
1180
+ if (result.upstream.status >= 200 && result.upstream.status < 300) {
1181
+ try {
1182
+ const cleanup = onSessionDeleted(sessionID);
1183
+ if (cleanup && typeof cleanup.catch === 'function') {
1184
+ cleanup.catch((error) => {
1185
+ console.warn('[proxy] session metadata cleanup after delete failed (retry via event):', error?.message ?? error);
1186
+ });
1187
+ }
1188
+ } catch (error) {
1189
+ console.warn('[proxy] session metadata cleanup after delete failed (retry via event):', error?.message ?? error);
1190
+ }
1191
+ }
1192
+ // Upstream failure keeps Host metadata (do not cleanup).
1193
+ res.end(result.bodyText);
1194
+ } catch (error) {
1195
+ if (isAbortError(error)) return;
1196
+ console.error('[proxy] OpenCode session.delete proxy error:', error?.message ?? error);
1197
+ if (!res.headersSent) {
1198
+ res.status(503).json({ error: 'OpenCode service unavailable' });
1199
+ return;
1200
+ }
1201
+ next(error);
1202
+ }
1203
+ });
1204
+
1002
1205
  // Official session.messages list (not exact message — that is owned by
1003
1206
  // session-turn-pages and registered before this proxy).
1004
1207
  app.get('/api/session/:sessionID/message', (req, res, next) => {
1005
1208
  return forwardSessionMessagesListRequest(req, res, next);
1006
1209
  });
1210
+ app.get('/api/session/:sessionID/context', (req, res, next) => {
1211
+ return forwardSessionMessagesListRequest(req, res, next);
1212
+ });
1007
1213
 
1008
1214
  app.get('/api/global/event', forwardSseRequest);
1009
1215
  app.get('/api/event', forwardSseRequest);