@openchambery/web 1.19.22-beta.1 → 2.0.0-beta.10

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 (328) 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-CzTtVMnI.js} +1 -1
  7. package/dist/assets/AssistantView-Cg1qQvVe.js +3 -0
  8. package/dist/assets/{DiagramView-D1LGK_s2.js → DiagramView-hOi72Wdx.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-B0kJo9Ii.js → FadeInOnReveal-DCqrQkkz.js} +1 -1
  10. package/dist/assets/Icon-B8fSTLdP.js +1 -0
  11. package/dist/assets/MarkdownRenderer-Cw74oVxV.js +2 -0
  12. package/dist/assets/{MarkdownRendererImpl-DfoCJ1Mq.js → MarkdownRendererImpl-YM7iRmzK.js} +3 -3
  13. package/dist/assets/MarkstreamRendererImpl-BDy_oGJQ.js +3 -0
  14. package/dist/assets/MobileDetailNavigation-iX_hb_Xt.js +1 -0
  15. package/dist/assets/{MobileShareBridge-2Yjn6wXg.js → MobileShareBridge-CAMUupwR.js} +2 -2
  16. package/dist/assets/MobileSurface-CHbdPcLH.js +2 -0
  17. package/dist/assets/{MultiRunWindow-tTbMFowS.js → MultiRunWindow-Ci1ODFFX.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-CD2KtlF3.js → OnboardingScreen-O4cGdz5p.js} +1 -1
  19. package/dist/assets/OverlayScrollbar-CRfGtcmG.js +2 -0
  20. package/dist/assets/SettingsView-Dg8BtTV1.js +25 -0
  21. package/dist/assets/{SettingsWindow-zIa76s4t.js → SettingsWindow-B8d8qIQ7.js} +1 -1
  22. package/dist/assets/TerminalView-CMiw3s_c.js +8 -0
  23. package/dist/assets/{ToolOutputDialog-DNqA4uBl.js → ToolOutputDialog-ByaQYRTp.js} +2 -2
  24. package/dist/assets/appThemeRegistry-BGGHio8T.js +1 -0
  25. package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
  26. package/dist/assets/{button-CI7Q-Zw_.js → button-DIIrsMjW.js} +1 -1
  27. package/dist/assets/{collapsible-D5Phaykh.js → collapsible-CnvUeYst.js} +1 -1
  28. package/dist/assets/{context-menu-CubSDW2g.js → context-menu-BGBUpZEQ.js} +1 -1
  29. package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-JGFqhlaM.js} +1 -1
  30. package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-CHEwrI93.js} +1 -1
  31. package/dist/assets/{diagram-DdLO8HZ9.js → diagram-rvn7_qBM.js} +1 -1
  32. package/dist/assets/{dialog-B6pnIYmp.js → dialog-CQ63sc9E.js} +1 -1
  33. package/dist/assets/es-Dsmc0A_3.js +15 -0
  34. package/dist/assets/fileMentionTouchSelection-BzzcWJCi.js +3 -0
  35. package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-DN6gbLhM.js} +1 -1
  36. package/dist/assets/fr-C_ZlZDI2.js +13 -0
  37. package/dist/assets/gitApi-BF8vuoZc.js +10 -0
  38. package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-BMztRu0c.js} +1 -1
  39. package/dist/assets/{input-D-__fwRi.js → input-BVu-iykl.js} +1 -1
  40. package/dist/assets/insertionBoundaries-EcAfGT2D.js +1 -0
  41. package/dist/assets/ja-BqqTaWTi.js +15 -0
  42. package/dist/assets/ko-DP2XPM37.js +15 -0
  43. package/dist/assets/main-B7t9lavz.js +2 -0
  44. package/dist/assets/main-D2BzpnUt.css +1 -0
  45. package/dist/assets/main-DWP-prNB.js +169 -0
  46. package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
  47. package/dist/assets/{markdownHeightCache-BhxqJOvf.js → markdownHeightCache-D9-B1wdY.js} +1 -1
  48. package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
  49. package/dist/assets/mcpQueries-CfXrAkuU.js +1 -0
  50. package/dist/assets/miniChat-DgOrEQbN.js +2 -0
  51. package/dist/assets/mobile-Co_ZOHkZ.js +2 -0
  52. package/dist/assets/multirun-DelyUO6k.js +4 -0
  53. package/dist/assets/persistence-D0y7226-.js +3 -0
  54. package/dist/assets/pl-BCf5uLlK.js +15 -0
  55. package/dist/assets/platform-CurqO28D.js +6 -0
  56. package/dist/assets/{projectMeta-S1hJ7oBp.js → projectMeta-CPElzJ0n.js} +2 -2
  57. package/dist/assets/pt-BR-W09Q2pvn.js +15 -0
  58. package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
  59. package/dist/assets/renderElectronMiniChatApp-pOE93w86.js +2 -0
  60. package/dist/assets/renderMobileApp-F7ZSLcw8.js +28 -0
  61. package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
  62. package/dist/assets/{runtimeConfig-CMkwoQ_X.js → runtimeConfig-DSS0KxG3.js} +1 -1
  63. package/dist/assets/runtimeEndpointReset-x2Wgx57R.js +149 -0
  64. package/dist/assets/sessionChangeSummary-BUGSYcHp.js +2 -0
  65. package/dist/assets/{sessionLookup-BrU3rFHJ.js → sessionLookup-Bss8M20i.js} +1 -1
  66. package/dist/assets/{sortable-tabs-strip-Fnfhf3K9.js → sortable-tabs-strip-CE7W7lJ9.js} +1 -1
  67. package/dist/assets/src-lS1keDVV.css +2 -0
  68. package/dist/assets/store-jaP79mqI.js +15 -0
  69. package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-mDR2jXTo.js} +1 -1
  70. package/dist/assets/textarea-XD2oX_Ee.js +1 -0
  71. package/dist/assets/{tooltip-0T5jv0cu.js → tooltip-DYeQ_Hvy.js} +1 -1
  72. package/dist/assets/uk-BgFzz8m2.js +15 -0
  73. package/dist/assets/{url-_z7evzdK.js → url-CGbJbiJq.js} +1 -1
  74. package/dist/assets/{useAppFontEffects-C02YFyAv.js → useAppFontEffects-BAWHZ-t0.js} +64 -45
  75. package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-Bz4tRflF.js} +1 -1
  76. package/dist/assets/useDesktopWindowControlsLayout-hN_nLb7T.js +1 -0
  77. package/dist/assets/useEffectiveDirectory-Do9DQ453.js +1 -0
  78. package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-Bjx8zkGM.js} +1 -1
  79. package/dist/assets/useMobileNavigationStore-BsY_gi_K.js +414 -0
  80. package/dist/assets/useSessionAutoCleanup-C4g5KmDf.js +2 -0
  81. package/dist/assets/useUIStore-syVmbuDA.js +1 -0
  82. package/dist/assets/{useWorkerHighlightedLines-C9z0ML1z.js → useWorkerHighlightedLines-B5QXxBok.js} +2 -2
  83. package/dist/assets/vendor-opencode-client-C12gfjag.js +12 -0
  84. package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-Dq5ocTF8.js} +1 -1
  85. package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
  86. package/dist/assets/zh-CN-BhqSLgUY.js +15 -0
  87. package/dist/assets/zh-TW-BsaBgY-p.js +15 -0
  88. package/dist/index.html +12 -12
  89. package/dist/mini-chat.html +12 -12
  90. package/dist/mobile.html +12 -12
  91. package/package.json +3 -2
  92. package/server/index.js +423 -20
  93. package/server/lib/assistants/DOCUMENTATION.md +26 -21
  94. package/server/lib/assistants/assign.js +2 -1
  95. package/server/lib/assistants/contact-store.js +37 -19
  96. package/server/lib/assistants/contracts.js +11 -1
  97. package/server/lib/assistants/projection-history.test.js +454 -0
  98. package/server/lib/assistants/routes.js +11 -5
  99. package/server/lib/assistants/service.js +850 -294
  100. package/server/lib/assistants/service.test.js +588 -274
  101. package/server/lib/browser-provider/DOCUMENTATION.md +141 -0
  102. package/server/lib/browser-provider/contract.js +312 -0
  103. package/server/lib/browser-provider/host.js +421 -0
  104. package/server/lib/browser-provider/host.test.js +304 -0
  105. package/server/lib/browser-provider/routes.js +122 -0
  106. package/server/lib/browser-provider/routes.test.js +120 -0
  107. package/server/lib/browser-provider/selection-store.js +43 -0
  108. package/server/lib/browser-provider/selection-store.test.js +35 -0
  109. package/server/lib/conversations/DOCUMENTATION.md +5 -5
  110. package/server/lib/conversations/service.js +87 -57
  111. package/server/lib/conversations/service.test.js +132 -100
  112. package/server/lib/conversations/validation.js +11 -1
  113. package/server/lib/event-stream/DOCUMENTATION.md +3 -1
  114. package/server/lib/event-stream/directory-ws-bridge.js +13 -2
  115. package/server/lib/event-stream/global-hub.js +51 -5
  116. package/server/lib/event-stream/reasoning-projection.js +16 -1
  117. package/server/lib/event-stream/reasoning-projection.test.js +15 -0
  118. package/server/lib/event-stream/runtime.js +57 -20
  119. package/server/lib/event-stream/runtime.test.js +112 -0
  120. package/server/lib/llm/DOCUMENTATION.md +95 -84
  121. package/server/lib/llm/catalog.js +111 -45
  122. package/server/lib/llm/catalog.test.js +121 -59
  123. package/server/lib/llm/completions.js +41 -12
  124. package/server/lib/llm/completions.test.js +45 -4
  125. package/server/lib/llm/generate.js +516 -453
  126. package/server/lib/llm/generate.test.js +705 -484
  127. package/server/lib/llm/routes.js +8 -4
  128. package/server/lib/message-queue/DOCUMENTATION.md +5 -5
  129. package/server/lib/message-queue/opencode-adapter.js +191 -104
  130. package/server/lib/message-queue/opencode-adapter.test.js +328 -58
  131. package/server/lib/message-queue/runtime.js +15 -2
  132. package/server/lib/message-queue/runtime.test.js +6 -6
  133. package/server/lib/message-queue/worker.js +7 -1
  134. package/server/lib/notifications/DOCUMENTATION.md +3 -1
  135. package/server/lib/notifications/runtime.js +133 -31
  136. package/server/lib/notifications/runtime.test.js +103 -0
  137. package/server/lib/notifications/template-runtime.js +130 -38
  138. package/server/lib/notifications/template-runtime.test.js +96 -0
  139. package/server/lib/opencode/DOCUMENTATION.md +111 -16
  140. package/server/lib/opencode/agent-document.d.ts +28 -0
  141. package/server/lib/opencode/agent-document.js +280 -0
  142. package/server/lib/opencode/agent-document.test.js +137 -0
  143. package/server/lib/opencode/agents.js +96 -0
  144. package/server/lib/opencode/auth-state-runtime.js +5 -0
  145. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  146. package/server/lib/opencode/config-entity-routes.js +117 -103
  147. package/server/lib/opencode/config-entity-routes.test.js +148 -10
  148. package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
  149. package/server/lib/opencode/ensure-cli.d.ts +8 -0
  150. package/server/lib/opencode/ensure-cli.js +238 -0
  151. package/server/lib/opencode/ensure-cli.test.js +119 -0
  152. package/server/lib/opencode/env-runtime.js +208 -111
  153. package/server/lib/opencode/env-runtime.test.js +211 -14
  154. package/server/lib/opencode/feature-routes-runtime.js +90 -5
  155. package/server/lib/opencode/feature-routes-runtime.test.js +25 -0
  156. package/server/lib/opencode/http-server-connections.js +98 -0
  157. package/server/lib/opencode/http-server-connections.test.js +321 -0
  158. package/server/lib/opencode/lifecycle.js +539 -95
  159. package/server/lib/opencode/lifecycle.test.js +698 -6
  160. package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
  161. package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
  162. package/server/lib/opencode/network-runtime.js +28 -16
  163. package/server/lib/opencode/network-runtime.test.js +68 -4
  164. package/server/lib/opencode/openchamber-routes.js +7 -1
  165. package/server/lib/opencode/opencode2-pin.js +139 -0
  166. package/server/lib/opencode/opencode2-pin.test.js +88 -0
  167. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +326 -0
  168. package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
  169. package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
  170. package/server/lib/opencode/plugin-compatibility.js +15 -0
  171. package/server/lib/opencode/plugin-compatibility.test.js +21 -0
  172. package/server/lib/opencode/plugin-routes.js +3 -18
  173. package/server/lib/opencode/plugin-routes.test.js +9 -9
  174. package/server/lib/opencode/plugins.js +4 -3
  175. package/server/lib/opencode/plugins.test.js +12 -0
  176. package/server/lib/opencode/provider-catalog.js +58 -12
  177. package/server/lib/opencode/provider-catalog.test.js +48 -0
  178. package/server/lib/opencode/proxy.js +373 -28
  179. package/server/lib/opencode/proxy.test.js +353 -0
  180. package/server/lib/opencode/routes.js +333 -76
  181. package/server/lib/opencode/routes.test.js +315 -50
  182. package/server/lib/opencode/runtime-contract.js +401 -0
  183. package/server/lib/opencode/runtime-contract.test.js +214 -0
  184. package/server/lib/opencode/server-opencode-fetch.js +242 -0
  185. package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
  186. package/server/lib/opencode/server-utils-runtime.js +35 -0
  187. package/server/lib/opencode/shared-service.integration.test.js +66 -0
  188. package/server/lib/opencode/shared-service.js +147 -0
  189. package/server/lib/opencode/shared-service.test.js +93 -0
  190. package/server/lib/opencode/shared.js +3 -3
  191. package/server/lib/opencode/shutdown-runtime.js +15 -6
  192. package/server/lib/opencode/shutdown-runtime.test.js +49 -1
  193. package/server/lib/opencode/skill-routes.js +26 -40
  194. package/server/lib/opencode/skill-routes.test.js +71 -7
  195. package/server/lib/opencode/skills.test.js +15 -0
  196. package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
  197. package/server/lib/opencode/static-routes-runtime.js +3 -1
  198. package/server/lib/opencode/static-routes-runtime.test.js +33 -0
  199. package/server/lib/opencode/upgrade-screen.d.ts +30 -0
  200. package/server/lib/opencode/upgrade-screen.js +299 -0
  201. package/server/lib/opencode/upgrade-screen.test.js +196 -0
  202. package/server/lib/opencode/v1-migration-gate.js +157 -0
  203. package/server/lib/opencode/v1-migration-gate.test.js +111 -0
  204. package/server/lib/opencode/v2-client.js +60 -0
  205. package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
  206. package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
  207. package/server/lib/opencode/watcher.js +2 -1
  208. package/server/lib/opencode/watcher.test.js +1 -1
  209. package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
  210. package/server/lib/permission-auto-accept/runtime.js +14 -7
  211. package/server/lib/permission-auto-accept/runtime.test.js +27 -12
  212. package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
  213. package/server/lib/scheduled-tasks/DOCUMENTATION.md +79 -39
  214. package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
  215. package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
  216. package/server/lib/scheduled-tasks/routes.js +27 -0
  217. package/server/lib/scheduled-tasks/routes.test.js +94 -0
  218. package/server/lib/scheduled-tasks/runtime.js +381 -310
  219. package/server/lib/scheduled-tasks/runtime.test.js +600 -309
  220. package/server/lib/session-goal/DOCUMENTATION.md +173 -22
  221. package/server/lib/session-goal/capability.js +57 -0
  222. package/server/lib/session-goal/capability.test.js +33 -0
  223. package/server/lib/session-goal/routes.js +240 -2
  224. package/server/lib/session-goal/routes.test.js +211 -0
  225. package/server/lib/session-goal/runtime.js +1617 -113
  226. package/server/lib/session-goal/runtime.test.js +1500 -70
  227. package/server/lib/session-index/DOCUMENTATION.md +18 -15
  228. package/server/lib/session-index/event-ingest.js +89 -16
  229. package/server/lib/session-index/event-ingest.test.js +73 -0
  230. package/server/lib/session-index/host-authority-snapshot.js +92 -0
  231. package/server/lib/session-index/routes.host-archive.test.js +201 -0
  232. package/server/lib/session-index/routes.js +63 -10
  233. package/server/lib/session-index/routes.test.js +8 -8
  234. package/server/lib/session-index/service.js +7 -1
  235. package/server/lib/session-index/sync-runtime.js +171 -41
  236. package/server/lib/session-index/sync-runtime.test.js +164 -9
  237. package/server/lib/session-metadata/DOCUMENTATION.md +96 -0
  238. package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
  239. package/server/lib/session-metadata/opencode-session-record.js +316 -0
  240. package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
  241. package/server/lib/session-metadata/routes.js +197 -0
  242. package/server/lib/session-metadata/routes.test.js +156 -0
  243. package/server/lib/session-metadata/session-archive.d.ts +73 -0
  244. package/server/lib/session-metadata/session-archive.js +654 -0
  245. package/server/lib/session-metadata/session-archive.test.js +359 -0
  246. package/server/lib/session-metadata/session-metadata-store.d.ts +38 -0
  247. package/server/lib/session-metadata/session-metadata-store.js +422 -0
  248. package/server/lib/session-metadata/session-metadata-store.test.js +379 -0
  249. package/server/lib/session-metadata/session-projection.d.ts +41 -0
  250. package/server/lib/session-metadata/session-projection.js +279 -0
  251. package/server/lib/session-metadata/session-projection.test.js +64 -0
  252. package/server/lib/session-metadata/system-session.js +55 -0
  253. package/server/lib/session-metadata/system-session.test.js +55 -0
  254. package/server/lib/session-title/DOCUMENTATION.md +40 -7
  255. package/server/lib/session-title/native-runtime.test.js +167 -0
  256. package/server/lib/session-title/runtime.js +69 -44
  257. package/server/lib/session-title/session-access.js +46 -0
  258. package/server/lib/session-title/session-access.test.js +91 -0
  259. package/server/lib/session-turn-pages/DOCUMENTATION.md +12 -11
  260. package/server/lib/session-turn-pages/reconcile.service.js +1 -1
  261. package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
  262. package/server/lib/session-turn-pages/routes.js +123 -75
  263. package/server/lib/session-turn-pages/routes.test.js +566 -0
  264. package/server/lib/session-turn-pages/service.js +6 -2
  265. package/server/lib/session-turn-pages/service.test.js +34 -0
  266. package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
  267. package/server/lib/session-turn-pages/session-message-projection.js +281 -0
  268. package/server/lib/small-model/DOCUMENTATION.md +101 -146
  269. package/server/lib/small-model/catalog.js +84 -54
  270. package/server/lib/small-model/catalog.test.js +111 -9
  271. package/server/lib/small-model/custom-api.js +68 -0
  272. package/server/lib/small-model/custom-api.test.js +64 -0
  273. package/server/lib/small-model/index.js +125 -241
  274. package/server/lib/small-model/resolve.js +35 -120
  275. package/server/lib/small-model/resolve.test.js +20 -110
  276. package/server/lib/small-model/routes.js +1 -3
  277. package/server/lib/small-model/summary-settings.test.js +157 -253
  278. package/server/opencode-proxy.test.js +63 -12
  279. package/dist/assets/AssistantView-BUveAgc4.js +0 -3
  280. package/dist/assets/Icon-BgdfPhNs.js +0 -1
  281. package/dist/assets/MarkdownRenderer-BbrbDH6-.js +0 -2
  282. package/dist/assets/MarkstreamRendererImpl-D2dsW5Lg.js +0 -3
  283. package/dist/assets/MobileDetailNavigation-B0aqKXd7.js +0 -1
  284. package/dist/assets/MobileSurface-DW7WfPsy.js +0 -2
  285. package/dist/assets/OverlayScrollbar-DrA0Vsg5.js +0 -2
  286. package/dist/assets/SettingsView-PkXIavr7.js +0 -25
  287. package/dist/assets/TerminalView-CDdzF4yi.js +0 -8
  288. package/dist/assets/appThemeRegistry-D5Yxg4YF.js +0 -1
  289. package/dist/assets/es-DA3MqOo0.js +0 -15
  290. package/dist/assets/fileMentionTouchSelection-DNeiqns-.js +0 -4
  291. package/dist/assets/fr-Dbz0CdIA.js +0 -13
  292. package/dist/assets/gitApi-DXyC4MiV.js +0 -7
  293. package/dist/assets/insertionBoundaries-KlOVrHHt.js +0 -1
  294. package/dist/assets/ja-DumanVQA.js +0 -15
  295. package/dist/assets/ko-C0IkiUjQ.js +0 -15
  296. package/dist/assets/main-BPS5ylib.js +0 -2
  297. package/dist/assets/main-Cpd9VWYs.js +0 -169
  298. package/dist/assets/main-qSeeJRW1.css +0 -1
  299. package/dist/assets/markdown-worker-CptoICmq.js +0 -2
  300. package/dist/assets/markdownTheme-DOfGg-A7.js +0 -1
  301. package/dist/assets/miniChat-B8_KLWiY.js +0 -2
  302. package/dist/assets/mobile-CtktKrXn.js +0 -2
  303. package/dist/assets/multirun-CYfP9Cfm.js +0 -4
  304. package/dist/assets/persistence-Ce3AnpEr.js +0 -3
  305. package/dist/assets/pl-B9No6MQp.js +0 -15
  306. package/dist/assets/platform-0w_Q9_ji.js +0 -2
  307. package/dist/assets/pt-BR-DyQ0WTDh.js +0 -15
  308. package/dist/assets/renderElectronMiniChatApp-iK14LWm8.js +0 -2
  309. package/dist/assets/renderMobileApp-DORLPWnb.js +0 -28
  310. package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
  311. package/dist/assets/runtimeEndpointReset-ChOxt-J7.js +0 -147
  312. package/dist/assets/sessionChangeSummary-NnxcqCTI.js +0 -2
  313. package/dist/assets/src-rS3zCcsC.css +0 -2
  314. package/dist/assets/store-5B_LDnLM.js +0 -15
  315. package/dist/assets/textarea-bHYxZtfH.js +0 -1
  316. package/dist/assets/uk-C_1KkqyP.js +0 -15
  317. package/dist/assets/useDesktopWindowControlsLayout-Dnr-jX0C.js +0 -1
  318. package/dist/assets/useEffectiveDirectory-CZLLyarc.js +0 -1
  319. package/dist/assets/useMobileNavigationStore-CoCiqNgm.js +0 -413
  320. package/dist/assets/useSessionAutoCleanup-xQyETQkG.js +0 -2
  321. package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
  322. package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
  323. package/dist/assets/zh-CN-uWILgBdt.js +0 -15
  324. package/dist/assets/zh-TW-MYfdvhbD.js +0 -15
  325. package/server/lib/small-model/call.js +0 -427
  326. package/server/lib/small-model/call.test.js +0 -382
  327. package/server/lib/small-model/opencode-session.js +0 -339
  328. package/server/lib/small-model/opencode-session.test.js +0 -177
@@ -24,24 +24,24 @@ const response = () => ({
24
24
  });
25
25
 
26
26
  describe('session index routes', () => {
27
- it('returns deterministic unsupported state outside Electron', () => {
27
+ it('returns deterministic unsupported state outside Electron', async () => {
28
28
  const { app, route } = registry();
29
29
  registerSessionIndexRoutes(app, { sessionIndexService: null });
30
30
  const res = response();
31
31
 
32
- route('GET', '/api/openchamber/session-index')({}, res);
32
+ await route('GET', '/api/openchamber/session-index')({}, res);
33
33
 
34
34
  expect(res.statusCode).toBe(501);
35
35
  expect(res.body).toMatchObject({ error: expect.stringContaining('unavailable') });
36
36
  });
37
37
 
38
- it('returns an Electron snapshot through the OpenChamber route', () => {
38
+ it('returns an Electron snapshot through the OpenChamber route', async () => {
39
39
  const { app, route } = registry();
40
40
  const sessionIndexService = { snapshot: () => ({ directories: [{ directory: '/repo', sessions: [] }] }) };
41
41
  registerSessionIndexRoutes(app, { sessionIndexService });
42
42
  const res = response();
43
43
 
44
- route('GET', '/api/openchamber/session-index')({}, res);
44
+ await route('GET', '/api/openchamber/session-index')({}, res);
45
45
 
46
46
  expect(res.statusCode).toBe(200);
47
47
  expect(res.body).toEqual({ available: true, directories: [{ directory: '/repo', sessions: [] }] });
@@ -91,7 +91,7 @@ describe('session index routes', () => {
91
91
  expect(route('GET', '/api/openchamber/session-index/changes')).toBeUndefined();
92
92
  });
93
93
 
94
- it('looks up a session by id for deep links', () => {
94
+ it('looks up a session by id for deep links', async () => {
95
95
  const { app, route } = registry();
96
96
  const findBySessionId = vi.fn(() => ({
97
97
  id: 'ses_abc',
@@ -106,7 +106,7 @@ describe('session index routes', () => {
106
106
  });
107
107
  const res = response();
108
108
 
109
- route('GET', '/api/openchamber/session-index/session/:sessionId')(
109
+ await route('GET', '/api/openchamber/session-index/session/:sessionId')(
110
110
  { params: { sessionId: 'ses_abc' } },
111
111
  res,
112
112
  );
@@ -119,7 +119,7 @@ describe('session index routes', () => {
119
119
  });
120
120
  });
121
121
 
122
- it('returns 404 when session id is unknown', () => {
122
+ it('returns 404 when session id is unknown', async () => {
123
123
  const { app, route } = registry();
124
124
  registerSessionIndexRoutes(app, {
125
125
  sessionIndexService: {
@@ -129,7 +129,7 @@ describe('session index routes', () => {
129
129
  });
130
130
  const res = response();
131
131
 
132
- route('GET', '/api/openchamber/session-index/session/:sessionId')(
132
+ await route('GET', '/api/openchamber/session-index/session/:sessionId')(
133
133
  { params: { sessionId: 'ses_missing' } },
134
134
  res,
135
135
  );
@@ -284,8 +284,14 @@ export const createSessionIndexService = ({ dbPath, getRuntimeConfig = () => nul
284
284
  const previousStateByID = new Map(
285
285
  existingSummaryState.all(key, normalizedDirectory).map((row) => [row.session_id, row]),
286
286
  );
287
+ // Active-only: archived sessions never enter the cold-start index. Host
288
+ // archive projection must set time.archived before replaceDirectory so a
289
+ // stale unarchived cache cannot resurrect them.
287
290
  const summaries = Array.isArray(sessions)
288
- ? sessions.map((session) => toSummary(session, normalizedDirectory)).filter(Boolean).slice(0, MAX_ROOT_SESSIONS)
291
+ ? sessions
292
+ .map((session) => toSummary(session, normalizedDirectory))
293
+ .filter((summary) => summary && !summary.archivedAt)
294
+ .slice(0, MAX_ROOT_SESSIONS)
289
295
  : [];
290
296
  touchDirectory.run({
291
297
  runtimeKey: key,
@@ -1,4 +1,6 @@
1
1
  const SESSION_LIMIT = 20;
2
+ /** Max upstream list pages while skipping consecutive Host-archived rows. */
3
+ const ARCHIVE_SKIP_PAGE_BUDGET = 5;
2
4
  const FULL_RECONCILE_INTERVAL_MS = 24 * 60 * 60 * 1000;
3
5
  const FETCH_TIMEOUT_MS = 6_000;
4
6
  const INTERACTIVE_YIELD_MS = 1_000;
@@ -15,12 +17,55 @@ const updatedAt = (session) => {
15
17
  return typeof value === 'number' && Number.isFinite(value) ? value : 0;
16
18
  };
17
19
 
20
+ const isArchivedSession = (session) => {
21
+ const archived = session?.time?.archived;
22
+ return typeof archived === 'number' && Number.isFinite(archived) && archived > 0;
23
+ };
24
+
25
+ const nonEmptySystemID = (value) => typeof value === 'string' && value.length > 0;
26
+
27
+ /** Match session-index service: system sessions never consume the active-20 budget. */
28
+ const isSystemSession = (session) => {
29
+ const openchamber = session?.metadata?.openchamber;
30
+ if (!openchamber || typeof openchamber !== 'object') return false;
31
+ if (openchamber.assigned?.from === 'contact') return false;
32
+ if (nonEmptySystemID(openchamber.assistant?.assistantID)) return true;
33
+ if (nonEmptySystemID(openchamber.scheduledTask?.taskID)) return true;
34
+ if (nonEmptySystemID(openchamber.smallModel?.purpose)) return true;
35
+ if (nonEmptySystemID(openchamber.llm?.purpose)) return true;
36
+ return false;
37
+ };
38
+
39
+ const isRootActiveCandidate = (session) => {
40
+ if (!session?.id || isArchivedSession(session)) return false;
41
+ if (typeof session.parentID === 'string' && session.parentID) return false;
42
+ if (session.title === 'smartfetch-secondary') return false;
43
+ if (isSystemSession(session)) return false;
44
+ return true;
45
+ };
46
+
47
+ /**
48
+ * v2 list cursor may be a string/number or `{ next }`. Never String(object).
49
+ * @returns {string | null}
50
+ */
51
+ export const extractSessionListCursorToken = (cursor) => {
52
+ if (cursor == null || cursor === '') return null;
53
+ if (typeof cursor === 'string' || typeof cursor === 'number' || typeof cursor === 'bigint') {
54
+ const token = String(cursor).trim();
55
+ return token.length > 0 ? token : null;
56
+ }
57
+ if (typeof cursor === 'object' && cursor.next != null && cursor.next !== '') {
58
+ return extractSessionListCursorToken(cursor.next);
59
+ }
60
+ return null;
61
+ };
62
+
18
63
  const mergeIncrementalSessions = (cached, changed) => {
19
64
  const byID = new Map();
20
65
  for (const session of cached ?? []) byID.set(session.id, session);
21
66
  for (const session of changed ?? []) {
22
67
  if (!session?.id) continue;
23
- if (session.time?.archived) byID.delete(session.id);
68
+ if (isArchivedSession(session)) byID.delete(session.id);
24
69
  else byID.set(session.id, session);
25
70
  }
26
71
  return [...byID.values()]
@@ -35,6 +80,12 @@ export const createSessionIndexSyncRuntime = ({
35
80
  waitForOpenCodeReady,
36
81
  /** Optional tip sink: ({ revision, sync }) after each revision bump. */
37
82
  onRevisionTip = null,
83
+ /**
84
+ * Optional Host authority projection applied to each upstream list page
85
+ * before active-only filtering (archive / metadata). Sync — no await.
86
+ * @type {((sessions: object[]) => object[]) | null}
87
+ */
88
+ projectSessions = null,
38
89
  fetchFn = globalThis.fetch,
39
90
  now = Date.now,
40
91
  setTimer = setTimeout,
@@ -116,53 +167,125 @@ export const createSessionIndexSyncRuntime = ({
116
167
  .find((entry) => entry.directory === directory);
117
168
 
118
169
  const fetchDirectory = async (task) => {
119
- const cached = readDirectory(task.directory);
120
- // Empty snapshots are not incrementally eligible. A successful [] still
121
- // stores lastSyncedAt as a worktree topology hint; using that watermark as
122
- // start would hide older historical roots until the 24h full reconcile.
123
- const useIncremental = Boolean(
124
- (cached?.sessions?.length ?? 0) > 0
125
- && cached?.lastSyncedAt > 0
126
- && now() - cached.lastFullSyncedAt < FULL_RECONCILE_INTERVAL_MS
127
- );
128
- // Keep parity with @opencode-ai/sdk/v2 experimental.session.list. The
129
- // regular /session endpoint has different pagination semantics.
130
- const url = new URL(buildOpenCodeUrl('/experimental/session'));
131
- url.searchParams.set('directory', task.directory);
132
- url.searchParams.set('roots', 'true');
133
- url.searchParams.set('limit', String(SESSION_LIMIT));
134
- if (useIncremental) url.searchParams.set('start', String(cached.lastSyncedAt));
135
-
170
+ // v2 `session.list` cursor pagination. Collect candidates across pages,
171
+ // then re-project once before commit so a Host archive that lands mid-sync
172
+ // cannot resurrect a row. Root/system filters run before the 20-slot budget.
173
+ // projectSessions failure aborts the refresh and keeps prior directory rows.
136
174
  const controller = new AbortController();
137
175
  currentController = controller;
138
176
  currentWasPreempted = false;
139
177
  const timeout = setTimer(() => controller.abort(), FETCH_TIMEOUT_MS);
140
178
  try {
141
- const response = await fetchFn(url, {
142
- method: 'GET',
143
- headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
144
- signal: controller.signal,
145
- });
146
- if (!response.ok) {
147
- const error = new Error(`OpenCode session list failed (${response.status})`);
148
- error.status = response.status;
149
- throw error;
179
+ /** @type {object[]} raw upstream sessions collected across pages */
180
+ const collected = [];
181
+ let pages = 0;
182
+ let cursorToken = null;
183
+ let upstreamHasMore = false;
184
+ let hitPageBudget = false;
185
+ let lastCursorToken = null;
186
+ let seenCursor = new Set();
187
+
188
+ while (collected.length < SESSION_LIMIT * 3 && pages < ARCHIVE_SKIP_PAGE_BUDGET) {
189
+ const url = new URL(buildOpenCodeUrl('/session'));
190
+ url.searchParams.set('directory', task.directory);
191
+ url.searchParams.set('limit', String(SESSION_LIMIT));
192
+ if (cursorToken) {
193
+ url.searchParams.set('cursor', cursorToken);
194
+ }
195
+
196
+ const response = await fetchFn(url, {
197
+ method: 'GET',
198
+ headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
199
+ signal: controller.signal,
200
+ });
201
+ if (!response.ok) {
202
+ const error = new Error(`OpenCode session list failed (${response.status})`);
203
+ error.status = response.status;
204
+ throw error;
205
+ }
206
+ const payload = await response.json();
207
+ const sessions = Array.isArray(payload) ? payload : payload?.data;
208
+ if (!Array.isArray(sessions)) throw new Error('Invalid OpenCode session list payload');
209
+ pages += 1;
210
+
211
+ for (const session of sessions) {
212
+ if (session?.id) collected.push(session);
213
+ }
214
+
215
+ // v2: exhaust by cursor.next when present. A short page with next is
216
+ // still more data (archived skips / filtered upstream). Bare arrays
217
+ // have no cursor — length < limit means end of list.
218
+ const isEnvelope = payload && typeof payload === 'object' && !Array.isArray(payload);
219
+ const nextToken = isEnvelope
220
+ ? extractSessionListCursorToken(payload.cursor)
221
+ : null;
222
+ const pageFull = sessions.length === SESSION_LIMIT;
223
+ if (nextToken) lastCursorToken = nextToken;
224
+
225
+ if (nextToken) {
226
+ upstreamHasMore = true;
227
+ if (seenCursor.has(nextToken)) {
228
+ // Duplicate cursor → stop (broken upstream) rather than loop forever.
229
+ hitPageBudget = true;
230
+ break;
231
+ }
232
+ seenCursor.add(nextToken);
233
+ if (pages >= ARCHIVE_SKIP_PAGE_BUDGET) {
234
+ hitPageBudget = true;
235
+ break;
236
+ }
237
+ cursorToken = nextToken;
238
+ continue;
239
+ }
240
+
241
+ // No next token: bare-array / terminal page.
242
+ if (!pageFull) {
243
+ upstreamHasMore = false;
244
+ break;
245
+ }
246
+ // Full page without cursor — cannot continue safely.
247
+ upstreamHasMore = true;
248
+ hitPageBudget = true;
249
+ break;
150
250
  }
151
- const sessions = await response.json();
152
- if (!Array.isArray(sessions)) throw new Error('Invalid OpenCode session list payload');
153
- const nextSessions = useIncremental
154
- ? mergeIncrementalSessions(cached?.sessions, sessions)
155
- : sessions.slice(0, SESSION_LIMIT);
156
- const oldest = nextSessions[nextSessions.length - 1];
251
+
252
+ // Re-project the full collected set at commit time (current Host authority).
253
+ let projected = collected;
254
+ if (typeof projectSessions === 'function') {
255
+ try {
256
+ const next = projectSessions(collected);
257
+ if (!Array.isArray(next)) {
258
+ throw new Error('projectSessions must return an array');
259
+ }
260
+ projected = next;
261
+ } catch (error) {
262
+ // Failure is not empty success — keep prior directory rows.
263
+ console.warn('[session-index] projectSessions failed; keeping prior directory:', error?.message ?? error);
264
+ throw error;
265
+ }
266
+ }
267
+
268
+ // Root + system filters before filling the active-20 capacity.
269
+ const active = [];
270
+ for (const session of projected) {
271
+ if (!isRootActiveCandidate(session)) continue;
272
+ if (active.length >= SESSION_LIMIT) break;
273
+ active.push(session);
274
+ }
275
+
276
+ const oldest = active[active.length - 1];
277
+ const cursorNumber = lastCursorToken && Number.isFinite(Number(lastCursorToken))
278
+ ? Number(lastCursorToken)
279
+ : (updatedAt(oldest) || null);
157
280
  sessionIndexService.replaceDirectory({
158
281
  directory: task.directory,
159
- sessions: nextSessions,
160
- cursor: useIncremental ? (cached?.cursor ?? null) : (updatedAt(oldest) || null),
161
- hasMore: useIncremental ? Boolean(cached?.hasMore) : sessions.length === SESSION_LIMIT,
162
- fullSync: !useIncremental,
282
+ sessions: active,
283
+ cursor: cursorNumber,
284
+ hasMore: upstreamHasMore || hitPageBudget || active.length === SESSION_LIMIT,
285
+ fullSync: true,
163
286
  now: now(),
164
287
  });
165
- for (const session of nextSessions) {
288
+ for (const session of active) {
166
289
  if (!session?.id) continue;
167
290
  const key = `${task.runtimeKey}\n${task.directory}\n${session.id}`;
168
291
  if (queuedChildFlagKeys.has(key)) continue;
@@ -187,7 +310,10 @@ export const createSessionIndexSyncRuntime = ({
187
310
  currentWasPreempted = false;
188
311
  const timeout = setTimer(() => controller.abort(), FETCH_TIMEOUT_MS);
189
312
  try {
190
- const url = new URL(buildOpenCodeUrl(`/session/${encodeURIComponent(task.sessionID)}/children`));
313
+ // v2 protocol has no `/session/:id/children` route; child sessions are a
314
+ // `parentID` filter on the session list (`GET /api/session`).
315
+ const url = new URL(buildOpenCodeUrl('/session'));
316
+ url.searchParams.set('parentID', task.sessionID);
191
317
  url.searchParams.set('directory', task.directory);
192
318
  const response = await fetchFn(url, {
193
319
  method: 'GET',
@@ -199,8 +325,12 @@ export const createSessionIndexSyncRuntime = ({
199
325
  error.status = response.status;
200
326
  throw error;
201
327
  }
202
- const children = await response.json();
203
- if (!Array.isArray(children)) throw new Error('Invalid OpenCode child-session list payload');
328
+ const payload = await response.json();
329
+ // v2 wraps list responses in `{ data, cursor }`; accept a bare array too.
330
+ const children = Array.isArray(payload)
331
+ ? payload
332
+ : (Array.isArray(payload?.data) ? payload.data : null);
333
+ if (!children) throw new Error('Invalid OpenCode child-session list payload');
204
334
  sessionIndexService.replaceChildSessions(task.directory, task.sessionID, children);
205
335
  return 'completed';
206
336
  } catch (error) {
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
 
3
- import { createSessionIndexSyncRuntime } from './sync-runtime.js';
3
+ import { createSessionIndexSyncRuntime, extractSessionListCursorToken } from './sync-runtime.js';
4
4
 
5
5
  const session = (id, updated, directory = '/repo') => ({
6
6
  id,
@@ -68,7 +68,7 @@ describe('session index background sync runtime', () => {
68
68
 
69
69
  expect(maxActive).toBe(1);
70
70
  expect(fetchFn).toHaveBeenCalledTimes(4);
71
- expect(fetchFn.mock.calls[0][0].pathname).toBe('/experimental/session');
71
+ expect(fetchFn.mock.calls[0][0].pathname).toBe('/session');
72
72
  expect(service.replaceDirectory).toHaveBeenCalledTimes(2);
73
73
  expect(service.replaceChildSessions).toHaveBeenCalledTimes(2);
74
74
  expect(runtime.snapshot().sync).toMatchObject({ completed: 2, total: 2, failedDirectories: [] });
@@ -89,7 +89,161 @@ describe('session index background sync runtime', () => {
89
89
  await expect(changed).resolves.toMatchObject({ revision: initialRevision + 1 });
90
90
  });
91
91
 
92
- it('uses the persisted watermark for a true incremental merge', async () => {
92
+ it('extracts cursor.next without stringifying objects', () => {
93
+ expect(extractSessionListCursorToken({ next: 'abc' })).toBe('abc');
94
+ expect(extractSessionListCursorToken({ next: 42 })).toBe('42');
95
+ expect(extractSessionListCursorToken({ foo: 1 })).toBeNull();
96
+ expect(extractSessionListCursorToken('plain')).toBe('plain');
97
+ });
98
+
99
+ it('continues paging when a short page still carries cursor.next', async () => {
100
+ const service = createService();
101
+ const cursors = [];
102
+ const runtime = createSessionIndexSyncRuntime({
103
+ sessionIndexService: service,
104
+ buildOpenCodeUrl: (route) => `http://opencode.test${route}`,
105
+ getOpenCodeAuthHeaders: () => ({}),
106
+ waitForOpenCodeReady: async () => true,
107
+ projectSessions: (sessions) => sessions,
108
+ fetchFn: async (url) => {
109
+ if (url.pathname !== '/session') {
110
+ return new Response(JSON.stringify([]), { status: 200 });
111
+ }
112
+ cursors.push(url.searchParams.get('cursor'));
113
+ if (!url.searchParams.get('cursor')) {
114
+ // Short page (not limit=20) but v2 still offers next — must continue.
115
+ return new Response(JSON.stringify({
116
+ data: [
117
+ { ...session('ses_arch_a', 30), time: { created: 1, updated: 30, archived: 9 } },
118
+ { ...session('ses_arch_b', 29), time: { created: 1, updated: 29, archived: 9 } },
119
+ ],
120
+ cursor: { next: 'more-after-short' },
121
+ }), { status: 200 });
122
+ }
123
+ return new Response(JSON.stringify({
124
+ data: [session('ses_active', 10)],
125
+ cursor: null,
126
+ }), { status: 200 });
127
+ },
128
+ });
129
+ runtime.enqueue(['/repo']);
130
+ await waitUntil(() => isFullyIdle(runtime));
131
+ expect(cursors).toContain('more-after-short');
132
+ expect(service.replaceDirectory.mock.calls[0][0].sessions).toEqual([
133
+ expect.objectContaining({ id: 'ses_active' }),
134
+ ]);
135
+ });
136
+
137
+ it('uses cursor.next for paging and does not write [object Object]', async () => {
138
+ const service = createService();
139
+ const cursors = [];
140
+ const runtime = createSessionIndexSyncRuntime({
141
+ sessionIndexService: service,
142
+ buildOpenCodeUrl: (route) => `http://opencode.test${route}`,
143
+ getOpenCodeAuthHeaders: () => ({}),
144
+ waitForOpenCodeReady: async () => true,
145
+ projectSessions: (sessions) => sessions,
146
+ fetchFn: async (url) => {
147
+ if (url.pathname !== '/session') {
148
+ return new Response(JSON.stringify([]), { status: 200 });
149
+ }
150
+ cursors.push(url.searchParams.get('cursor'));
151
+ if (!url.searchParams.get('cursor')) {
152
+ const archivedPage = Array.from({ length: 20 }, (_, index) => ({
153
+ ...session(`ses_arch_${index}`, 100 - index),
154
+ time: { created: 1, updated: 100 - index, archived: 9 },
155
+ }));
156
+ return new Response(JSON.stringify({
157
+ data: archivedPage,
158
+ cursor: { next: 'page-2-token' },
159
+ }), { status: 200 });
160
+ }
161
+ return new Response(JSON.stringify({
162
+ data: [session('ses_active', 10)],
163
+ cursor: { next: null },
164
+ }), { status: 200 });
165
+ },
166
+ });
167
+
168
+ runtime.enqueue(['/repo']);
169
+ await waitUntil(() => isFullyIdle(runtime));
170
+ expect(cursors[0]).toBeNull();
171
+ expect(cursors).toContain('page-2-token');
172
+ expect(cursors.some((value) => value === '[object Object]')).toBe(false);
173
+ expect(service.replaceDirectory.mock.calls[0][0].sessions).toEqual([
174
+ expect.objectContaining({ id: 'ses_active' }),
175
+ ]);
176
+ });
177
+
178
+ it('keeps prior directory rows when projectSessions fails', async () => {
179
+ const service = createService([{
180
+ directory: '/repo',
181
+ sessions: [session('ses_old', 10)],
182
+ cursor: null,
183
+ hasMore: false,
184
+ }]);
185
+ const runtime = createSessionIndexSyncRuntime({
186
+ sessionIndexService: service,
187
+ buildOpenCodeUrl: (route) => `http://opencode.test${route}`,
188
+ getOpenCodeAuthHeaders: () => ({}),
189
+ waitForOpenCodeReady: async () => true,
190
+ projectSessions: () => {
191
+ throw new Error('metadata unavailable');
192
+ },
193
+ fetchFn: async () => new Response(JSON.stringify([session('ses_new', 20)]), { status: 200 }),
194
+ });
195
+ runtime.enqueue(['/repo']);
196
+ await waitUntil(() => isFullyIdle(runtime));
197
+ // replaceDirectory must not have replaced with empty/new on failure.
198
+ expect(service.replaceDirectory).not.toHaveBeenCalled();
199
+ expect(runtime.snapshot().sync.failedDirectories).toContain('/repo');
200
+ });
201
+
202
+ it('skips Host-archived rows and pages within budget to fill active slots', async () => {
203
+ const service = createService();
204
+ const urls = [];
205
+ const runtime = createSessionIndexSyncRuntime({
206
+ sessionIndexService: service,
207
+ buildOpenCodeUrl: (route) => `http://opencode.test${route}`,
208
+ getOpenCodeAuthHeaders: () => ({}),
209
+ waitForOpenCodeReady: async () => true,
210
+ projectSessions: (sessions) => sessions.map((item) => (
211
+ String(item.id).startsWith('ses_arch')
212
+ ? { ...item, time: { ...item.time, archived: 9 } }
213
+ : item
214
+ )),
215
+ fetchFn: async (url) => {
216
+ if (url.pathname !== '/session') {
217
+ return new Response(JSON.stringify([]), { status: 200 });
218
+ }
219
+ urls.push(url.searchParams.get('cursor'));
220
+ if (!url.searchParams.get('cursor')) {
221
+ // Full page of archived rows — sync must continue with cursor.
222
+ const archivedPage = Array.from({ length: 20 }, (_, index) => session(`ses_arch_${index}`, 100 - index));
223
+ return new Response(JSON.stringify({
224
+ data: archivedPage,
225
+ cursor: 'page-2',
226
+ }), { status: 200 });
227
+ }
228
+ return new Response(JSON.stringify({
229
+ data: [session('ses_active', 10)],
230
+ cursor: null,
231
+ }), { status: 200 });
232
+ },
233
+ });
234
+
235
+ runtime.enqueue(['/repo']);
236
+ await waitUntil(() => isFullyIdle(runtime));
237
+
238
+ expect(urls[0]).toBeNull();
239
+ expect(urls).toContain('page-2');
240
+ expect(service.replaceDirectory.mock.calls[0][0].sessions).toEqual([
241
+ expect.objectContaining({ id: 'ses_active' }),
242
+ ]);
243
+ expect(service.replaceDirectory.mock.calls[0][0].sessions.some((item) => String(item.id).startsWith('ses_arch'))).toBe(false);
244
+ });
245
+
246
+ it('v2 session.list has no incremental start — always fetches a full page', async () => {
93
247
  const service = createService([{
94
248
  directory: '/repo',
95
249
  sessions: [session('ses_old', 10)],
@@ -105,7 +259,7 @@ describe('session index background sync runtime', () => {
105
259
  getOpenCodeAuthHeaders: () => ({}),
106
260
  waitForOpenCodeReady: async () => true,
107
261
  fetchFn: async (url) => {
108
- if (url.pathname === '/experimental/session') requestedUrl = url;
262
+ if (url.pathname === '/session') requestedUrl = url;
109
263
  return new Response(JSON.stringify([session('ses_new', 20)]), { status: 200 });
110
264
  },
111
265
  now: () => 2000,
@@ -114,10 +268,11 @@ describe('session index background sync runtime', () => {
114
268
  runtime.enqueue(['/repo']);
115
269
  await waitUntil(() => isFullyIdle(runtime));
116
270
 
117
- expect(requestedUrl.searchParams.get('start')).toBe('1000');
271
+ expect(requestedUrl.searchParams.get('start')).toBeNull();
272
+ expect(requestedUrl.searchParams.get('roots')).toBeNull();
118
273
  expect(service.replaceDirectory.mock.calls[0][0]).toMatchObject({
119
- fullSync: false,
120
- sessions: [expect.objectContaining({ id: 'ses_new' }), expect.objectContaining({ id: 'ses_old' })],
274
+ fullSync: true,
275
+ sessions: [expect.objectContaining({ id: 'ses_new' })],
121
276
  });
122
277
  });
123
278
 
@@ -195,7 +350,7 @@ describe('session index background sync runtime', () => {
195
350
  });
196
351
  });
197
352
 
198
- it('omits start when recovering an empty cached directory', async () => {
353
+ it('recovers an empty cached directory with a full page', async () => {
199
354
  const service = createService([{
200
355
  directory: '/repo',
201
356
  sessions: [],
@@ -211,7 +366,7 @@ describe('session index background sync runtime', () => {
211
366
  getOpenCodeAuthHeaders: () => ({}),
212
367
  waitForOpenCodeReady: async () => true,
213
368
  fetchFn: async (url) => {
214
- if (url.pathname === '/experimental/session') requestedUrl = url;
369
+ if (url.pathname === '/session') requestedUrl = url;
215
370
  return new Response(JSON.stringify([session('ses_history', 10)]), { status: 200 });
216
371
  },
217
372
  now: () => 2000,
@@ -0,0 +1,96 @@
1
+ # Session metadata store
2
+
3
+ OpenChamber session metadata whose durable copy lives on the OpenCode session
4
+ record. The side JSON file is the cache and the pre-migration source.
5
+
6
+ ## Why
7
+
8
+ Session goal progress, Host archive stamps, titles, and other Host state under
9
+ the `openchamber` namespace travel with the OpenCode session. `session.update`
10
+ (`PATCH /api/session/:id`, body `{ title?, metadata?, permissions? }`) replaces
11
+ `metadata` wholesale. Archive is `metadata.openchamber.archive.archivedAt` on
12
+ that record and is still projected onto `time.archived`. Title is the record's
13
+ `title` field and is not sent when only metadata changes.
14
+
15
+ `opencode-session-record.js` is the only module that knows that call. A client
16
+ whose types omit `session.update` metadata still goes through that module;
17
+ a missing API throws and is never an empty write.
18
+
19
+ The side file remains until each row has been copied or OpenCode has confirmed
20
+ the session is gone. Until that load succeeds, the file is not an authoritative
21
+ empty catalog: a failed read stays unwritable and does not clear other sessions.
22
+ A session OpenCode still has is listed even when the side file has no row.
23
+ One failed copy does not delete the other rows or the other OpenCode sessions.
24
+
25
+ Fork-specific fields (`goal`, `assistant`, `scheduledTask`, `llm`, `smallModel`,
26
+ and the rest of `openchamber`) are merged onto the record. They are not dropped
27
+ to match a narrower upstream metadata shape.
28
+
29
+ | OpenCode client | Session metadata update |
30
+ |---|---|
31
+ | Earlier 2.x releases used during the migration | `metadata` accepted at create time only |
32
+ | 2.0.14+ / 2.0.15 | `PATCH /api/session/:id` with body `{ title, metadata, permissions }` (`session.update`) |
33
+
34
+ ## Layout
35
+
36
+ | File | Role |
37
+ |---|---|
38
+ | `opencode-session-record.js` | Read/write title and metadata on the OpenCode session record; migrate the side file without treating a failed load as empty or dropping other sessions |
39
+ | `session-metadata-store.js` | Side-file cache and migration source: `{ [sessionID]: metadata }`, RFC 7386 merge patch, full RMW serialization, refuse-failed-read refuses write/read-as-empty. When a record reader/writer is wired, commits the merged object onto the OpenCode session first |
40
+ | `session-projection.js` | Pure Host authority projection (metadata deep-merge + archive → `time.archived`) |
41
+ | `session-archive.js` | Domain archive/unarchive: validate upstream session + directory, persist, project, best-effort index/broadcast |
42
+ | `system-session.js` | Isolation patches for system session creators: `buildScheduledTaskMetadata`, `buildLlmSessionMetadata`, `buildAssistantSessionMetadata`. Title prefixes are labels and never ownership. |
43
+ | `routes.js` | `PUT .../metadata`, `PUT .../archive`, re-exports store/archive/projection |
44
+ | `*.d.ts` | Sibling type surfaces for Extension Host (`packages/vscode` facades) — runtime stays in `.js` |
45
+ | `*.test.js` | Store + route + archive contracts |
46
+
47
+ ## Contracts
48
+
49
+ - Writes are JSON Merge Patch (RFC 7386): nested objects merge key-by-key; `null` deletes a key.
50
+ - Mutations are fully serialized (read-merge-write + persist) so concurrent writers cannot lose neighbors.
51
+ - Committed `entries` update **only after** successful persist — concurrent readers never observe uncommitted drafts. Persist failure keeps the last committed snapshot and surfaces the error.
52
+ - `mutateSessionMetadata(sessionID, decide)` runs `decide(committedMetadata)` on the same exclusive lock. Return `{ ok: false, reason? }` to reject without writing, or `{ ok: true, patch }` to merge-persist-publish. Session-goal uses this for generation/status preconditions.
53
+ - Goal-shaped metadata PUT patches advance `openchamber.goal.executionGeneration` via `withGoalExecutionGeneration` inside the mutate lock (pause/resume/condition changes).
54
+ - A failed or **corrupt** file read is **not** empty success — the store stays unwritable, keeps the file, and throws; it does not overwrite unknown disk state with `{}`.
55
+ - Entries are keyed by `sessionID` only (not directory). Runtime isolation is the Host data dir. Multiple Hosts sharing one data file are outside the current contract.
56
+ - Generic metadata patches **cannot** wipe Host archive: `openchamber.archive` is stripped from the patch and restored after merge (including null ancestor deletes). Archive writes use `setSessionMetadata(..., { allowArchive: true })`.
57
+ - Host archive authority (`metadata.openchamber.archive.archivedAt`):
58
+ - positive number → archived (`time.archived` = that value)
59
+ - `0` → explicit unarchive (clears `time.archived` even if upstream still has history)
60
+ - missing → fall back to upstream `time.archived`
61
+ - `PUT /api/openchamber/sessions/:sessionId/archive` body `{ archivedAt: number, directory?: string }` returns `{ session, index? }`. Validates upstream id + directory (`location.directory` first). Does not mutate upstream `created`/`updated`. Metadata commit is authoritative; `index: { ok: false, retryable: true }` is observable when SQLite apply fails after commit (`retryIndexUpdate` / `getIndexRepairStatus`).
62
+ - When the Host store is wired, proxy list/get return **503 retryable** on metadata read failure (never silent un-overlaid upstream). Until the store is ready, session lifecycle SSE/hub frames are suppressed; transcript/message paths stay open. Hub `replayAfter` re-projects with **current** committed metadata.
63
+ - Successful proxy `DELETE /api/session/:id` and assistant SDK delete run idempotent `forgetSession`; `session.deleted` remains compensatory. Upstream delete failure keeps metadata.
64
+ - Successful metadata `PUT` may broadcast `openchamber:session-metadata` with the **full** merged metadata object.
65
+ - Successful archive broadcasts `session.updated` with projected `info` (global UI + openchamber SSE).
66
+ - `session.deleted` events best-effort `removeSession` metadata cleanup.
67
+
68
+ ## Server wiring
69
+
70
+ Wired from `packages/web/server/index.js` + `feature-routes-runtime.js`:
71
+
72
+ 1. `createHttpSessionRecordClient` + `createSessionMetadataStore({ dataDir, recordReader, recordWriter })` + initial `load()`, then `startSideStoreMigration`
73
+ 2. proxy `getStoredSessionMetadata` / `getStoredSessionMetadataSync` → list/get + direct SSE overlay
74
+ 3. global hub `projectOutboundSessionPayload` → live + replay session lifecycle authority
75
+ 4. session-index sync `projectSessions` + event-ingest `projectSession` / `onSessionDeleted`
76
+ 5. `createSessionArchiveService` after session index exists
77
+ 6. session-goal `readSessionMetadata` / `mutateSessionMetadata` / `persistSessionGoal`
78
+ 7. `registerSessionMetadataRoutes` (metadata + archive)
79
+ 8. assistants `archiveSessionHost` → shared Host archive, which write-throughs `metadata` onto the OpenCode session record without clearing `title`
80
+ 9. system session creators persist isolation through `persistSessionMetadata`
81
+ (`setSessionMetadata` + `openchamber:session-metadata` broadcast) and then
82
+ `onSystemSessionPersisted` (index upsert that removes the row). Wired from
83
+ `index.js` into scheduled-task runtime, LLM attachment generate, and
84
+ Assistant binding create. Create-time `metadata` is merged onto the session record; it is not a second authority.
85
+
86
+ Capability is open (`supported: true`). Manual UI metadata writes, session-goal
87
+ routes, and scheduled-task goal create all persist `openchamber.goal` through
88
+ this store. After first create/resume persist, the server notifies the goal
89
+ runtime with a synthetic `session.updated` so the loop can arm.
90
+
91
+ Sidebar / session-index / notifications hide a session only when Host metadata
92
+ carries a non-empty `openchamber.assistant.assistantID` (unless
93
+ `assigned.from === 'contact'`), `openchamber.scheduledTask.taskID`,
94
+ `openchamber.smallModel.purpose`, or `openchamber.llm.purpose`. Title prefixes
95
+ are human labels and never participate. Every system session creator must
96
+ persist the matching patch through this store.