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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/README.md +13 -7
  2. package/bin/cli-opencode.test.js +208 -0
  3. package/bin/cli.js +62 -9
  4. package/bin/lib/cli-startup.js +105 -2
  5. package/bin/lib/cli-startup.test.js +179 -0
  6. package/dist/assets/{AgentAvatar-CRaBJXKs.js → AgentAvatar-B_JQoreg.js} +1 -1
  7. package/dist/assets/AssistantView-ClQAfGOT.js +3 -0
  8. package/dist/assets/{DiagramView-D1LGK_s2.js → DiagramView-BNnda8qO.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-B0kJo9Ii.js → FadeInOnReveal-VYQTSAkr.js} +1 -1
  10. package/dist/assets/{Icon-BgdfPhNs.js → Icon-BiEQEcih.js} +1 -1
  11. package/dist/assets/MarkdownRenderer-CJHSsWin.js +2 -0
  12. package/dist/assets/{MarkdownRendererImpl-DfoCJ1Mq.js → MarkdownRendererImpl-DwrHFGUp.js} +3 -3
  13. package/dist/assets/{MarkstreamRendererImpl-D2dsW5Lg.js → MarkstreamRendererImpl-CSnbQMu3.js} +1 -1
  14. package/dist/assets/MobileDetailNavigation-BXr6AGLT.js +1 -0
  15. package/dist/assets/{MobileShareBridge-2Yjn6wXg.js → MobileShareBridge-D7OE9OP_.js} +2 -2
  16. package/dist/assets/MobileSurface-D2jUkX9l.js +2 -0
  17. package/dist/assets/{MultiRunWindow-tTbMFowS.js → MultiRunWindow-DF3iIFCd.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-CD2KtlF3.js → OnboardingScreen-BjHmlwlw.js} +1 -1
  19. package/dist/assets/OverlayScrollbar-DLtWO_Gp.js +2 -0
  20. package/dist/assets/SettingsView-BRxELjow.js +25 -0
  21. package/dist/assets/{SettingsWindow-zIa76s4t.js → SettingsWindow-BI5swspb.js} +1 -1
  22. package/dist/assets/{TerminalView-CDdzF4yi.js → TerminalView-BU3x8-fr.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-DNqA4uBl.js → ToolOutputDialog-BEWhBcRd.js} +2 -2
  24. package/dist/assets/appThemeRegistry-4MR0F7Jz.js +1 -0
  25. package/dist/assets/{button-CI7Q-Zw_.js → button-DKuz3L8l.js} +1 -1
  26. package/dist/assets/collapsible-DXbDynN4.js +3 -0
  27. package/dist/assets/context-menu-CqolsLCm.js +1 -0
  28. package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-CoDkOnyS.js} +1 -1
  29. package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DDlVKyOE.js} +1 -1
  30. package/dist/assets/{diagram-DdLO8HZ9.js → diagram-D7T8HQgj.js} +1 -1
  31. package/dist/assets/{dialog-B6pnIYmp.js → dialog-DQYl3W_o.js} +1 -1
  32. package/dist/assets/es-CN3E_zXj.js +15 -0
  33. package/dist/assets/{fileMentionTouchSelection-DNeiqns-.js → fileMentionTouchSelection-BZ6ovBYH.js} +1 -1
  34. package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-JrpcsOCs.js} +1 -1
  35. package/dist/assets/fr-9zZvQZD2.js +13 -0
  36. package/dist/assets/gitApi-CM42qUed.js +10 -0
  37. package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-CIl0FL10.js} +1 -1
  38. package/dist/assets/{input-D-__fwRi.js → input-CzQy8UhV.js} +1 -1
  39. package/dist/assets/insertionBoundaries-CtkZ9720.js +1 -0
  40. package/dist/assets/ja-Dnjk_82N.js +15 -0
  41. package/dist/assets/ko-BxCEE99C.js +15 -0
  42. package/dist/assets/main-BILgBI3B.js +169 -0
  43. package/dist/assets/main-BeveiW6F.js +2 -0
  44. package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
  45. package/dist/assets/{markdownHeightCache-BhxqJOvf.js → markdownHeightCache-Cj1I5NDw.js} +1 -1
  46. package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
  47. package/dist/assets/mcpQueries-D5mQymgM.js +1 -0
  48. package/dist/assets/miniChat-BgrzM3u7.js +2 -0
  49. package/dist/assets/mobile-DtYoQE8C.js +2 -0
  50. package/dist/assets/multirun-jy8dEI96.js +4 -0
  51. package/dist/assets/{persistence-Ce3AnpEr.js → persistence-C9LQpiOS.js} +2 -2
  52. package/dist/assets/pl-CNNsWnPM.js +15 -0
  53. package/dist/assets/platform-BQghHRfp.js +6 -0
  54. package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
  55. package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +2 -0
  56. package/dist/assets/renderMobileApp-u3c4GtFC.js +28 -0
  57. package/dist/assets/{runtimeConfig-CMkwoQ_X.js → runtimeConfig-BIGBRGQI.js} +1 -1
  58. package/dist/assets/{runtimeEndpointReset-ChOxt-J7.js → runtimeEndpointReset-D2savkuw.js} +12 -12
  59. package/dist/assets/{sessionChangeSummary-NnxcqCTI.js → sessionChangeSummary-eIbpSfrO.js} +1 -1
  60. package/dist/assets/{sessionLookup-BrU3rFHJ.js → sessionLookup-myHe5ltu.js} +1 -1
  61. package/dist/assets/{sortable-tabs-strip-Fnfhf3K9.js → sortable-tabs-strip-BQtmHx9r.js} +1 -1
  62. package/dist/assets/src-CLN7Z-zx.css +2 -0
  63. package/dist/assets/store-DLCiZaA6.js +15 -0
  64. package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-BxcdiduX.js} +1 -1
  65. package/dist/assets/{textarea-bHYxZtfH.js → textarea-CakKvqFw.js} +1 -1
  66. package/dist/assets/{tooltip-0T5jv0cu.js → tooltip-Cx5gTbdY.js} +1 -1
  67. package/dist/assets/uk-rwQrxeYM.js +15 -0
  68. package/dist/assets/{url-_z7evzdK.js → url-CDHnjlsm.js} +1 -1
  69. package/dist/assets/{useAppFontEffects-C02YFyAv.js → useAppFontEffects-CNeo0aZs.js} +59 -45
  70. package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
  71. package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +1 -0
  72. package/dist/assets/useEffectiveDirectory-B4LvR8Gt.js +1 -0
  73. package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-CUnQ4TVm.js} +1 -1
  74. package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +414 -0
  75. package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
  76. package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +2 -0
  77. package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
  78. package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
  79. package/dist/assets/{useWorkerHighlightedLines-C9z0ML1z.js → useWorkerHighlightedLines-CJZbJDW3.js} +2 -2
  80. package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
  81. package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
  82. package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
  83. package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
  84. package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
  85. package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
  86. package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
  87. package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
  88. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
  89. package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
  90. package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
  91. package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
  92. package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
  93. package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
  94. package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
  95. package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
  96. package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
  97. package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
  98. package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
  99. package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
  100. package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
  101. package/dist/index.html +13 -13
  102. package/dist/mini-chat.html +13 -13
  103. package/dist/mobile.html +13 -13
  104. package/package.json +2 -2
  105. package/server/index.js +392 -20
  106. package/server/lib/assistants/DOCUMENTATION.md +26 -21
  107. package/server/lib/assistants/assign.js +2 -1
  108. package/server/lib/assistants/contact-store.js +37 -19
  109. package/server/lib/assistants/contracts.js +11 -1
  110. package/server/lib/assistants/projection-history.test.js +454 -0
  111. package/server/lib/assistants/routes.js +11 -5
  112. package/server/lib/assistants/service.js +850 -294
  113. package/server/lib/assistants/service.test.js +588 -274
  114. package/server/lib/conversations/DOCUMENTATION.md +5 -5
  115. package/server/lib/conversations/service.js +87 -57
  116. package/server/lib/conversations/service.test.js +132 -100
  117. package/server/lib/conversations/validation.js +11 -1
  118. package/server/lib/event-stream/directory-ws-bridge.js +13 -2
  119. package/server/lib/event-stream/global-hub.js +51 -5
  120. package/server/lib/event-stream/runtime.js +57 -20
  121. package/server/lib/event-stream/runtime.test.js +112 -0
  122. package/server/lib/llm/DOCUMENTATION.md +95 -84
  123. package/server/lib/llm/catalog.js +111 -45
  124. package/server/lib/llm/catalog.test.js +121 -59
  125. package/server/lib/llm/completions.js +41 -12
  126. package/server/lib/llm/completions.test.js +45 -4
  127. package/server/lib/llm/generate.js +516 -453
  128. package/server/lib/llm/generate.test.js +705 -484
  129. package/server/lib/llm/routes.js +8 -4
  130. package/server/lib/message-queue/DOCUMENTATION.md +5 -5
  131. package/server/lib/message-queue/opencode-adapter.js +191 -104
  132. package/server/lib/message-queue/opencode-adapter.test.js +328 -58
  133. package/server/lib/message-queue/runtime.js +15 -2
  134. package/server/lib/message-queue/runtime.test.js +6 -6
  135. package/server/lib/message-queue/worker.js +7 -1
  136. package/server/lib/notifications/runtime.js +5 -0
  137. package/server/lib/opencode/DOCUMENTATION.md +103 -16
  138. package/server/lib/opencode/agent-document.d.ts +28 -0
  139. package/server/lib/opencode/agent-document.js +280 -0
  140. package/server/lib/opencode/agent-document.test.js +137 -0
  141. package/server/lib/opencode/agents.js +96 -0
  142. package/server/lib/opencode/auth-state-runtime.js +5 -0
  143. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  144. package/server/lib/opencode/config-entity-routes.js +111 -102
  145. package/server/lib/opencode/config-entity-routes.test.js +123 -10
  146. package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
  147. package/server/lib/opencode/ensure-cli.js +238 -0
  148. package/server/lib/opencode/ensure-cli.test.js +119 -0
  149. package/server/lib/opencode/env-runtime.js +208 -111
  150. package/server/lib/opencode/env-runtime.test.js +211 -14
  151. package/server/lib/opencode/feature-routes-runtime.js +80 -5
  152. package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
  153. package/server/lib/opencode/http-server-connections.js +98 -0
  154. package/server/lib/opencode/http-server-connections.test.js +321 -0
  155. package/server/lib/opencode/lifecycle.js +539 -95
  156. package/server/lib/opencode/lifecycle.test.js +698 -6
  157. package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
  158. package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
  159. package/server/lib/opencode/network-runtime.js +28 -16
  160. package/server/lib/opencode/network-runtime.test.js +68 -4
  161. package/server/lib/opencode/openchamber-routes.js +7 -1
  162. package/server/lib/opencode/opencode2-pin.js +139 -0
  163. package/server/lib/opencode/opencode2-pin.test.js +88 -0
  164. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +326 -0
  165. package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
  166. package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
  167. package/server/lib/opencode/plugin-compatibility.js +15 -0
  168. package/server/lib/opencode/plugin-compatibility.test.js +21 -0
  169. package/server/lib/opencode/plugin-routes.js +3 -18
  170. package/server/lib/opencode/plugin-routes.test.js +9 -9
  171. package/server/lib/opencode/plugins.js +4 -3
  172. package/server/lib/opencode/plugins.test.js +12 -0
  173. package/server/lib/opencode/provider-catalog.js +58 -12
  174. package/server/lib/opencode/provider-catalog.test.js +48 -0
  175. package/server/lib/opencode/proxy.js +368 -28
  176. package/server/lib/opencode/proxy.test.js +338 -0
  177. package/server/lib/opencode/routes.js +303 -76
  178. package/server/lib/opencode/routes.test.js +315 -50
  179. package/server/lib/opencode/runtime-contract.js +401 -0
  180. package/server/lib/opencode/runtime-contract.test.js +214 -0
  181. package/server/lib/opencode/server-opencode-fetch.js +242 -0
  182. package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
  183. package/server/lib/opencode/server-utils-runtime.js +35 -0
  184. package/server/lib/opencode/shared-service.integration.test.js +66 -0
  185. package/server/lib/opencode/shared-service.js +147 -0
  186. package/server/lib/opencode/shared-service.test.js +93 -0
  187. package/server/lib/opencode/shared.js +3 -3
  188. package/server/lib/opencode/shutdown-runtime.js +15 -6
  189. package/server/lib/opencode/shutdown-runtime.test.js +49 -1
  190. package/server/lib/opencode/skill-routes.js +26 -40
  191. package/server/lib/opencode/skill-routes.test.js +71 -7
  192. package/server/lib/opencode/skills.test.js +15 -0
  193. package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
  194. package/server/lib/opencode/static-routes-runtime.js +3 -1
  195. package/server/lib/opencode/static-routes-runtime.test.js +33 -0
  196. package/server/lib/opencode/v1-migration-gate.js +157 -0
  197. package/server/lib/opencode/v1-migration-gate.test.js +111 -0
  198. package/server/lib/opencode/v2-client.js +60 -0
  199. package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
  200. package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
  201. package/server/lib/opencode/watcher.js +2 -1
  202. package/server/lib/opencode/watcher.test.js +1 -1
  203. package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
  204. package/server/lib/permission-auto-accept/runtime.js +14 -7
  205. package/server/lib/permission-auto-accept/runtime.test.js +27 -12
  206. package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
  207. package/server/lib/scheduled-tasks/DOCUMENTATION.md +79 -39
  208. package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
  209. package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
  210. package/server/lib/scheduled-tasks/routes.js +27 -0
  211. package/server/lib/scheduled-tasks/routes.test.js +94 -0
  212. package/server/lib/scheduled-tasks/runtime.js +381 -310
  213. package/server/lib/scheduled-tasks/runtime.test.js +600 -309
  214. package/server/lib/session-goal/DOCUMENTATION.md +173 -22
  215. package/server/lib/session-goal/capability.js +57 -0
  216. package/server/lib/session-goal/capability.test.js +33 -0
  217. package/server/lib/session-goal/routes.js +240 -2
  218. package/server/lib/session-goal/routes.test.js +211 -0
  219. package/server/lib/session-goal/runtime.js +1617 -113
  220. package/server/lib/session-goal/runtime.test.js +1500 -70
  221. package/server/lib/session-index/DOCUMENTATION.md +18 -15
  222. package/server/lib/session-index/event-ingest.js +89 -16
  223. package/server/lib/session-index/event-ingest.test.js +73 -0
  224. package/server/lib/session-index/host-authority-snapshot.js +92 -0
  225. package/server/lib/session-index/routes.host-archive.test.js +201 -0
  226. package/server/lib/session-index/routes.js +63 -10
  227. package/server/lib/session-index/routes.test.js +8 -8
  228. package/server/lib/session-index/service.js +7 -1
  229. package/server/lib/session-index/sync-runtime.js +171 -41
  230. package/server/lib/session-index/sync-runtime.test.js +164 -9
  231. package/server/lib/session-metadata/DOCUMENTATION.md +86 -0
  232. package/server/lib/session-metadata/routes.js +193 -0
  233. package/server/lib/session-metadata/routes.test.js +156 -0
  234. package/server/lib/session-metadata/session-archive.d.ts +73 -0
  235. package/server/lib/session-metadata/session-archive.js +654 -0
  236. package/server/lib/session-metadata/session-archive.test.js +359 -0
  237. package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
  238. package/server/lib/session-metadata/session-metadata-store.js +375 -0
  239. package/server/lib/session-metadata/session-metadata-store.test.js +379 -0
  240. package/server/lib/session-metadata/session-projection.d.ts +41 -0
  241. package/server/lib/session-metadata/session-projection.js +255 -0
  242. package/server/lib/session-metadata/session-projection.test.js +64 -0
  243. package/server/lib/session-metadata/system-session.js +55 -0
  244. package/server/lib/session-metadata/system-session.test.js +55 -0
  245. package/server/lib/session-title/DOCUMENTATION.md +40 -7
  246. package/server/lib/session-title/native-runtime.test.js +167 -0
  247. package/server/lib/session-title/runtime.js +67 -42
  248. package/server/lib/session-title/session-access.js +46 -0
  249. package/server/lib/session-title/session-access.test.js +91 -0
  250. package/server/lib/session-turn-pages/DOCUMENTATION.md +12 -11
  251. package/server/lib/session-turn-pages/reconcile.service.js +1 -1
  252. package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
  253. package/server/lib/session-turn-pages/routes.js +123 -75
  254. package/server/lib/session-turn-pages/routes.test.js +566 -0
  255. package/server/lib/session-turn-pages/service.js +6 -2
  256. package/server/lib/session-turn-pages/service.test.js +34 -0
  257. package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
  258. package/server/lib/session-turn-pages/session-message-projection.js +281 -0
  259. package/server/lib/small-model/DOCUMENTATION.md +101 -146
  260. package/server/lib/small-model/catalog.js +84 -54
  261. package/server/lib/small-model/catalog.test.js +111 -9
  262. package/server/lib/small-model/custom-api.js +68 -0
  263. package/server/lib/small-model/custom-api.test.js +64 -0
  264. package/server/lib/small-model/index.js +125 -241
  265. package/server/lib/small-model/resolve.js +35 -120
  266. package/server/lib/small-model/resolve.test.js +20 -110
  267. package/server/lib/small-model/routes.js +1 -3
  268. package/server/lib/small-model/summary-settings.test.js +157 -253
  269. package/server/opencode-proxy.test.js +63 -12
  270. package/dist/assets/AssistantView-BUveAgc4.js +0 -3
  271. package/dist/assets/MarkdownRenderer-BbrbDH6-.js +0 -2
  272. package/dist/assets/MobileDetailNavigation-B0aqKXd7.js +0 -1
  273. package/dist/assets/MobileSurface-DW7WfPsy.js +0 -2
  274. package/dist/assets/OverlayScrollbar-DrA0Vsg5.js +0 -2
  275. package/dist/assets/SettingsView-PkXIavr7.js +0 -25
  276. package/dist/assets/appThemeRegistry-D5Yxg4YF.js +0 -1
  277. package/dist/assets/collapsible-D5Phaykh.js +0 -1
  278. package/dist/assets/context-menu-CubSDW2g.js +0 -1
  279. package/dist/assets/es-DA3MqOo0.js +0 -15
  280. package/dist/assets/fr-Dbz0CdIA.js +0 -13
  281. package/dist/assets/gitApi-DXyC4MiV.js +0 -7
  282. package/dist/assets/insertionBoundaries-KlOVrHHt.js +0 -1
  283. package/dist/assets/ja-DumanVQA.js +0 -15
  284. package/dist/assets/ko-C0IkiUjQ.js +0 -15
  285. package/dist/assets/main-BPS5ylib.js +0 -2
  286. package/dist/assets/main-Cpd9VWYs.js +0 -169
  287. package/dist/assets/markdown-worker-CptoICmq.js +0 -2
  288. package/dist/assets/miniChat-B8_KLWiY.js +0 -2
  289. package/dist/assets/mobile-CtktKrXn.js +0 -2
  290. package/dist/assets/multirun-CYfP9Cfm.js +0 -4
  291. package/dist/assets/pl-B9No6MQp.js +0 -15
  292. package/dist/assets/platform-0w_Q9_ji.js +0 -2
  293. package/dist/assets/projectMeta-S1hJ7oBp.js +0 -3
  294. package/dist/assets/pt-BR-DyQ0WTDh.js +0 -15
  295. package/dist/assets/renderElectronMiniChatApp-iK14LWm8.js +0 -2
  296. package/dist/assets/renderMobileApp-DORLPWnb.js +0 -28
  297. package/dist/assets/src-rS3zCcsC.css +0 -2
  298. package/dist/assets/store-5B_LDnLM.js +0 -15
  299. package/dist/assets/uk-C_1KkqyP.js +0 -15
  300. package/dist/assets/useDesktopWindowControlsLayout-Dnr-jX0C.js +0 -1
  301. package/dist/assets/useEffectiveDirectory-CZLLyarc.js +0 -1
  302. package/dist/assets/useMobileNavigationStore-CoCiqNgm.js +0 -413
  303. package/dist/assets/useSessionAutoCleanup-xQyETQkG.js +0 -2
  304. package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
  305. package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
  306. package/dist/assets/zh-CN-uWILgBdt.js +0 -15
  307. package/dist/assets/zh-TW-MYfdvhbD.js +0 -15
  308. package/server/lib/small-model/call.js +0 -427
  309. package/server/lib/small-model/call.test.js +0 -382
  310. package/server/lib/small-model/opencode-session.js +0 -339
  311. package/server/lib/small-model/opencode-session.test.js +0 -177
@@ -131,14 +131,18 @@ const buildTitleSystemPrompt = () => [
131
131
  ].join('\n');
132
132
 
133
133
  const extractSessionStatus = (payload) => {
134
- if (!payload || payload.type !== 'session.status') return null;
135
- const properties = payload.properties && typeof payload.properties === 'object' ? payload.properties : {};
134
+ if (!payload) return null;
135
+ const executionType = payload.type === 'session.execution.started' ? 'busy'
136
+ : payload.type === 'session.execution.succeeded' || payload.type === 'session.execution.failed' ? 'idle'
137
+ : null;
138
+ if (payload.type !== 'session.status' && !executionType) return null;
139
+ const properties = payload.properties ?? payload.data ?? {};
136
140
  const status = properties.status && typeof properties.status === 'object' ? properties.status : {};
137
141
  const info = properties.info && typeof properties.info === 'object' ? properties.info : {};
138
142
  const sessionId = typeof properties.sessionID === 'string' ? properties.sessionID.trim() : '';
139
- const type = typeof status.type === 'string'
143
+ const type = executionType ?? (typeof status.type === 'string'
140
144
  ? status.type.trim()
141
- : (typeof info.type === 'string' ? info.type.trim() : '');
145
+ : (typeof info.type === 'string' ? info.type.trim() : ''));
142
146
  if (!sessionId || !type) return null;
143
147
  const directory = typeof properties.directory === 'string' && properties.directory
144
148
  ? properties.directory
@@ -159,9 +163,11 @@ export const isSystemOwnedSession = (sessionOrInfo) => {
159
163
  };
160
164
 
161
165
  const extractTitleRefreshRequest = (payload) => {
162
- if (!payload || payload.type !== 'session.updated') return null;
166
+ if (!payload || (payload.type !== 'session.updated' && payload.type !== 'openchamber:session-metadata')) return null;
163
167
  const properties = payload.properties && typeof payload.properties === 'object' ? payload.properties : {};
164
- const info = properties.info && typeof properties.info === 'object' ? properties.info : {};
168
+ const info = payload.type === 'openchamber:session-metadata'
169
+ ? { id: properties.sessionID, metadata: properties.metadata }
170
+ : (properties.info && typeof properties.info === 'object' ? properties.info : {});
165
171
  if (isSystemOwnedSession(info)) return null;
166
172
  const sessionId = typeof info.id === 'string' ? info.id.trim() : '';
167
173
  const titleRefresh = info.metadata?.openchamber?.titleRefresh;
@@ -176,20 +182,35 @@ const extractTitleRefreshRequest = (payload) => {
176
182
 
177
183
  const extractCreatedSession = (payload) => {
178
184
  if (!payload || payload.type !== 'session.created') return null;
179
- const properties = payload.properties && typeof payload.properties === 'object' ? payload.properties : {};
180
- const info = properties.info && typeof properties.info === 'object' ? properties.info : {};
185
+ const properties = payload.properties ?? payload.data ?? {};
186
+ const info = properties.info ?? { ...properties, id: properties.sessionID };
181
187
  const sessionId = typeof info.id === 'string' ? info.id.trim() : '';
182
188
  if (!sessionId || (typeof info.parentID === 'string' && info.parentID)) return null;
183
189
  if (isSystemOwnedSession(info)) return null;
184
190
  const directory = typeof properties.directory === 'string' && properties.directory
185
191
  ? properties.directory
186
- : (typeof info.directory === 'string' ? info.directory : '');
187
- const createdAt = typeof info.time?.created === 'number' ? info.time.created : Date.now();
192
+ : (typeof info.location?.directory === 'string' ? info.location.directory : (typeof info.directory === 'string' ? info.directory : ''));
193
+ const createdAt = typeof info.time?.created === 'number' ? info.time.created : (payload.created ?? Date.now());
188
194
  const title = typeof info.title === 'string' ? info.title : '';
189
195
  return { sessionId, directory, createdAt, title };
190
196
  };
191
197
 
192
198
  const extractUserMessage = (payload) => {
199
+ if (payload?.type === 'session.inbox.enqueued') {
200
+ const properties = payload.properties ?? payload.data ?? {};
201
+ const item = properties.item;
202
+ if (typeof properties.sessionID !== 'string' || item?.type !== 'user') return null;
203
+ const text = typeof item.payload?.text === 'string' ? item.payload.text.slice(0, TRANSCRIPT_USER_CHAR_LIMIT) : '';
204
+ return {
205
+ sessionId: properties.sessionID,
206
+ createdAt: payload.created ?? 0,
207
+ directory: payload.location?.directory ?? '',
208
+ initialRecord: text.trim() ? {
209
+ info: { id: properties.inboxID, role: 'user' },
210
+ parts: [{ type: 'text', text }],
211
+ } : null,
212
+ };
213
+ }
193
214
  if (!payload || payload.type !== 'message.updated') return null;
194
215
  const info = payload.properties?.info;
195
216
  if (!info || typeof info !== 'object' || info.role !== 'user') return null;
@@ -351,6 +372,9 @@ export const buildLatestTitleTranscript = (
351
372
  export const createSessionTitleRuntime = ({
352
373
  buildOpenCodeUrl,
353
374
  getOpenCodeAuthHeaders,
375
+ /** Ticket 11: Host write admission shared with proxy (optional). */
376
+ serverOpenCodeFetch = null,
377
+ sessionAccess = null,
354
378
  getSmallModelService,
355
379
  quietMs = TITLE_QUIET_MS,
356
380
  throttleMs = TITLE_THROTTLE_MS,
@@ -362,6 +386,7 @@ export const createSessionTitleRuntime = ({
362
386
  const forcedRefreshes = new Set();
363
387
  const initialRefreshes = new Set();
364
388
  const newSessions = new Set();
389
+ const initialUserMessages = new Map();
365
390
  const forkFirstSendPending = new Map();
366
391
  const forkFirstRefreshes = new Set();
367
392
  /** In-memory throttle timestamps (metadata also persists across restarts). */
@@ -377,6 +402,20 @@ export const createSessionTitleRuntime = ({
377
402
  };
378
403
 
379
404
  const openCodeFetch = async (pathname, { directory, method = 'GET', body } = {}) => {
405
+ if (sessionAccess) {
406
+ const sessionId = decodeURIComponent(pathname.slice('/session/'.length));
407
+ return method === 'PATCH'
408
+ ? sessionAccess.update(sessionId, directory, body)
409
+ : sessionAccess.get(sessionId, directory);
410
+ }
411
+ if (typeof serverOpenCodeFetch === 'function') {
412
+ return serverOpenCodeFetch(pathname, {
413
+ directory,
414
+ method,
415
+ body,
416
+ timeoutMs: FETCH_TIMEOUT_MS,
417
+ });
418
+ }
380
419
  const base = buildOpenCodeUrl(pathname, '');
381
420
  const url = directory ? `${base}?directory=${encodeURIComponent(directory)}` : base;
382
421
  const response = await fetch(url, {
@@ -396,6 +435,7 @@ export const createSessionTitleRuntime = ({
396
435
  };
397
436
 
398
437
  const fetchRecentMessages = async (sessionId, directory) => {
438
+ if (sessionAccess) return sessionAccess.messages(sessionId, directory, TRANSCRIPT_MESSAGE_LIMIT);
399
439
  const base = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}/message`, '');
400
440
  const params = new URLSearchParams({ limit: String(TRANSCRIPT_MESSAGE_LIMIT) });
401
441
  if (directory) params.set('directory', directory);
@@ -421,20 +461,13 @@ export const createSessionTitleRuntime = ({
421
461
 
422
462
  const meta = readTitleRefreshMeta(session);
423
463
  if (meta.titleRefresh.isGenerating !== true) return;
424
- const {
425
- isGenerating: _isGenerating,
426
- requestedAt: _requestedAt,
427
- ...titleRefresh
428
- } = meta.titleRefresh;
429
464
  await openCodeFetch(`/session/${encodeURIComponent(sessionId)}`, {
430
465
  directory,
431
466
  method: 'PATCH',
432
467
  body: {
433
468
  metadata: {
434
- ...meta.metadata,
435
469
  openchamber: {
436
- ...meta.openchamber,
437
- titleRefresh,
470
+ titleRefresh: { isGenerating: null, requestedAt: null },
438
471
  },
439
472
  },
440
473
  },
@@ -446,17 +479,13 @@ export const createSessionTitleRuntime = ({
446
479
  .catch(() => null);
447
480
  if (!session || typeof session !== 'object') return;
448
481
 
449
- const meta = readTitleRefreshMeta(session);
450
482
  await openCodeFetch(`/session/${encodeURIComponent(sessionId)}`, {
451
483
  directory,
452
484
  method: 'PATCH',
453
485
  body: {
454
486
  metadata: {
455
- ...meta.metadata,
456
487
  openchamber: {
457
- ...meta.openchamber,
458
488
  titleRefresh: {
459
- ...meta.titleRefresh,
460
489
  lastError: error instanceof Error ? error.message : String(error),
461
490
  failedAt: now(),
462
491
  },
@@ -505,11 +534,8 @@ export const createSessionTitleRuntime = ({
505
534
  method: 'PATCH',
506
535
  body: {
507
536
  metadata: {
508
- ...meta.metadata,
509
537
  openchamber: {
510
- ...meta.openchamber,
511
538
  titleRefresh: {
512
- ...meta.titleRefresh,
513
539
  activityUpdatedAt,
514
540
  },
515
541
  },
@@ -524,6 +550,8 @@ export const createSessionTitleRuntime = ({
524
550
  const generateTitle = async (sessionId, directory) => {
525
551
  const forceRefresh = forcedRefreshes.delete(sessionId);
526
552
  const initialRefresh = initialRefreshes.delete(sessionId);
553
+ const initialUserMessage = initialUserMessages.get(sessionId);
554
+ initialUserMessages.delete(sessionId);
527
555
  const forkFirstRefresh = forkFirstRefreshes.delete(sessionId);
528
556
  if (!forceRefresh && !isTitleRefreshEnabled()) return;
529
557
 
@@ -558,15 +586,14 @@ export const createSessionTitleRuntime = ({
558
586
  return;
559
587
  }
560
588
 
561
- const messages = await fetchRecentMessages(sessionId, directory);
589
+ const messages = initialUserMessage ? [initialUserMessage] : await fetchRecentMessages(sessionId, directory);
562
590
  if (!messages || messages.length === 0) return;
563
591
 
564
592
  const { transcript, lastAssistantId, realUserCount, languageSample } = buildLatestTitleTranscript(messages);
565
593
  if (!transcript) return;
566
594
 
567
- // Let OpenCode's first-message title land first. We only refresh once the
568
- // conversation has moved on (2+ real user turns), unless the title is
569
- // still the default placeholder.
595
+ // Initial titles use the admitted first user message immediately; ordinary
596
+ // later refreshes still require a moved-on conversation or placeholder.
570
597
  if (!forceRefresh && !initialRefresh && !forkFirstRefresh && realUserCount < 2 && !isDefaultSessionTitle(currentTitle)) {
571
598
  return;
572
599
  }
@@ -597,14 +624,11 @@ export const createSessionTitleRuntime = ({
597
624
  method: 'PATCH',
598
625
  body: {
599
626
  metadata: {
600
- ...meta.metadata,
601
627
  openchamber: {
602
- ...meta.openchamber,
603
628
  titleRefresh: {
604
- ...meta.titleRefresh,
605
629
  isGenerating: true,
606
- lastError: undefined,
607
- failedAt: undefined,
630
+ lastError: null,
631
+ failedAt: null,
608
632
  },
609
633
  },
610
634
  },
@@ -677,9 +701,6 @@ export const createSessionTitleRuntime = ({
677
701
  const generatedAt = now();
678
702
  lastGeneratedAtBySession.set(sessionId, generatedAt);
679
703
 
680
- const currentMetadata = freshMeta.metadata;
681
- const currentNamespace = freshMeta.openchamber;
682
-
683
704
  console.log(`[session-title] refreshed ${sessionId} → "${nextTitle}" via ${generated.providerID}/${generated.modelID}`);
684
705
  await openCodeFetch(`/session/${encodeURIComponent(sessionId)}`, {
685
706
  directory,
@@ -687,11 +708,8 @@ export const createSessionTitleRuntime = ({
687
708
  body: {
688
709
  title: nextTitle,
689
710
  metadata: {
690
- ...currentMetadata,
691
711
  openchamber: {
692
- ...currentNamespace,
693
712
  titleRefresh: {
694
- ...freshMeta.titleRefresh,
695
713
  lastAutoTitle: nextTitle,
696
714
  forMessageID: lastAssistantId || latestAssistantId || '',
697
715
  generatedAt,
@@ -723,6 +741,7 @@ export const createSessionTitleRuntime = ({
723
741
 
724
742
  const processPayload = (payload, directoryHint = '') => {
725
743
  if (stopped) return;
744
+ directoryHint = directoryHint || payload?.location?.directory || '';
726
745
  const createdSession = extractCreatedSession(payload);
727
746
  if (createdSession) {
728
747
  if (isForkedSessionTitle(createdSession.title)) {
@@ -758,13 +777,18 @@ export const createSessionTitleRuntime = ({
758
777
  initialRefreshes.add(status.sessionId);
759
778
  armTimer(status.sessionId, status.directory || directoryHint, 0);
760
779
  }
761
- } else {
780
+ } else if (!initialRefreshes.has(status.sessionId)) {
762
781
  clearTimer(status.sessionId);
763
782
  }
764
783
  return;
765
784
  }
766
785
  const userMessage = extractUserMessage(payload);
767
786
  if (userMessage) {
787
+ if (userMessage.initialRecord && newSessions.delete(userMessage.sessionId)) {
788
+ initialRefreshes.add(userMessage.sessionId);
789
+ initialUserMessages.set(userMessage.sessionId, userMessage.initialRecord);
790
+ armTimer(userMessage.sessionId, userMessage.directory || directoryHint, 0);
791
+ }
768
792
  const pendingFork = forkFirstSendPending.get(userMessage.sessionId);
769
793
  if (pendingFork && userMessage.createdAt > pendingFork.createdAt) {
770
794
  pendingFork.hasNewUserMessage = true;
@@ -780,7 +804,7 @@ export const createSessionTitleRuntime = ({
780
804
  // session settles. Only a message created after the timer was armed
781
805
  // means the user actually moved on.
782
806
  const armed = timers.get(userMessage.sessionId);
783
- if (armed && userMessage.createdAt >= armed.armedAt) {
807
+ if (armed && !initialRefreshes.has(userMessage.sessionId) && userMessage.createdAt >= armed.armedAt) {
784
808
  clearTimer(userMessage.sessionId);
785
809
  }
786
810
  }
@@ -792,6 +816,7 @@ export const createSessionTitleRuntime = ({
792
816
  clearTimeout(timer);
793
817
  }
794
818
  timers.clear();
819
+ initialUserMessages.clear();
795
820
  };
796
821
 
797
822
  return { processPayload, stop, armTimer };
@@ -0,0 +1,46 @@
1
+ import { makeOpenCodeV2Client } from '../opencode/v2-client.js';
2
+ import { projectSessionWithHostMetadata } from '../session-metadata/session-projection.js';
3
+ import { projectSessionMessageRecords } from '../session-turn-pages/session-message-projection.js';
4
+
5
+ /** Title reads use OpenCode; title-refresh metadata and its notifications belong to the Host. */
6
+ export function createTitleSessionAccess({
7
+ buildOpenCodeUrl,
8
+ getOpenCodeAuthHeaders,
9
+ readSessionMetadata,
10
+ persistSessionMetadata,
11
+ publishSession,
12
+ }) {
13
+ const client = () => makeOpenCodeV2Client({
14
+ baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
15
+ authHeaders: getOpenCodeAuthHeaders(),
16
+ });
17
+ const options = () => ({ signal: AbortSignal.timeout(30_000) });
18
+ const get = async (sessionID) => {
19
+ const session = await client().session.get({ sessionID }, options());
20
+ return projectSessionWithHostMetadata(session, await readSessionMetadata(sessionID));
21
+ };
22
+ return {
23
+ get,
24
+ async messages(sessionID, _directory, limit) {
25
+ const page = await client().message.list({ sessionID, limit, order: 'desc' }, options());
26
+ if (!Array.isArray(page?.data)) throw new Error('Session title: invalid message page');
27
+ return projectSessionMessageRecords([...page.data].reverse());
28
+ },
29
+ async update(sessionID, directory, patch) {
30
+ if (typeof patch.title === 'string') {
31
+ await client().session.update({ sessionID, title: patch.title }, options());
32
+ }
33
+ // Never write the complete metadata snapshot back: goal/archive/ownership
34
+ // may have changed while the title model was running.
35
+ const refresh = patch.metadata?.openchamber?.titleRefresh;
36
+ if (refresh) {
37
+ await persistSessionMetadata(sessionID, { openchamber: { titleRefresh: refresh } });
38
+ }
39
+ if (typeof patch.title === 'string' || (refresh && Object.hasOwn(refresh, 'isGenerating'))) {
40
+ const session = await get(sessionID);
41
+ await publishSession(session, directory);
42
+ return session;
43
+ }
44
+ },
45
+ };
46
+ }
@@ -0,0 +1,91 @@
1
+ import { beforeEach, expect, it, vi } from 'vitest';
2
+ import { mergeMetadataPatch } from '../session-metadata/session-metadata-store.js';
3
+
4
+ const { api } = vi.hoisted(() => ({ api: {
5
+ session: { get: vi.fn(), update: vi.fn() },
6
+ message: { list: vi.fn() },
7
+ } }));
8
+ vi.mock('../opencode/v2-client.js', () => ({ makeOpenCodeV2Client: () => api }));
9
+ import { createTitleSessionAccess } from './session-access.js';
10
+
11
+ beforeEach(() => vi.resetAllMocks());
12
+
13
+ function fixture() {
14
+ let metadata = { openchamber: {
15
+ goal: { status: 'active' },
16
+ titleRefresh: { isGenerating: true, requestedAt: 1000, lastError: 'old failure', failedAt: 1 },
17
+ } };
18
+ api.session.get.mockResolvedValue({
19
+ id: 'ses_title', title: 'Updated title', location: { directory: '/repo' },
20
+ time: { created: 10, updated: 20 },
21
+ });
22
+ const persist = vi.fn(async (_id, patch) => { metadata = mergeMetadataPatch(metadata, patch); });
23
+ const publish = vi.fn();
24
+ const access = createTitleSessionAccess({
25
+ buildOpenCodeUrl: (path) => `http://opencode${path}`,
26
+ getOpenCodeAuthHeaders: () => ({}),
27
+ readSessionMetadata: async () => metadata,
28
+ persistSessionMetadata: persist,
29
+ publishSession: publish,
30
+ });
31
+ return { access, persist, publish, metadata: () => metadata };
32
+ }
33
+
34
+ it('publishes the full authoritative title row and clears Host loading without overwriting unrelated state', async () => {
35
+ const f = fixture();
36
+ await f.access.update('ses_title', '/repo', {
37
+ title: 'Updated title',
38
+ metadata: { openchamber: { goal: { status: 'stale' }, titleRefresh: {
39
+ lastAutoTitle: 'Updated title', isGenerating: null, requestedAt: null, lastError: null, failedAt: null,
40
+ } } },
41
+ });
42
+ expect(api.session.update.mock.calls[0][0]).toEqual({ sessionID: 'ses_title', title: 'Updated title' });
43
+ expect(f.metadata()).toEqual({ openchamber: {
44
+ goal: { status: 'active' }, titleRefresh: { lastAutoTitle: 'Updated title' },
45
+ } });
46
+ expect(f.publish).toHaveBeenCalledWith(expect.objectContaining({
47
+ id: 'ses_title', title: 'Updated title', directory: '/repo', metadata: f.metadata(),
48
+ }), '/repo');
49
+ });
50
+
51
+ it('projects only the bounded newest message page in conversation order', async () => {
52
+ const f = fixture();
53
+ api.message.list.mockResolvedValue({ data: [
54
+ { id: 'a1', type: 'assistant', content: [{ type: 'text', text: 'Finished' }] },
55
+ { id: 'u1', type: 'user', text: 'Implement title refresh' },
56
+ ] });
57
+ const messages = await f.access.messages('ses_title', '/repo', 16);
58
+ expect(api.message.list.mock.calls[0][0]).toEqual({ sessionID: 'ses_title', limit: 16, order: 'desc' });
59
+ expect(messages.map((row) => row.info.id)).toEqual(['u1', 'a1']);
60
+ expect(messages[0].parts[0].text).toBe('Implement title refresh');
61
+ expect(messages[1].parts[0].text).toBe('Finished');
62
+ });
63
+
64
+ it('publishes a full loading row before the new session exists in client catalogs', async () => {
65
+ const f = fixture();
66
+ await f.access.update('ses_title', '/repo', { metadata: { openchamber: { titleRefresh: { isGenerating: true } } } });
67
+ expect(f.publish).toHaveBeenCalledWith(expect.objectContaining({
68
+ id: 'ses_title', directory: '/repo', metadata: expect.objectContaining({
69
+ openchamber: expect.objectContaining({ titleRefresh: expect.objectContaining({ isGenerating: true }) }),
70
+ }),
71
+ }), '/repo');
72
+ expect(api.session.update).not.toHaveBeenCalled();
73
+ });
74
+
75
+ it('does not publish success on a failed title write', async () => {
76
+ const f = fixture();
77
+ api.session.update.mockRejectedValue(new Error('offline'));
78
+ await expect(f.access.update('ses_title', '/repo', {
79
+ title: 'Failed', metadata: { openchamber: { titleRefresh: { lastAutoTitle: 'Failed' } } },
80
+ })).rejects.toThrow('offline');
81
+ expect(f.persist).not.toHaveBeenCalled();
82
+ expect(f.publish).not.toHaveBeenCalled();
83
+ });
84
+
85
+ it('does not turn a failed or malformed message read into empty success', async () => {
86
+ const f = fixture();
87
+ api.message.list.mockRejectedValueOnce(new Error('offline'));
88
+ await expect(f.access.messages('ses_title', '/repo', 16)).rejects.toThrow('offline');
89
+ api.message.list.mockResolvedValueOnce({});
90
+ await expect(f.access.messages('ses_title', '/repo', 16)).rejects.toThrow('invalid message page');
91
+ });
@@ -4,7 +4,7 @@
4
4
 
5
5
  OpenChamber-owned turn-window pagination and anchor reconcile for session messages.
6
6
 
7
- 1. **Turn pages** — clients request the last N **authored user turns** (default 3) rather than a raw message count. The host loops official OpenCode `session.messages` pages (`limit` / `before` / `x-next-cursor`) until the turn budget is met or history is exhausted.
7
+ 1. **Turn pages** — clients request the last N **authored user turns** (default 3) rather than a raw message count. The host loops OpenCode v2 `message.list` pages (first page `limit` + `order=desc`; continuation `limit` + `cursor` from the previous body `cursor.next` — upstream rejects `order` together with `cursor` with 400) until the turn budget is met or history is exhausted.
8
8
  2. **Anchor reconcile** — after SSE reconnect, clients request the gap from a stable turn-boundary anchor to the current head. The host scans newest→older, returns chronological gap records (including the anchor's overlap turn), and paginates via host-owned continuation tokens with page/byte/total budgets.
9
9
 
10
10
  Exposed as:
@@ -16,7 +16,7 @@ Exposed as:
16
16
 
17
17
  ## Why this exists
18
18
 
19
- Official OpenCode pagination is message-count based and uses **opaque** cursors (not message ids). Each upstream page is chronological **old→new** (including the latest slice). UI prepend/load-more needs a stable turn boundary so synthetic loop messages, subtasks, and compaction rows do not consume the turn budget. This module aggregates upstream pages on the host, returns chronological records, and exposes a continuous **host-owned opaque cursor** that points just before the earliest returned authored user.
19
+ Official OpenCode pagination is message-count based and uses **opaque** cursors (not message ids). v2 pages arrive **newest-first**; `fetchPage` reverses each page to chronological **old→new** before aggregation. UI prepend/load-more needs a stable turn boundary so synthetic loop messages, subtasks, and compaction rows do not consume the turn budget. This module aggregates upstream pages on the host, returns chronological records, and exposes a continuous **host-owned opaque cursor** that points just before the earliest returned authored user.
20
20
 
21
21
  ## Scope
22
22
 
@@ -34,7 +34,7 @@ Official OpenCode pagination is message-count based and uses **opaque** cursors
34
34
  | `scanLimit` | **omitted** | `10..200` when present | Optional override only. Host→OpenCode local page size; when omitted it resolves to `_inner_scanLimit`. Clients should omit this. |
35
35
  | `before` | omitted | opaque string ≤ 8192 chars | Resume older history: host cursor (`oc1.…`) or raw OpenCode SDK cursor on first page |
36
36
  | `directory` | omitted | path string | Forwarded to directory-scoped OpenCode client |
37
- | `includeReasoning` | omitted (include) | strict `'false'` only disables | OpenChamber projection control. When `'false'`, drop `type=reasoning` parts from turn-page / reconcile / exact responses after other projections. Missing or any other value keeps existing behavior (slim reasoning identity on turn pages; full parts on reconcile/exact). **Never forwarded to OpenCode.** Same contract on transcript-cache reads, official `session.messages` list proxy, and event SSE/WS send boundaries — see `event-stream/reasoning-projection.js`. |
37
+ | `includeReasoning` | omitted (include) | strict `'false'` only disables | OpenChamber projection control. When `'false'`, drop `type=reasoning` parts from turn-page / reconcile / exact responses. Host order is **normalize native SessionMessageInfo → slim/L1 bounds → reasoning strip** so `content`/`text` carriers cannot rebuild filtered parts or leak through `info` expansion. Missing or any other value keeps existing behavior (slim reasoning identity on turn pages; full parts on reconcile/exact). **Never forwarded to OpenCode.** Same contract on transcript-cache reads, official `session.messages` list proxy, and event SSE/WS send boundaries — see `event-stream/reasoning-projection.js`. |
38
38
 
39
39
  Invalid `turns`, explicit invalid `scanLimit`, or `before` (malformed/stale host token, oversize) → HTTP 400.
40
40
 
@@ -66,7 +66,7 @@ Success `200`:
66
66
  }
67
67
  ```
68
68
 
69
- - `records`: chronological oldest → newest, including intermediate non-boundary rows (synthetic, tools, subtask, compaction) inside the window.
69
+ - `records`: chronological oldest → newest, including intermediate non-boundary rows (synthetic, tools, subtask, compaction) inside the window. Host projection drops v2 **control** SessionMessage rows (`idle`, `model-switched`, `agent-selected`, `agent-switched`, `location-switched`) so they never reach ChatMessage as empty Assistant headers. Native assistants project `content[]` once (never also top-level `text` when `content` is present). Wire `model.id` becomes domain `modelID`; `finish`/`error` on failed assistants (e.g. `provider.auth` 401) are preserved.
70
70
  - `turnCount`: count of authored user boundaries in `records`.
71
71
  - `cursor`: host-owned opaque token for the next `before=` request; `null` when `complete` is true. The token represents the position **just before** the earliest returned authored user (so the client can load older history without overlap).
72
72
  - `complete`: `upstreamComplete && selected.length === accumulated.length`. True when upstream history is exhausted **and** `selectTurnRecords` did not trim any older scanned rows (nothing left for the client). When the scan window held more than N turns and older rows were trimmed, or when upstream stopped on a full page with a missing `nextCursor` (suspicious truncation), `complete` stays `false` with a `cursor` so the client can fetch / retry.
@@ -82,7 +82,7 @@ Every turn-page response summarizes `tool`, `reasoning`, and `file` parts plus m
82
82
  |---|---|---|
83
83
  | **L1** | Turn pages, reconcile response pages, message/session SSE fan-out, exact `GET /api/session/:sessionID/message/:messageID` | Any `info.summary.diffs` **array** is projected to a **slim file list** `[{ file, status?, additions, deletions }]` plus additive markers `summary.diffCount: number` and `summary.hasDiffs: boolean`. Patch/before/after/from/to and other heavy bodies are stripped. Other lightweight summary fields are preserved. `diffCount` equals the slim array length; `hasDiffs === diffCount > 0`. Consumers may use either the slim array or the markers. |
84
84
  | **L2** | `GET /api/openchamber/sessions/:sessionID/changes?messageID=` (no `file`) | Host reads official `session.message` locally, returns only `{ files: [{ file, status?, additions, deletions }] }`. Never returns patch/before/after/from/to or the full message envelope. |
85
- | **L3** | Same route with `file=` | Host calls official `session.diff({ sessionID, messageID, directory })`, returns `{ diff: SnapshotFileDiff }` for the exact `file` string match (includes that file's patch). No match → HTTP 404 `{ error: 'change file not found', code: 'change_not_found' }`. Older hosts may ignore `file=` and answer 200 `{ files }` (L2 only). Current clients treat a 200 without `{ diff }` as missing L3 and fall back to official `session.diff`. |
85
+ | **L3** | Same route with `file=` | Host currently still reads `info.summary.diffs` from the exact message payload when present. **v2 `SessionMessageInfo` has no `summary.diffs`**, so L3 fails closed with upstream 502 until a parent follow-up lands an authoritative per-message diff source. Do **not** substitute working-tree / `vcs.diff` here. When diffs are available, returns `{ diff: SnapshotFileDiff }` for the exact `file` string match. No match → HTTP 404 `{ error: 'change file not found', code: 'change_not_found' }`. Older hosts may ignore `file=` and answer 200 `{ files }` (L2 only). |
86
86
 
87
87
  Exact message GET keeps the official `{ info, parts }` response shape and only applies L1 summary projection so materialize/recovery never relays raw multi-megabyte patch bodies through the generic proxy. Abort/45s timeout semantics match the turn-page routes. Global `/api` auth still applies.
88
88
 
@@ -126,7 +126,7 @@ On resume:
126
126
  1. Strict decode / shape / length validation of the host token.
127
127
  2. First `fetchPage` uses the token’s raw upstream `before`.
128
128
  3. Locate `boundaryID` in that page; keep only records strictly older than it (`slice(0, index)` in old→new order). Boundary and newer rows are excluded.
129
- 4. If fewer than N turns remain, continue with that raw page’s `x-next-cursor` for older pages.
129
+ 4. If fewer than N turns remain, continue with that raw page’s `cursor.next` for older pages.
130
130
  5. Each accumulated record records its upstream page’s request-before origin; when `complete=false`, the next host cursor encodes the earliest selected authored user’s origin + id.
131
131
 
132
132
  A raw OpenCode SDK cursor (no `oc1.` prefix) is still accepted as the first `before` and is passed through unchanged to `fetchPage`.
@@ -146,14 +146,15 @@ A message is an authored user turn boundary when:
146
146
 
147
147
  ## Aggregation rules
148
148
 
149
- - Upstream pages are chronological **old→new** within each page; **do not reverse**. Older pages are **prepended** while deduping by `info.id` (global order remains old→new).
150
- - Continues paging with raw upstream `before` until `turns` authored boundaries are collected or upstream reports no next cursor.
151
- - **Suspicious truncation:** an upstream page whose `records.length >= scanLimit` (full page) but `nextCursor == null` is treated as a non-authoritative cut (observed OpenCode `x-next-cursor` race), not as end-of-history. The host stops scanning that round with `upstreamComplete=false`, so the response stays `complete=false` and still emits a boundary host cursor the client can retry. A short page (`records.length < scanLimit`) with no cursor, an empty page with no cursor, or an explicit `page.complete === true` remain authoritative exhaustion. Client no-growth handling still bounds retries if the upstream view stays truncated.
149
+ - `fetchPage` returns each page chronological **old→new** (v2 `data` is newest-first and is reversed there). Older pages are **prepended** while deduping by `info.id` (global order remains old→new).
150
+ - Continues paging with the raw upstream `cursor.next` (sent as `cursor`, never with `order`) until `turns` authored boundaries are collected or upstream reports no next cursor.
151
+ - **Suspicious truncation:** an upstream page whose `records.length >= scanLimit` (full page) but `nextCursor == null` is treated as a non-authoritative cut (observed OpenCode next-cursor race), not as end-of-history. The host stops scanning that round with `upstreamComplete=false`, so the response stays `complete=false` and still emits a boundary host cursor the client can retry. A short page (`records.length < scanLimit`) with no cursor, an empty page with no cursor, or an explicit `page.complete === true` remain authoritative exhaustion. Client no-growth handling still bounds retries if the upstream view stays truncated.
152
152
  - Every upstream record must have a non-empty `info.id`; otherwise structured error, no partial `records`, HTTP 502.
153
153
  - Hard scan caps (no partial success): **50 pages** / **5000 messages** → structured error, HTTP 413.
154
154
  - Repeated / stalled cursor, or empty page that still carries a next cursor → structured error, no partial `records`, HTTP 502.
155
155
  - Malformed upstream payloads and SDK/transport failures → HTTP 502 (`error: "upstream"`).
156
- - `AbortSignal` is forwarded to each `session.messages` call; route also applies a 45s linked timeout.
156
+ - Native v2 `SessionMessageInfo` rows are projected to the external `{ info, parts }` record contract by `session-message-projection.js` (control rows such as `idle` / `model-switched` are dropped). The VS Code turn-page bridge imports the same module so both runtimes emit identical records.
157
+ - `AbortSignal` is forwarded to each `message.list` call; route also applies a 45s linked timeout.
157
158
  - Request abort only when the client disconnects mid-flight: `req` aborted, or `res` `close` while `!res.writableEnded`. A normal GET request `close` after the response has ended must not abort.
158
159
 
159
160
  ## Anchor reconcile API
@@ -263,7 +264,7 @@ When the per-page budget fills before the gap is complete, the host returns `con
263
264
  - `service.js` — `isUserAuthoredTurnBoundary`, `selectTurnRecords`, `encodeHostCursor`, `decodeHostCursor`, `projectMessageDiffSummaries` (L1), `projectSlimParts`, `createSessionTurnPageService({ fetchPage, maxScanPages?, maxScanMessages? })`
264
265
  - `reconcile.service.js` — `encodeReconcileContinuation`, `decodeReconcileContinuation`, `createSessionReconcileService({ fetchPage, runtimeKey?, page/total budgets?, continuationSecret?, clock?, continuationTtlMs? })`
265
266
  - `changes.service.js` — `createSessionChangesService({ fetchMessage, fetchDiff })` for L2/L3; injectable for unit tests
266
- - `routes.js` — Express registration for turn-page, reconcile, Changes, and exact message GET; optional injected `sessionTurnPageService` / `sessionReconcileService` / `sessionChangesService` / `fetchExactMessage` for tests; default wires SDK `session.messages` / `session.message` / `session.diff` via `buildOpenCodeUrl` + `getOpenCodeAuthHeaders`
267
+ - `routes.js` — Express registration for turn-page, reconcile, Changes, and exact message GET; optional injected `sessionTurnPageService` / `sessionReconcileService` / `sessionChangesService` / `fetchExactMessage` for tests; default wires `@opencode/client` `OpenCode.make` (`message.list` / `session.message`) via `buildOpenCodeUrl` + `getOpenCodeAuthHeaders`. Exact/not-found classification honors real client `_tag` (`MessageNotFoundError` / `SessionNotFoundError`) and preserves abort → HTTP 499 (never folds abort into 502).
267
268
  - `service.test.js` / `routes.test.js` / `reconcile.service.test.js` / `reconcile.routes.test.js` / `changes.service.test.js` — unit contracts
268
269
 
269
270
  ## Registration
@@ -47,7 +47,7 @@ const PROCESS_CONTINUATION_SECRET = randomBytes(32);
47
47
  const fail = (error) => ({ ok: false, error });
48
48
 
49
49
  const recordId = (record) => {
50
- const id = record?.info?.id;
50
+ const id = record?.info?.id ?? record?.id;
51
51
  return typeof id === 'string' && id.length > 0 ? id : null;
52
52
  };
53
53
 
@@ -686,4 +686,20 @@ describe('createSessionReconcileService', () => {
686
686
  expect(result.ok).toBe(true);
687
687
  expect(result.resetRequired).toBe(true);
688
688
  });
689
+
690
+ it('locates a SessionMessageInfo anchor by top-level id', async () => {
691
+ const fetchPage = vi.fn(async () => pageResult([
692
+ { id: 'msg_u1', type: 'user', time: { created: 1 }, text: 'hi' },
693
+ { id: 'msg_a1', type: 'assistant', time: { created: 2, completed: 3 }, content: [{ type: 'text', text: 'ok' }] },
694
+ ], null));
695
+ const service = createSessionReconcileService(serviceOptions(fetchPage));
696
+ const result = await service.reconcile({
697
+ sessionID: 'ses_1',
698
+ directory: '/repo',
699
+ anchor: 'msg_u1',
700
+ });
701
+ expect(result.ok).toBe(true);
702
+ expect(result.anchorFound).toBe(true);
703
+ expect(result.records.map((entry) => entry.id)).toEqual(['msg_u1', 'msg_a1']);
704
+ });
689
705
  });