@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
@@ -0,0 +1,654 @@
1
+ /**
2
+ * Host-owned session archive domain operations.
3
+ *
4
+ * Persists `metadata.openchamber.archive.archivedAt` in the session-metadata
5
+ * store, projects full Session records for HTTP/SSE consumers, and schedules
6
+ * bounded index repair after durable metadata commit when SQLite apply fails.
7
+ */
8
+
9
+ import {
10
+ isSessionArchived,
11
+ projectSessionWithHostMetadata,
12
+ readHostArchivedAt,
13
+ resolveSessionDirectory,
14
+ } from './session-projection.js';
15
+
16
+ const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
17
+
18
+ const asNonEmptyString = (value) => {
19
+ if (typeof value !== 'string') return null;
20
+ const trimmed = value.trim();
21
+ return trimmed.length > 0 ? trimmed : null;
22
+ };
23
+
24
+ const normalizeDirectory = (value) => {
25
+ if (typeof value !== 'string') return null;
26
+ const normalized = value.trim().replace(/\\/g, '/').replace(/\/+$/, '');
27
+ return normalized || null;
28
+ };
29
+
30
+ /** Bounded backoff for index repair and metadata cleanup (ms). */
31
+ const REPAIR_DELAYS_MS = [250, 750, 1_500, 3_000, 6_000];
32
+ const CLEANUP_DELAYS_MS = [200, 600, 1_200, 2_500, 5_000];
33
+
34
+ export class SessionArchiveError extends Error {
35
+ /**
36
+ * @param {string} code
37
+ * @param {string} message
38
+ * @param {number} [status]
39
+ */
40
+ constructor(code, message, status = 400) {
41
+ super(message);
42
+ this.name = 'SessionArchiveError';
43
+ this.code = code;
44
+ this.status = status;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * @param {object} deps
50
+ * @param {{ setSessionMetadata: Function, get: Function, removeSession?: Function, getSnapshotSync?: Function }} deps.sessionMetadataStore
51
+ * @param {(input: { sessionID: string, directory?: string | null }) => Promise<object | null>} deps.fetchUpstreamSession
52
+ * @param {{ upsert?: Function, upsertAndReportChange?: Function, remove?: Function } | null} [deps.sessionIndexService]
53
+ * @param {(event: { type: string, properties: object }, options?: object) => void} [deps.broadcastSessionEvent]
54
+ * @param {(sessionID: string) => void} [deps.onIndexChanged]
55
+ * @param {() => number} [deps.now]
56
+ * @param {typeof setTimeout} [deps.setTimer]
57
+ * @param {typeof clearTimeout} [deps.clearTimer]
58
+ */
59
+ export const createSessionArchiveService = (deps = {}) => {
60
+ const {
61
+ sessionMetadataStore = null,
62
+ fetchUpstreamSession = null,
63
+ sessionIndexService = null,
64
+ broadcastSessionEvent = null,
65
+ onIndexChanged = null,
66
+ now = Date.now,
67
+ setTimer = setTimeout,
68
+ clearTimer = clearTimeout,
69
+ } = deps;
70
+
71
+ if (!sessionMetadataStore || typeof sessionMetadataStore.setSessionMetadata !== 'function') {
72
+ throw new Error('createSessionArchiveService requires sessionMetadataStore');
73
+ }
74
+ if (typeof fetchUpstreamSession !== 'function') {
75
+ throw new Error('createSessionArchiveService requires fetchUpstreamSession');
76
+ }
77
+
78
+ /**
79
+ * Per-session operation generation. Bumped on every successful Host mutation
80
+ * (archive/unarchive), forget/delete, and stop. In-flight repairs capture the
81
+ * generation at start and abort after await if a newer operation won.
82
+ * @type {Map<string, number>}
83
+ */
84
+ const operationGeneration = new Map();
85
+ /** @type {Map<string, { sessionID: string, directory: string | null, archivedAt: number, generation: number, attempts: number, error?: string, failedAt?: number, timer?: ReturnType<typeof setTimeout> }>} */
86
+ const pendingIndexRepair = new Map();
87
+ /** @type {Map<string, { sessionID: string, attempts: number, error?: string, failedAt?: number, timer?: ReturnType<typeof setTimeout> }>} */
88
+ const pendingCleanup = new Map();
89
+ let stopped = false;
90
+
91
+ const getOperationGeneration = (sessionID) => operationGeneration.get(sessionID) ?? 0;
92
+
93
+ const bumpOperationGeneration = (sessionID) => {
94
+ const next = getOperationGeneration(sessionID) + 1;
95
+ operationGeneration.set(sessionID, next);
96
+ return next;
97
+ };
98
+
99
+ const clearRepairTimer = (entry) => {
100
+ if (entry?.timer) {
101
+ clearTimer(entry.timer);
102
+ entry.timer = undefined;
103
+ }
104
+ };
105
+
106
+ /**
107
+ * Invalidate in-flight repair for this session: bump generation + clear timer.
108
+ * Does not delete a *newer* pending that already carries the bumped generation.
109
+ */
110
+ const invalidateSessionRepairs = (sessionID, { clearPending = false } = {}) => {
111
+ const id = asNonEmptyString(sessionID);
112
+ if (!id) return 0;
113
+ const gen = bumpOperationGeneration(id);
114
+ const pending = pendingIndexRepair.get(id);
115
+ if (pending) {
116
+ clearRepairTimer(pending);
117
+ if (clearPending || pending.generation < gen) {
118
+ // Only drop pending when explicitly clearing or it belongs to an older op.
119
+ if (pending.generation < gen) pendingIndexRepair.delete(id);
120
+ else if (clearPending) pendingIndexRepair.delete(id);
121
+ }
122
+ }
123
+ return gen;
124
+ };
125
+
126
+ const readHostMetadataSync = (sessionID) => {
127
+ if (typeof sessionMetadataStore.getSnapshotSync === 'function') {
128
+ const snap = sessionMetadataStore.getSnapshotSync();
129
+ if (snap && isPlainObject(snap[sessionID])) return snap[sessionID];
130
+ return null;
131
+ }
132
+ return null;
133
+ };
134
+
135
+ const projectSession = (session, hostMetadata = null) => {
136
+ const host = hostMetadata ?? (
137
+ session?.id && typeof sessionMetadataStore.getSnapshotSync === 'function'
138
+ ? readHostMetadataSync(session.id)
139
+ : null
140
+ );
141
+ return projectSessionWithHostMetadata(session, host);
142
+ };
143
+
144
+ const applyIndexRows = (projected) => {
145
+ if (!sessionIndexService || !projected?.id) {
146
+ return { ok: true, skipped: true };
147
+ }
148
+ try {
149
+ if (typeof sessionIndexService.upsertAndReportChange === 'function') {
150
+ const changed = sessionIndexService.upsertAndReportChange(projected);
151
+ if (changed && typeof onIndexChanged === 'function') onIndexChanged(projected.id);
152
+ } else if (typeof sessionIndexService.upsert === 'function') {
153
+ sessionIndexService.upsert(projected);
154
+ if (typeof onIndexChanged === 'function') onIndexChanged(projected.id);
155
+ }
156
+ return { ok: true };
157
+ } catch (error) {
158
+ const message = error?.message ?? String(error);
159
+ console.warn('[session-archive] session index update failed after commit:', message);
160
+ return { ok: false, error: message, retryable: true };
161
+ }
162
+ };
163
+
164
+ const isRepairStillCurrent = (sessionID, generation) => {
165
+ if (stopped) return false;
166
+ return getOperationGeneration(sessionID) === generation;
167
+ };
168
+
169
+ /**
170
+ * Fetch upstream first (may hang), then read the *latest* committed Host
171
+ * snapshot and apply — never use metadata captured before the await.
172
+ * Aborts without writing if generation was superseded (unarchive/delete/stop).
173
+ */
174
+ const rebuildAndApplyIndex = async ({ sessionID, directory, archivedAt, generation }) => {
175
+ const capturedGen = typeof generation === 'number' ? generation : getOperationGeneration(sessionID);
176
+
177
+ let upstream = null;
178
+ let fetchError = null;
179
+ try {
180
+ upstream = await fetchUpstreamSession({
181
+ sessionID,
182
+ directory: directory || undefined,
183
+ });
184
+ } catch (error) {
185
+ fetchError = error;
186
+ }
187
+
188
+ // Generation check AFTER the hangable await — stale repairs stop here.
189
+ if (!isRepairStillCurrent(sessionID, capturedGen)) {
190
+ return { ok: true, stale: true, projected: null };
191
+ }
192
+
193
+ // Latest committed Host authority — after upstream returns.
194
+ let metadata = {};
195
+ try {
196
+ metadata = await sessionMetadataStore.get(sessionID);
197
+ } catch (error) {
198
+ return { ok: false, error: error?.message ?? String(error), retryable: true };
199
+ }
200
+ if (!isRepairStillCurrent(sessionID, capturedGen)) {
201
+ return { ok: true, stale: true, projected: null };
202
+ }
203
+
204
+ const hostArchived = readHostArchivedAt(metadata);
205
+ const effectiveArchived = typeof hostArchived === 'number' ? hostArchived : archivedAt;
206
+
207
+ if (fetchError) {
208
+ if (Number(fetchError?.status) === 404 || Number(fetchError?.statusCode) === 404) {
209
+ if (typeof effectiveArchived === 'number' && effectiveArchived > 0 && sessionIndexService?.remove) {
210
+ if (!isRepairStillCurrent(sessionID, capturedGen)) {
211
+ return { ok: true, stale: true, projected: null };
212
+ }
213
+ try {
214
+ sessionIndexService.remove(sessionID);
215
+ if (typeof onIndexChanged === 'function') onIndexChanged(sessionID);
216
+ return { ok: true, projected: null };
217
+ } catch (removeError) {
218
+ return { ok: false, error: removeError?.message ?? String(removeError), retryable: true };
219
+ }
220
+ }
221
+ return { ok: true, projected: null };
222
+ }
223
+ return { ok: false, error: fetchError?.message ?? String(fetchError), retryable: true };
224
+ }
225
+
226
+ if (!upstream || typeof upstream !== 'object' || upstream.id !== sessionID) {
227
+ return { ok: false, error: 'upstream session unavailable for index repair', retryable: true };
228
+ }
229
+
230
+ const projected = projectSessionWithHostMetadata(
231
+ {
232
+ ...upstream,
233
+ directory: resolveSessionDirectory(upstream) || directory || upstream.directory,
234
+ },
235
+ metadata,
236
+ );
237
+
238
+ if (!isRepairStillCurrent(sessionID, capturedGen)) {
239
+ return { ok: true, stale: true, projected: null };
240
+ }
241
+
242
+ const applied = applyIndexRows(projected);
243
+ if (!isRepairStillCurrent(sessionID, capturedGen)) {
244
+ return { ok: true, stale: true, projected: null };
245
+ }
246
+ return applied.ok
247
+ ? { ok: true, projected }
248
+ : { ok: false, error: applied.error, retryable: true, projected };
249
+ };
250
+
251
+ const scheduleIndexRepair = (sessionID, directory, archivedAt, priorError = null, generation = null) => {
252
+ if (stopped) return;
253
+ const id = asNonEmptyString(sessionID);
254
+ if (!id) return;
255
+ const gen = typeof generation === 'number' ? generation : getOperationGeneration(id);
256
+ // A newer mutation already owns this session — do not reschedule an old op.
257
+ if (gen !== getOperationGeneration(id)) return;
258
+
259
+ const existing = pendingIndexRepair.get(id);
260
+ if (existing && existing.generation > gen) {
261
+ // Newer repair already pending — leave it alone.
262
+ return;
263
+ }
264
+ if (existing && existing.generation === gen) {
265
+ clearRepairTimer(existing);
266
+ } else if (existing) {
267
+ clearRepairTimer(existing);
268
+ }
269
+
270
+ const entry = {
271
+ sessionID: id,
272
+ directory: directory || existing?.directory || null,
273
+ archivedAt,
274
+ generation: gen,
275
+ attempts: existing?.generation === gen ? existing.attempts : 0,
276
+ error: priorError || existing?.error,
277
+ failedAt: now(),
278
+ };
279
+ pendingIndexRepair.set(id, entry);
280
+
281
+ if (entry.attempts >= REPAIR_DELAYS_MS.length) {
282
+ console.warn(`[session-archive] index repair exhausted for ${id}: ${entry.error}`);
283
+ return;
284
+ }
285
+
286
+ const delay = REPAIR_DELAYS_MS[entry.attempts];
287
+ entry.attempts += 1;
288
+ entry.timer = setTimer(() => {
289
+ entry.timer = undefined;
290
+ return (async () => {
291
+ if (!isRepairStillCurrent(id, gen)) {
292
+ // Only drop pending if it still matches this superseded generation.
293
+ const current = pendingIndexRepair.get(id);
294
+ if (current && current.generation === gen) pendingIndexRepair.delete(id);
295
+ return;
296
+ }
297
+ const result = await rebuildAndApplyIndex(entry);
298
+ if (result.stale) {
299
+ const current = pendingIndexRepair.get(id);
300
+ if (current && current.generation === gen) pendingIndexRepair.delete(id);
301
+ return;
302
+ }
303
+ if (result.ok) {
304
+ const current = pendingIndexRepair.get(id);
305
+ if (current && current.generation === gen) pendingIndexRepair.delete(id);
306
+ return;
307
+ }
308
+ if (!isRepairStillCurrent(id, gen)) return;
309
+ entry.error = result.error;
310
+ entry.failedAt = now();
311
+ scheduleIndexRepair(id, entry.directory, entry.archivedAt, result.error, gen);
312
+ })();
313
+ }, delay);
314
+ };
315
+
316
+ const applyIndexAfterCommit = (projected, { directory, archivedAt, generation } = {}) => {
317
+ const gen = typeof generation === 'number' ? generation : getOperationGeneration(projected?.id);
318
+ const result = applyIndexRows(projected);
319
+ if (result.ok) {
320
+ if (projected?.id) {
321
+ const pending = pendingIndexRepair.get(projected.id);
322
+ // Clear only pending belonging to this operation — not a newer repair.
323
+ if (pending && pending.generation === gen) {
324
+ clearRepairTimer(pending);
325
+ pendingIndexRepair.delete(projected.id);
326
+ }
327
+ }
328
+ return { ok: true };
329
+ }
330
+ if (projected?.id) {
331
+ scheduleIndexRepair(
332
+ projected.id,
333
+ directory ?? projected.directory ?? null,
334
+ archivedAt,
335
+ result.error,
336
+ gen,
337
+ );
338
+ }
339
+ return { ok: false, error: result.error, retryable: true };
340
+ };
341
+
342
+ const broadcastAfterCommit = (projected) => {
343
+ if (typeof broadcastSessionEvent !== 'function' || !projected?.id) return;
344
+ try {
345
+ broadcastSessionEvent({
346
+ type: 'session.updated',
347
+ properties: {
348
+ info: projected,
349
+ sessionID: projected.id,
350
+ },
351
+ }, {
352
+ directory: typeof projected.directory === 'string' ? projected.directory : undefined,
353
+ });
354
+ } catch (error) {
355
+ console.warn('[session-archive] broadcast failed after commit:', error?.message ?? error);
356
+ }
357
+ };
358
+
359
+ /**
360
+ * Manual / status retry: rebuild from latest authority (not stale pending blob).
361
+ */
362
+ const retryIndexUpdate = async (sessionID) => {
363
+ const id = asNonEmptyString(sessionID);
364
+ if (!id) return { ok: false, error: 'session id required' };
365
+ const pending = pendingIndexRepair.get(id);
366
+ const directory = pending?.directory ?? null;
367
+ const archivedAt = pending?.archivedAt ?? 0;
368
+ const gen = pending?.generation ?? getOperationGeneration(id);
369
+ const result = await rebuildAndApplyIndex({
370
+ sessionID: id,
371
+ directory,
372
+ archivedAt,
373
+ generation: gen,
374
+ });
375
+ if (result.stale) return { ok: true, stale: true };
376
+ if (result.ok) {
377
+ const current = pendingIndexRepair.get(id);
378
+ if (current && current.generation === gen) pendingIndexRepair.delete(id);
379
+ return { ok: true };
380
+ }
381
+ scheduleIndexRepair(id, directory, archivedAt, result.error, gen);
382
+ return { ok: false, error: result.error, retryable: true };
383
+ };
384
+
385
+ const getIndexRepairStatus = (sessionID) => {
386
+ const id = asNonEmptyString(sessionID);
387
+ if (!id) return null;
388
+ const pending = pendingIndexRepair.get(id);
389
+ if (!pending) return { ok: true, pending: false };
390
+ return {
391
+ ok: false,
392
+ pending: true,
393
+ retryable: pending.attempts < REPAIR_DELAYS_MS.length,
394
+ attempts: pending.attempts,
395
+ error: pending.error,
396
+ failedAt: pending.failedAt,
397
+ };
398
+ };
399
+
400
+ const setArchive = async (input = {}) => {
401
+ const sessionID = asNonEmptyString(input.sessionID);
402
+ if (!sessionID) {
403
+ throw new SessionArchiveError('validation_error', 'a session id is required', 400);
404
+ }
405
+ if (typeof input.archivedAt !== 'number' || !Number.isFinite(input.archivedAt)) {
406
+ throw new SessionArchiveError('validation_error', 'archivedAt must be a finite number', 400);
407
+ }
408
+ if (input.archivedAt < 0) {
409
+ throw new SessionArchiveError('validation_error', 'archivedAt must be >= 0', 400);
410
+ }
411
+ const archivedAt = input.archivedAt > 0 ? Math.trunc(input.archivedAt) : 0;
412
+ const directoryHint = normalizeDirectory(input.directory);
413
+
414
+ let upstream;
415
+ try {
416
+ upstream = await fetchUpstreamSession({
417
+ sessionID,
418
+ directory: directoryHint,
419
+ });
420
+ } catch (error) {
421
+ const status = Number(error?.status) || Number(error?.statusCode) || 502;
422
+ if (status === 404) {
423
+ throw new SessionArchiveError('not_found', `session ${sessionID} was not found`, 404);
424
+ }
425
+ throw new SessionArchiveError(
426
+ 'upstream_error',
427
+ error?.message || 'failed to read upstream session',
428
+ status >= 400 && status < 600 ? status : 502,
429
+ );
430
+ }
431
+ if (!upstream || typeof upstream !== 'object' || typeof upstream.id !== 'string') {
432
+ throw new SessionArchiveError('not_found', `session ${sessionID} was not found`, 404);
433
+ }
434
+ if (upstream.id !== sessionID) {
435
+ throw new SessionArchiveError(
436
+ 'id_mismatch',
437
+ 'upstream session id does not match the requested session id',
438
+ 502,
439
+ );
440
+ }
441
+
442
+ const upstreamDirectory = normalizeDirectory(resolveSessionDirectory(upstream));
443
+ if (directoryHint && upstreamDirectory && directoryHint !== upstreamDirectory) {
444
+ throw new SessionArchiveError(
445
+ 'directory_mismatch',
446
+ 'directory does not match the upstream session',
447
+ 403,
448
+ );
449
+ }
450
+
451
+ const metadata = await sessionMetadataStore.setSessionMetadata(
452
+ sessionID,
453
+ { openchamber: { archive: { archivedAt } } },
454
+ { allowArchive: true },
455
+ );
456
+
457
+ // New Host mutation wins: bump generation and cancel older repair timers
458
+ // without wiping a pending that will be scheduled for *this* generation.
459
+ const gen = bumpOperationGeneration(sessionID);
460
+ const priorPending = pendingIndexRepair.get(sessionID);
461
+ if (priorPending && priorPending.generation < gen) {
462
+ clearRepairTimer(priorPending);
463
+ pendingIndexRepair.delete(sessionID);
464
+ }
465
+
466
+ const projected = projectSessionWithHostMetadata(
467
+ {
468
+ ...upstream,
469
+ directory: upstreamDirectory || directoryHint || upstream.directory,
470
+ },
471
+ metadata,
472
+ );
473
+
474
+ const index = applyIndexAfterCommit(projected, {
475
+ directory: upstreamDirectory || directoryHint,
476
+ archivedAt,
477
+ generation: gen,
478
+ });
479
+ broadcastAfterCommit(projected);
480
+
481
+ return {
482
+ session: projected,
483
+ metadata,
484
+ archivedAt,
485
+ index: index.ok
486
+ ? { ok: true }
487
+ : { ok: false, error: index.error, retryable: true },
488
+ };
489
+ };
490
+
491
+ const archiveSession = (sessionID, directory = null, archivedAt = now()) => setArchive({
492
+ sessionID,
493
+ directory: directory ?? undefined,
494
+ archivedAt: typeof archivedAt === 'number' && Number.isFinite(archivedAt) && archivedAt > 0
495
+ ? archivedAt
496
+ : now(),
497
+ });
498
+
499
+ const unarchiveSession = (sessionID, directory = null) => setArchive({
500
+ sessionID,
501
+ directory: directory ?? undefined,
502
+ archivedAt: 0,
503
+ });
504
+
505
+ const scheduleCleanup = (sessionID, priorError = null) => {
506
+ if (stopped) return;
507
+ const id = asNonEmptyString(sessionID);
508
+ if (!id) return;
509
+ const existing = pendingCleanup.get(id) ?? { sessionID: id, attempts: 0 };
510
+ clearRepairTimer(existing);
511
+ existing.error = priorError || existing.error;
512
+ existing.failedAt = now();
513
+ pendingCleanup.set(id, existing);
514
+
515
+ if (existing.attempts >= CLEANUP_DELAYS_MS.length) {
516
+ console.warn(`[session-archive] metadata cleanup exhausted for ${id}: ${existing.error}`);
517
+ return;
518
+ }
519
+
520
+ const delay = CLEANUP_DELAYS_MS[existing.attempts];
521
+ existing.attempts += 1;
522
+ existing.timer = setTimer(() => {
523
+ existing.timer = undefined;
524
+ return (async () => {
525
+ if (stopped) return;
526
+ const result = await runCleanupOnce(id);
527
+ if (result.ok) {
528
+ pendingCleanup.delete(id);
529
+ return;
530
+ }
531
+ existing.error = result.error;
532
+ existing.failedAt = now();
533
+ scheduleCleanup(id, result.error);
534
+ })();
535
+ }, delay);
536
+ };
537
+
538
+ const runCleanupOnce = async (sessionID) => {
539
+ if (typeof sessionMetadataStore.removeSession !== 'function') {
540
+ return { ok: true, removed: false };
541
+ }
542
+ try {
543
+ const removed = await sessionMetadataStore.removeSession(sessionID);
544
+ pendingIndexRepair.delete(sessionID);
545
+ // false = already absent → idempotent success
546
+ return { ok: true, removed: removed === true };
547
+ } catch (error) {
548
+ const message = error?.message ?? String(error);
549
+ console.warn('[session-archive] metadata cleanup write failed (retryable):', message);
550
+ return { ok: false, error: message, retryable: true };
551
+ }
552
+ };
553
+
554
+ /**
555
+ * Idempotent Host metadata cleanup after authoritative session delete.
556
+ * Not-found is success. Write failures schedule bounded retry and return
557
+ * observable retryable status — HTTP delete success is not reversed.
558
+ */
559
+ const forgetSession = async (sessionID) => {
560
+ const id = asNonEmptyString(sessionID);
561
+ if (!id) return { ok: true, removed: false };
562
+ // Supersede any in-flight index repair for this session (late GET must not write).
563
+ invalidateSessionRepairs(id, { clearPending: true });
564
+ const result = await runCleanupOnce(id);
565
+ if (result.ok) {
566
+ pendingCleanup.delete(id);
567
+ return result;
568
+ }
569
+ scheduleCleanup(id, result.error);
570
+ return { ok: false, removed: false, retryable: true, error: result.error };
571
+ };
572
+
573
+ const getCleanupStatus = (sessionID) => {
574
+ const id = asNonEmptyString(sessionID);
575
+ if (!id) return null;
576
+ const pending = pendingCleanup.get(id);
577
+ if (!pending) return { ok: true, pending: false };
578
+ return {
579
+ ok: false,
580
+ pending: true,
581
+ retryable: pending.attempts < CLEANUP_DELAYS_MS.length,
582
+ attempts: pending.attempts,
583
+ error: pending.error,
584
+ failedAt: pending.failedAt,
585
+ };
586
+ };
587
+
588
+ const stop = () => {
589
+ stopped = true;
590
+ // Bump every known session generation so late repair awaits refuse to write.
591
+ for (const id of new Set([
592
+ ...operationGeneration.keys(),
593
+ ...pendingIndexRepair.keys(),
594
+ ...pendingCleanup.keys(),
595
+ ])) {
596
+ bumpOperationGeneration(id);
597
+ }
598
+ for (const entry of pendingIndexRepair.values()) clearRepairTimer(entry);
599
+ for (const entry of pendingCleanup.values()) clearRepairTimer(entry);
600
+ pendingIndexRepair.clear();
601
+ pendingCleanup.clear();
602
+ };
603
+
604
+ return {
605
+ setArchive,
606
+ archiveSession,
607
+ unarchiveSession,
608
+ forgetSession,
609
+ retryIndexUpdate,
610
+ getIndexRepairStatus,
611
+ getCleanupStatus,
612
+ projectSession,
613
+ readHostArchivedAt: (metadata) => readHostArchivedAt(metadata),
614
+ isSessionArchived,
615
+ readHostMetadataSync,
616
+ stop,
617
+ };
618
+ };
619
+
620
+ export const createUpstreamSessionFetcher = ({
621
+ buildOpenCodeUrl,
622
+ getOpenCodeAuthHeaders,
623
+ fetchFn = globalThis.fetch,
624
+ }) => async ({ sessionID, directory = null }) => {
625
+ const id = asNonEmptyString(sessionID);
626
+ if (!id) return null;
627
+ const url = new URL(buildOpenCodeUrl(`/session/${encodeURIComponent(id)}`));
628
+ if (directory) url.searchParams.set('directory', directory);
629
+ const response = await fetchFn(url, {
630
+ method: 'GET',
631
+ headers: {
632
+ Accept: 'application/json',
633
+ ...(typeof getOpenCodeAuthHeaders === 'function' ? getOpenCodeAuthHeaders() : {}),
634
+ },
635
+ });
636
+ if (response.status === 404) {
637
+ const error = new Error('session not found');
638
+ error.status = 404;
639
+ throw error;
640
+ }
641
+ if (!response.ok) {
642
+ const error = new Error(`OpenCode session.get failed (${response.status})`);
643
+ error.status = response.status;
644
+ throw error;
645
+ }
646
+ const payload = await response.json();
647
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
648
+ if (payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
649
+ return payload.data;
650
+ }
651
+ if (typeof payload.id === 'string') return payload;
652
+ }
653
+ return null;
654
+ };