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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/README.md +15 -11
  2. package/bin/cli-opencode.test.js +4 -4
  3. package/bin/cli.js +1 -1
  4. package/bin/lib/cli-startup.js +1 -1
  5. package/bin/lib/cli-startup.test.js +18 -4
  6. package/dist/assets/{AgentAvatar-CLWWNu_Q.js → AgentAvatar-Q1SmFRx2.js} +1 -1
  7. package/dist/assets/AssistantView-CwXkiCAd.js +3 -0
  8. package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-BW92schO.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-BZmwl-zE.js} +1 -1
  10. package/dist/assets/{Icon-DD-BuMfZ.js → Icon-AQPvxMiU.js} +1 -1
  11. package/dist/assets/{MarkdownRenderer-bc_p1H7l.js → MarkdownRenderer-Bv05iDZZ.js} +2 -2
  12. package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-C81xqYib.js} +3 -3
  13. package/dist/assets/MarkstreamRendererImpl-CXUCQuKJ.js +3 -0
  14. package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-BMgIu7zc.js} +1 -1
  15. package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-BOXGNHmC.js} +2 -2
  16. package/dist/assets/MobileSurface-xuc_z0_G.js +2 -0
  17. package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-B5At2Z95.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-bsPLgz4r.js} +1 -1
  19. package/dist/assets/OverlayScrollbar-4wp4migh.js +2 -0
  20. package/dist/assets/SettingsView-CJ6tcH5_.js +25 -0
  21. package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-CMtX2bMl.js} +1 -1
  22. package/dist/assets/{TerminalView-Dh9MWCpS.js → TerminalView-Dvr-ysTS.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-uH1ZZDSS.js} +2 -2
  24. package/dist/assets/appThemeRegistry-DK7q2juD.js +1 -0
  25. package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
  26. package/dist/assets/{button-D-rATN7w.js → button-CcNJUxUd.js} +1 -1
  27. package/dist/assets/collapsible-ZmZ6rLWs.js +3 -0
  28. package/dist/assets/context-menu-B2LXJgXL.js +1 -0
  29. package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-C6HrP0jq.js} +1 -1
  30. package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DdVBp45U.js} +1 -1
  31. package/dist/assets/{diagram-DQf7Y_uR.js → diagram-CxTs9hCQ.js} +1 -1
  32. package/dist/assets/{dialog-bfbvR4VH.js → dialog-KMVL9QKF.js} +1 -1
  33. package/dist/assets/es-CN3E_zXj.js +15 -0
  34. package/dist/assets/{fileMentionTouchSelection-BvrNCroG.js → fileMentionTouchSelection-7RIwlpkB.js} +1 -1
  35. package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-CjdBRy8y.js} +1 -1
  36. package/dist/assets/fr-9zZvQZD2.js +13 -0
  37. package/dist/assets/gitApi-yuWg9BN4.js +10 -0
  38. package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-9vQBq3zp.js} +1 -1
  39. package/dist/assets/{input-8r6F_Fza.js → input-CrIvk3HZ.js} +1 -1
  40. package/dist/assets/insertionBoundaries-BiUCSkx-.js +1 -0
  41. package/dist/assets/ja-Dnjk_82N.js +15 -0
  42. package/dist/assets/ko-BxCEE99C.js +15 -0
  43. package/dist/assets/{main-CCE_4y5F.js → main-C9mIBo77.js} +2 -2
  44. package/dist/assets/main-DiL6w6KF.js +169 -0
  45. package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
  46. package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-BmXVY_gc.js} +1 -1
  47. package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
  48. package/dist/assets/mcpQueries-C7jknHV8.js +1 -0
  49. package/dist/assets/miniChat-CwtUaElU.js +2 -0
  50. package/dist/assets/mobile-DB-6--ws.js +2 -0
  51. package/dist/assets/multirun-TAsxFI8e.js +4 -0
  52. package/dist/assets/{persistence-Ce3AnpEr.js → persistence-56kq8Z4V.js} +2 -2
  53. package/dist/assets/pl-CNNsWnPM.js +15 -0
  54. package/dist/assets/platform-C13fMVEs.js +6 -0
  55. package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
  56. package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
  57. package/dist/assets/renderElectronMiniChatApp-CGj0MNRU.js +2 -0
  58. package/dist/assets/renderMobileApp-CGMMqbsO.js +28 -0
  59. package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
  60. package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-BEANkvOd.js} +1 -1
  61. package/dist/assets/runtimeEndpointReset-A_ZBUJFS.js +147 -0
  62. package/dist/assets/sessionChangeSummary-qOLrP9fw.js +2 -0
  63. package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-34XFmP9C.js} +1 -1
  64. package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-DyEV0Heg.js} +1 -1
  65. package/dist/assets/src-ubCWXZhR.css +2 -0
  66. package/dist/assets/store-DLCiZaA6.js +15 -0
  67. package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-CXaBEoa_.js} +1 -1
  68. package/dist/assets/textarea-BBOlRI9O.js +1 -0
  69. package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-CPI4fr1a.js} +1 -1
  70. package/dist/assets/uk-rwQrxeYM.js +15 -0
  71. package/dist/assets/{url-_z7evzdK.js → url-DRDQXjyX.js} +1 -1
  72. package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-CiDe42I5.js} +57 -43
  73. package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
  74. package/dist/assets/useDesktopWindowControlsLayout-BlizAlOt.js +1 -0
  75. package/dist/assets/useEffectiveDirectory-C_CouL5v.js +1 -0
  76. package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-B0oUZAiB.js} +1 -1
  77. package/dist/assets/useMobileNavigationStore-DfPClaW-.js +414 -0
  78. package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
  79. package/dist/assets/useSessionAutoCleanup-DtrfbxAp.js +2 -0
  80. package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
  81. package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
  82. package/dist/assets/{useWorkerHighlightedLines-Bqn4AiBL.js → useWorkerHighlightedLines-CPNYz6Wf.js} +2 -2
  83. package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
  84. package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
  85. package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
  86. package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
  87. package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
  88. package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
  89. package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
  90. package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
  91. package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
  92. package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
  93. package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
  94. package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
  95. package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
  96. package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
  97. package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
  98. package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
  99. package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
  100. package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
  101. package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
  102. package/dist/index.html +13 -13
  103. package/dist/mini-chat.html +13 -13
  104. package/dist/mobile.html +13 -13
  105. package/package.json +1 -1
  106. package/server/index.js +327 -62
  107. package/server/lib/assistants/DOCUMENTATION.md +14 -9
  108. package/server/lib/assistants/contracts.js +11 -1
  109. package/server/lib/assistants/projection-history.test.js +265 -18
  110. package/server/lib/assistants/routes.js +6 -0
  111. package/server/lib/assistants/service.js +417 -231
  112. package/server/lib/assistants/service.test.js +392 -148
  113. package/server/lib/conversations/DOCUMENTATION.md +1 -1
  114. package/server/lib/conversations/service.js +1 -0
  115. package/server/lib/conversations/service.test.js +16 -0
  116. package/server/lib/conversations/validation.js +11 -1
  117. package/server/lib/event-stream/DOCUMENTATION.md +3 -1
  118. package/server/lib/event-stream/directory-ws-bridge.js +13 -2
  119. package/server/lib/event-stream/global-hub.js +49 -4
  120. package/server/lib/event-stream/reasoning-projection.js +16 -1
  121. package/server/lib/event-stream/reasoning-projection.test.js +15 -0
  122. package/server/lib/event-stream/runtime.js +57 -20
  123. package/server/lib/event-stream/runtime.test.js +112 -0
  124. package/server/lib/llm/DOCUMENTATION.md +33 -9
  125. package/server/lib/llm/catalog.js +1 -1
  126. package/server/lib/llm/completions.js +5 -1
  127. package/server/lib/llm/completions.test.js +3 -3
  128. package/server/lib/llm/generate.js +237 -145
  129. package/server/lib/llm/generate.test.js +201 -46
  130. package/server/lib/llm/routes.js +2 -0
  131. package/server/lib/message-queue/runtime.js +15 -2
  132. package/server/lib/notifications/runtime.js +23 -5
  133. package/server/lib/notifications/runtime.test.js +11 -0
  134. package/server/lib/opencode/DOCUMENTATION.md +85 -17
  135. package/server/lib/opencode/agent-document.d.ts +28 -0
  136. package/server/lib/opencode/agent-document.js +280 -0
  137. package/server/lib/opencode/agent-document.test.js +137 -0
  138. package/server/lib/opencode/agents.js +96 -0
  139. package/server/lib/opencode/auth-state-runtime.js +5 -0
  140. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  141. package/server/lib/opencode/config-entity-routes.js +42 -99
  142. package/server/lib/opencode/config-entity-routes.test.js +107 -0
  143. package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
  144. package/server/lib/opencode/ensure-cli.js +1 -2
  145. package/server/lib/opencode/env-runtime.js +40 -26
  146. package/server/lib/opencode/env-runtime.test.js +10 -17
  147. package/server/lib/opencode/feature-routes-runtime.js +43 -4
  148. package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
  149. package/server/lib/opencode/http-server-connections.js +98 -0
  150. package/server/lib/opencode/http-server-connections.test.js +321 -0
  151. package/server/lib/opencode/lifecycle.js +349 -39
  152. package/server/lib/opencode/lifecycle.test.js +322 -0
  153. package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
  154. package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
  155. package/server/lib/opencode/openchamber-routes.js +7 -1
  156. package/server/lib/opencode/opencode2-pin.js +4 -4
  157. package/server/lib/opencode/opencode2-pin.test.js +1 -0
  158. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +127 -4
  159. package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
  160. package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
  161. package/server/lib/opencode/plugin-routes.js +3 -18
  162. package/server/lib/opencode/plugin-routes.test.js +9 -9
  163. package/server/lib/opencode/provider-catalog.js +58 -12
  164. package/server/lib/opencode/provider-catalog.test.js +48 -0
  165. package/server/lib/opencode/proxy.js +252 -46
  166. package/server/lib/opencode/proxy.test.js +235 -2
  167. package/server/lib/opencode/routes.js +296 -62
  168. package/server/lib/opencode/routes.test.js +278 -12
  169. package/server/lib/opencode/runtime-contract.js +401 -0
  170. package/server/lib/opencode/runtime-contract.test.js +214 -0
  171. package/server/lib/opencode/server-opencode-fetch.js +242 -0
  172. package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
  173. package/server/lib/opencode/server-utils-runtime.js +33 -0
  174. package/server/lib/opencode/shared-service.integration.test.js +66 -0
  175. package/server/lib/opencode/shared-service.js +147 -0
  176. package/server/lib/opencode/shared-service.test.js +93 -0
  177. package/server/lib/opencode/shared.js +3 -3
  178. package/server/lib/opencode/shutdown-runtime.js +15 -6
  179. package/server/lib/opencode/shutdown-runtime.test.js +49 -1
  180. package/server/lib/opencode/skill-routes.js +20 -33
  181. package/server/lib/opencode/skill-routes.test.js +35 -2
  182. package/server/lib/opencode/skills.test.js +15 -0
  183. package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
  184. package/server/lib/opencode/static-routes-runtime.js +3 -1
  185. package/server/lib/opencode/static-routes-runtime.test.js +33 -0
  186. package/server/lib/opencode/v2-client.js +14 -3
  187. package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
  188. package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
  189. package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
  190. package/server/lib/permission-auto-accept/runtime.js +14 -7
  191. package/server/lib/permission-auto-accept/runtime.test.js +27 -12
  192. package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
  193. package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -0
  194. package/server/lib/scheduled-tasks/runtime.js +52 -4
  195. package/server/lib/scheduled-tasks/runtime.test.js +80 -1
  196. package/server/lib/session-goal/DOCUMENTATION.md +113 -13
  197. package/server/lib/session-goal/routes.js +9 -0
  198. package/server/lib/session-goal/runtime.js +1579 -117
  199. package/server/lib/session-goal/runtime.test.js +1453 -84
  200. package/server/lib/session-index/DOCUMENTATION.md +18 -15
  201. package/server/lib/session-index/event-ingest.js +89 -16
  202. package/server/lib/session-index/event-ingest.test.js +73 -0
  203. package/server/lib/session-index/host-authority-snapshot.js +92 -0
  204. package/server/lib/session-index/routes.host-archive.test.js +201 -0
  205. package/server/lib/session-index/routes.js +63 -10
  206. package/server/lib/session-index/routes.test.js +8 -8
  207. package/server/lib/session-index/service.js +7 -1
  208. package/server/lib/session-index/sync-runtime.js +161 -34
  209. package/server/lib/session-index/sync-runtime.test.js +155 -1
  210. package/server/lib/session-metadata/DOCUMENTATION.md +61 -16
  211. package/server/lib/session-metadata/routes.js +108 -9
  212. package/server/lib/session-metadata/routes.test.js +47 -2
  213. package/server/lib/session-metadata/session-archive.d.ts +73 -0
  214. package/server/lib/session-metadata/session-archive.js +654 -0
  215. package/server/lib/session-metadata/session-archive.test.js +359 -0
  216. package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
  217. package/server/lib/session-metadata/session-metadata-store.js +212 -72
  218. package/server/lib/session-metadata/session-metadata-store.test.js +205 -10
  219. package/server/lib/session-metadata/session-projection.d.ts +41 -0
  220. package/server/lib/session-metadata/session-projection.js +255 -0
  221. package/server/lib/session-metadata/session-projection.test.js +64 -0
  222. package/server/lib/session-metadata/system-session.js +55 -0
  223. package/server/lib/session-metadata/system-session.test.js +55 -0
  224. package/server/lib/session-title/DOCUMENTATION.md +40 -7
  225. package/server/lib/session-title/native-runtime.test.js +167 -0
  226. package/server/lib/session-title/runtime.js +67 -42
  227. package/server/lib/session-title/session-access.js +46 -0
  228. package/server/lib/session-title/session-access.test.js +91 -0
  229. package/server/lib/session-turn-pages/DOCUMENTATION.md +9 -8
  230. package/server/lib/session-turn-pages/routes.js +17 -102
  231. package/server/lib/session-turn-pages/routes.test.js +281 -0
  232. package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
  233. package/server/lib/session-turn-pages/session-message-projection.js +281 -0
  234. package/server/lib/small-model/DOCUMENTATION.md +101 -147
  235. package/server/lib/small-model/catalog.js +31 -51
  236. package/server/lib/small-model/catalog.test.js +65 -10
  237. package/server/lib/small-model/custom-api.js +68 -0
  238. package/server/lib/small-model/custom-api.test.js +64 -0
  239. package/server/lib/small-model/index.js +124 -240
  240. package/server/lib/small-model/resolve.js +35 -120
  241. package/server/lib/small-model/resolve.test.js +20 -110
  242. package/server/lib/small-model/routes.js +1 -3
  243. package/server/lib/small-model/summary-settings.test.js +157 -253
  244. package/server/opencode-proxy.test.js +2 -0
  245. package/dist/assets/AssistantView-BTCkm8yk.js +0 -3
  246. package/dist/assets/MarkstreamRendererImpl-CnkodGJW.js +0 -3
  247. package/dist/assets/MobileSurface-BXLeiZ4d.js +0 -2
  248. package/dist/assets/OverlayScrollbar-pMwxaCca.js +0 -2
  249. package/dist/assets/SettingsView-B5I0KDTZ.js +0 -25
  250. package/dist/assets/appThemeRegistry-COGfxTFV.js +0 -1
  251. package/dist/assets/collapsible-BXRd5_Hr.js +0 -1
  252. package/dist/assets/context-menu-jGtH6fR9.js +0 -1
  253. package/dist/assets/es-c52LTHHe.js +0 -15
  254. package/dist/assets/fr-PisFbUE8.js +0 -13
  255. package/dist/assets/gitApi-ButwzQ0N.js +0 -9
  256. package/dist/assets/insertionBoundaries-DGs7S5zv.js +0 -1
  257. package/dist/assets/ja-C8Ikun3Z.js +0 -15
  258. package/dist/assets/ko-ClFys5o2.js +0 -15
  259. package/dist/assets/main-Dn9rpsgl.js +0 -169
  260. package/dist/assets/markdown-worker-CptoICmq.js +0 -2
  261. package/dist/assets/miniChat-BpjxVqgg.js +0 -2
  262. package/dist/assets/mobile-nMJCaNq1.js +0 -2
  263. package/dist/assets/multirun-BvSG94mW.js +0 -4
  264. package/dist/assets/pl-CnaKWN5F.js +0 -15
  265. package/dist/assets/platform-CWTIp02q.js +0 -4
  266. package/dist/assets/projectMeta-DOe49dM_.js +0 -3
  267. package/dist/assets/pt-BR-s8OpfRI6.js +0 -15
  268. package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +0 -2
  269. package/dist/assets/renderMobileApp-y-S8y8v_.js +0 -28
  270. package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
  271. package/dist/assets/runtimeEndpointReset-B6BxhvR0.js +0 -147
  272. package/dist/assets/sessionChangeSummary-VVBlEXuh.js +0 -2
  273. package/dist/assets/src-BzX5UI-c.css +0 -2
  274. package/dist/assets/store-DJczmczv.js +0 -15
  275. package/dist/assets/textarea-3XR8sUfy.js +0 -1
  276. package/dist/assets/uk-CaX0iqkB.js +0 -15
  277. package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +0 -1
  278. package/dist/assets/useEffectiveDirectory-CN6JoCFw.js +0 -1
  279. package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +0 -414
  280. package/dist/assets/useSessionAutoCleanup-BLjymaFt.js +0 -2
  281. package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
  282. package/dist/assets/zh-CN-20MYEjha.js +0 -15
  283. package/dist/assets/zh-TW-DiN04qTw.js +0 -15
  284. package/server/lib/small-model/call.js +0 -427
  285. package/server/lib/small-model/call.test.js +0 -382
  286. package/server/lib/small-model/opencode-session.js +0 -183
  287. package/server/lib/small-model/opencode-session.test.js +0 -147
@@ -111,7 +111,8 @@ and keep their existing error result handling.
111
111
  - Successful `assign_session` and `watch_session` return `terminate: true` to stop the mutation loop. The harness then requests a tools-disabled model summary. Prerequisite tools (`list_projects` / `list_sessions`) may still run before assign/watch/stop in the same turn; multi-operation user intents remain ordered **across turns** after a successful assign/watch.
112
112
  - Same contact-turn assign gate (tools instance lifetime only, not cross-turn): identical args after success replay the cached success result (no second worker); different args after success, or parallel mismatched args, fail closed with a validation error. The idempotent key includes worker model selection and current-turn attachment scope. Failure clears the gate so a corrected retry may run. Parallel identical args share one in-flight promise.
113
113
  - After a successful assign or watch, the service persists the existing session card into the contact transcript (user bubbles first, then the card) and watches that worker session in `assistant_contact_watch` (watch baselines the live status). The user-facing outcome is the session card plus any short **spoken** preamble; English tool-result confirms (`Opened a coding session.`, `Watching that coding session.`, `Created assistant…`, `Sent to…`) for card/side-effect tools are **not** written into the transcript.
114
- - Existing OpenCode SSE on the same `globalEventHub` as notifications (`session.idle`, `session.error`, `session.status`, `question.asked`, `permission.asked`) updates **the same card** (`busy` → `complete` / `error` / `question` / `cancelled`). **No canned settle transcript bubbles** (`oc.settle.complete` / `error` / `question` are not written). On **complete, error, or cancelled (user interrupt)** when the watch status actually changed, the service schedules an async contact-lane **continuation** (`inContactTurnLane` + `runContactTurn`) per related assistant: no admitted user row (internal `userText` only), active contact turn (green dot / 3-dot), stable resume id `resume_${assistantID}_${sessionID}_${status}_${updatedAt}`, bounded last worker assistant text (~2000 chars; fetch failure still resumes with status only). Cancelled continuations include `reason: user_interrupted` so the contact can acknowledge the user's abort. The contact Agent can perform read-only lookups and summarize in the user's language, but cannot reassign or mutate a session during this notification — the user sees that Agent summary, not 「会话已完成/会话失败」. `question` updates the card only (worker waiting on the user). `stop_session` cancellation does not schedule this continuation. Session-goal settle (`emitGoalNotification`) uses the same reporter — not a second scheduler. Idle after `session.error` must not rewrite 失败 as 完成 (`changed=false` → no second resume). Same complete status re-reported is idempotent.
114
+ - Existing OpenCode SSE on the same `globalEventHub` as notifications (`session.idle`, `session.error`, `session.status`, `session.execution.*`, `question.asked`, `permission.asked`) updates **the same card** (`busy` → `complete` / `error` / `question` / `cancelled`). **No canned settle transcript bubbles** (`oc.settle.complete` / `error` / `question` are not written). On **complete, error, or cancelled (user interrupt)** when the watch status actually changed, the service schedules an async contact-lane **continuation** (`inContactTurnLane` + `runContactTurn`) per related assistant: no admitted user row (internal `userText` only), active contact turn (green dot / 3-dot), stable resume id `resume_${assistantID}_${sessionID}_${status}_${updatedAt}`, bounded last worker assistant text (~2000 chars; fetch failure still resumes with status only). Cancelled continuations include `reason: user_interrupted` so the contact can acknowledge the user's abort. The contact Agent can perform read-only lookups and summarize in the user's language, but cannot reassign or mutate a session during this notification — the user sees that Agent summary, not 「会话已完成/会话失败」. `question` updates the card only (worker waiting on the user). `stop_session` cancellation does not schedule this continuation. Session-goal settle (`emitGoalNotification`) uses the same reporter — not a second scheduler. Idle after `session.error` must not rewrite 失败 as 完成 (`changed=false` → no second resume). Same complete status re-reported is idempotent.
115
+ - **Shutdown interrupt (ticket 07):** official `session.execution.interrupted` reason is `user | shutdown | superseded | inactivity`. **`reason: shutdown` does not settle** the watch and does **not** schedule contact resume — upstream keeps the execution claim for restart recovery. The card stays `busy` until an authoritative terminal (`session.execution.succeeded` / `failed` / user interrupt / reconcile). `reason: user` settles `cancelled` + resume. Other non-shutdown interrupts settle `error` unless `MessageAbortedError`. No parallel recovery store — the existing watch row remains the owner.
115
116
  - Restart or a missed `session.idle` cannot leave the watch stuck: boot and the existing 60s reconcile timer `listInFlightWatches` and poll OpenCode `session.get` / `message.list`. Idle, `time.completed`, or a missing session settles the same card (`busy` → 完成) and triggers the same contact continuation (no `oc.settle.complete` bubble). `session.error` or last-assistant `info.error` settles 失败 + resume once; last-assistant `MessageAbortedError` settles `cancelled` + the interrupt notify once. Transport / non-404 get errors leave the watch in flight (failure is not empty success). One failed watch does not block unrelated watches. Reconcile reuses `reportAssignedSession`, so an error watch is never rewritten as complete. Tombstoned assistants skip resume.
116
117
  - Worker sessions stamp `metadata.openchamber.assigned` (`from: 'contact'`, `assistantID`, `name`). They must **not** use `openchamber.assistant.assistantID` — that marker hides archived Assistant bindings from Chat.
117
118
  - `AssistantDTO.assignedSessionIDs` lists in-flight (`busy`/`question`) watches for assigned worker sessions. Session-card busy stays on the card; it does **not** drive the list green dot.
@@ -243,13 +244,17 @@ and keep their existing error result handling.
243
244
 
244
245
  ## History coverage
245
246
 
246
- Snapshots expose the latest 50 `historySessionIDs` in chronological order and `historySessionCount` for the full count. The messages pagination endpoint owns complete history, including the current binding. A successful direct, shared, or queued OpenCode admission writes its user message and provisional parts to the Assistant SQLite mirror before the HTTP operation settles; this covered admission row survives binding replacement, restart, and an unavailable OpenCode history endpoint. Matching live parts replace provisional parts by message ID. Other event mirrors retain raw JSON as uncovered (`covered=0`) provisional fallback rows: they remain visible in archived history until an explicit shrink signal. Authoritative OpenCode backfill upserts covered rows and reconciles authoritative parts, rejects entries whose embedded session ID belongs to another session, and never deletes provisional mirrors merely because a successful page omitted them. Pagination `complete` only records that the OpenCode cursor was exhausted; it does not authorize deleting uncovered event mirrors. The only shrink whitelist is explicit `message.removed`, an authoritative `message.list` 404 (`session-missing`), or assistant deletion. Late archived `message.updated` / `message.part.updated` and session lifecycle events (`session.idle`, `session.error`, `session.status` busy/retry/idle) call `invalidateBackfill` to reopen that session's cursor/complete without clearing covered flags. `history-state.js` owns the pure backfill reducer (`invalidate` → preserve; `page` → complete iff next cursor empty, always preserve; `session-missing` → discard provisional). Backfill persists the OpenCode `x-next-cursor` header verbatim and derives completion from its presence. A demand request commits up to three upstream pages and returns committed archived mirror rows (covered and provisional) with a stable scan-boundary cursor when more work remains. Startup begins one non-blocking current-binding backfill page. Archived rows preserve their stored directory; a legacy null directory triggers an authoritative session lookup of `data.directory` and `data.project.worktree`, with containment validation before persistence. Unresolved legacy directories remain `null` in persisted and historical response data. Ordinary mirror upserts preserve covered rows and never blanket-invalidate a session; explicit authoritative upserts elevate coverage; session-wide `invalidateCoverage` preserves every already-covered user row, including admitted rows whose local marker was replaced by live OpenCode info, while resetting other coverage on archive/re-archive; message-scoped coverage invalidation runs only after structural part deletes that require re-backfill. Removing an assistant tombstones the row and deletes its session history, message/part mirrors, and backfill state in one transaction. Schema v11 adds the mirror coverage marker.
247
+ Snapshots expose the latest 50 `historySessionIDs` in chronological order and `historySessionCount` for the full count. The messages pagination endpoint owns complete OpenCode history across archived and current bindings.
247
248
 
248
- `assistants` owns Assistant configuration, session bindings, and a durable assistant-owned OpenCode message/part mirror in `assistants.sqlite`. Routes register before the generic OpenCode proxy.
249
+ **V2 authority:** OpenCode session projections (`message.list` / `GET /session/:id/message`) are the sole message-body source. Assistant SQLite does **not** persist message/part bodies on send, share, live events, or demand reads. Legacy `assistant_message_mirror` / `assistant_message_part_mirror` / `assistant_message_backfill` tables may still exist for leftover rows and assistant-delete cleanup, but runtime never writes or serves them for history. Live SSE events may still drive assigned-session watch settlement; they do not cache transcript bodies.
249
250
 
250
- `GET /api/openchamber/assistants/capability` returns `supported`, the persisted `enabled` switch, domain revision, relay-derived stable `serverInstanceID`, and explicit v2 gaps `sharingAvailable`, `archiveMetadataAvailable`, and `sessionMetadataAvailable` (all `false`). Fresh databases default `enabled` to off; Settings flips the switch, and product navigation entries stay hidden until it is on. `PUT /api/openchamber/assistants/settings` changes the global switch with `expectedRevision`. CRUD and snapshots live under `/api/openchamber/assistants`. Session routes are `POST /:id/session/ensure`, `/:id/session/new`, and `/:id/session/compact`; composer sends use `POST /:id/messages` (contact harness, not `session.prompt`); contact history is `GET /:id/contact/messages` (full transcript, including rows before a clear-memory boundary); contact reset wipe is `POST /:id/contact/reset`; shared contact read watermark is `POST /:id/contact/read`; session cards are `POST /:id/contact/cards`; read-only inter-assistant DMs are `POST /:fromID/contact/dm`; assistant-owned scheduled tasks are `GET /:id/scheduled-tasks`. Shares still use `POST /:id/share` and `GET /share-operations/:operationID`.
251
+ `GET /api/openchamber/assistants/:assistantID/messages?before=&limit=` returns `{ entries: [{ sessionID, directory, info, parts }], nextCursor, complete, partial, failed? }`. The service lists bindings from `assistant_session_history` plus the live current session and walks them newest-first. Each binding reads the official v2 projection (`message.list` / `GET /api/session/:id/message`): first page uses `order=desc` only; continuation uses the opaque `cursor` only (never combine `order` + `cursor`). Rows keep the authoritative projection sequence (not `time.created` re-sort). The Host `before` cursor stores binding ordinal + upstream opaque cursor + page-local `skip` (positions only — no message bodies). A demand request uses at most three upstream pages with a stable `limit` page size. Public `entries` are ascending. `complete` is true only when every binding is exhausted and nothing failed. A mid-scan binding failure sets `partial: true`, `complete: false`, `failed: [{ sessionID, sessionOrdinal, status }]`, keeps already-fetched newer siblings, and leaves `nextCursor` on that binding so a retry can fill the gap without permanently skipping it. Transient 5xx/network failures retry up to three times; persistent total failure (no successful binding and no entries) throws `upstream_error`. Authoritative 404 for a binding skips it without blocking others. Leftover mirror rows are ignored. When `fetchImpl` is injected, history uses the same official path shape (`/api/session/:id/message`, `limit`, first-page `order=desc` or continuation `cursor`, optional `directory`).
251
252
 
252
- `AssistantDTO.mode` remains `continuous` or `stateless` (default `continuous`) for share/queue OpenCode session binding. It is **not** a user-facing memory setting: Settings no longer exposes the continuous/stateless toggle, and new assistants stay continuous contact history with server-side LLM trim. `historySessionIDs` lists prior OpenCode session bindings in chronological order (oldest first, capped). Continuous Assistants keep one long-lived current session across composer turns. Stateless Assistants create and bind a fresh OpenCode session for every composer send, share admission, and queued delivery so the model executes without prior transcript context; prior sessions remain available in OpenCode history. The Assistant surface follows the current binding, so each stateless turn rematerializes onto that fresh session. `AssistantDTO.workspacePath` retains the configuration value: `null` selects the managed workspace and a string selects a registered project path. `AssistantDTO.managedWorkspacePath` always carries the canonical managed target at `dataDir/assistant-workspaces/<assistantID>`, including for project-configured Assistants. `AssistantDTO.effectiveWorkspacePath` carries the resolved OpenCode session directory. `AssistantDTO.variant` is nullable and reaches OpenCode `session.prompt` with provider, model, agent, and system prompt. `SessionBinding` is `{ sessionID, directory, sessionGeneration }` with the effective directory. `ensure` and `new` return `SessionBinding`; `compact` returns `{ binding, summarized: true }`; messages return `{ binding, messageID, admitted: true }`; shares return top-level `{ operationID, assistantID, sessionID, messageID, state, phase, attempt, leaseExpiresAt, errorCode }`. `contracts.js` exports serialization fixtures for these DTOs. Binding replacement uses generation CAS, so concurrent creation retains one winner while orphan sessions stay available in OpenCode history. Workspace changes create and bind a session in the new directory. Session metadata carries `openchamber.assistant.assistantID` and `name` as the authoritative ownership marker. New OpenCode sessions use the fixed human title `[Assistant] ${name}` and are archived via `session.update({ time: { archived } })` immediately after create and before binding is returned, so ordinary active lists never flash them; metadata isolation still applies if archive fails after create. The session index and global session lists hide system sessions by this metadata (never by title prefix). Assistant history and direct source open remain the entry points.
253
+ `assistants` owns Assistant configuration, session bindings, and contact/share metadata in `assistants.sqlite`. OpenCode message bodies are read from upstream projections, not mirrored. Routes register before the generic OpenCode proxy.
254
+
255
+ `GET /api/openchamber/assistants/capability` returns `supported`, the persisted `enabled` switch, domain revision, relay-derived stable `serverInstanceID`, and explicit v2 gaps `sharingAvailable` (always `false`), `archiveMetadataAvailable` (true only when Host archive is injected), and `sessionMetadataAvailable` (always `false`). Fresh databases default `enabled` to off; Settings flips the switch, and product navigation entries stay hidden until it is on. `PUT /api/openchamber/assistants/settings` changes the global switch with `expectedRevision`. CRUD and snapshots live under `/api/openchamber/assistants`. Session routes are `POST /:id/session/ensure`, `/:id/session/new`, and `/:id/session/compact`; composer sends use `POST /:id/messages` (contact harness, not `session.prompt`); contact history is `GET /:id/contact/messages` (full transcript, including rows before a clear-memory boundary); contact reset wipe is `POST /:id/contact/reset`; shared contact read watermark is `POST /:id/contact/read`; session cards are `POST /:id/contact/cards`; read-only inter-assistant DMs are `POST /:fromID/contact/dm`; assistant-owned scheduled tasks are `GET /:id/scheduled-tasks`. Shares still use `POST /:id/share` and `GET /share-operations/:operationID`.
256
+
257
+ `AssistantDTO.mode` remains `continuous` or `stateless` (default `continuous`) for share/queue OpenCode session binding. It is **not** a user-facing memory setting: Settings no longer exposes the continuous/stateless toggle, and new assistants stay continuous contact history with server-side LLM trim. `historySessionIDs` lists prior OpenCode session bindings in chronological order (oldest first, capped). Continuous Assistants keep one long-lived current session across composer turns. Stateless Assistants create and bind a fresh OpenCode session for every composer send, share admission, and queued delivery so the model executes without prior transcript context; prior sessions remain available in OpenCode history. The Assistant surface follows the current binding, so each stateless turn rematerializes onto that fresh session. `AssistantDTO.workspacePath` retains the configuration value: `null` selects the managed workspace and a string selects a registered project path. `AssistantDTO.managedWorkspacePath` always carries the canonical managed target at `dataDir/assistant-workspaces/<assistantID>`, including for project-configured Assistants. `AssistantDTO.effectiveWorkspacePath` carries the resolved OpenCode session directory. `AssistantDTO.variant` is nullable and reaches OpenCode `session.prompt` with provider, model, agent, and system prompt. `SessionBinding` is `{ sessionID, directory, sessionGeneration }` with the effective directory. `ensure` and `new` return `SessionBinding`; `compact` returns `{ binding, summarized: true }`; messages return `{ binding, messageID, admitted: true }`; shares return top-level `{ operationID, assistantID, sessionID, messageID, state, phase, attempt, leaseExpiresAt, errorCode }`. `contracts.js` exports serialization fixtures for these DTOs. Binding replacement uses generation CAS, so concurrent creation retains one winner while orphan sessions stay available in OpenCode history. Workspace changes create and bind a session in the new directory. Session metadata carries `openchamber.assistant.assistantID` and `name` as the authoritative ownership marker. New OpenCode sessions use the fixed human title `[Assistant] ${name}` and persist that isolation through the Host session-metadata store immediately after create (`persistSessionMetadata` + `onSystemSessionPersisted`) so the sidebar index drops them without waiting for list sync. They are archived via Host archive immediately after create and before binding is returned, so ordinary active lists never flash them; metadata isolation still applies if archive fails after create. The session index and global session lists hide system sessions by this metadata (never by title prefix). Assistant history and direct source open remain the entry points.
253
258
 
254
259
  **Contact send admission + SSE (this PR)**
255
260
 
@@ -264,11 +269,11 @@ Snapshots expose the latest 50 `historySessionIDs` in chronological order and `h
264
269
 
265
270
  Composer messages carry a client message ID. The service accepts the existing OpenCode delivery parts (`{ type: 'text' }` and `{ type: 'file', mime, url, filename? }` data URLs — not a second attachment store), extracts text (`[attachment]` when the send is file-only), admits the user at 202, then runs the contact harness asynchronously (`runContactTurn` / pi-agent-core, thinking off, pi `read` / `write` / `edit` / `bash` in the assistant workspace, OpenChamber tools `new_conversation` / `clear_chat_history` / `list_projects` / `list_sessions` / `get_assistant_settings` / `update_default_prompt` / `create_assistant` / `schedule_task` / `message_assistant` / `assign_session`, merged `.agents`/`.claude` skills, plus the registered-projects catalog each turn) through the OpenChamber completions gateway (public HTTP non-streaming; in-process `onTextDelta` for contact SSE). Assistant bubbles and tool cards persist when they publish (bubble `done:true` / card insert) and turn end upserts any remainder. File parts survive `GET /:id/contact/messages`. The harness forwards those file parts to the completions gateway. Image data URLs reach `session.prompt` only when the connected catalog marks the model as vision-capable; otherwise generate keeps the `[image: …]` description and text-file bytes so a non-vision model cannot stall the contact turn. Gateway 502 responses include `{ error, message }` so the composer can show the OpenCode error string. OpenCode `session.prompt` is not the contact conversation engine; assign uses it only on the **worker** session. Share and queued delivery still use it on Assistant bindings. OpenCode `204` is an admitted empty response. A 404 restores one generation-scoped binding before retrying. Ambiguous network errors preserve the client message ID without another send. Every committed Assistant revision publishes `openchamber:assistants-changed` only after its SQLite transaction completes; clients use the tip to reload the authoritative snapshot after worker-driven stateless binding changes. `POST /:id/session/abort` requires the same `{ sessionID, sessionGeneration }`, returns the binding and `aborted: true`, reports a changed binding as `revision_conflict`, and preserves upstream missing-session `not_found` semantics.
266
271
 
267
- `GET /api/openchamber/assistants/:assistantID/messages?before=&limit=` returns archived and current bindings as `{ entries: [{ sessionID, directory, info, parts }], nextCursor, complete }`. It reads assistant-owned mirror rows in chronological session and message order, pages toward older rows with an opaque stable cursor, and preserves the raw OpenCode `info` and `part` JSON. `directory` is the effective workspace for that source session and is nullable for legacy archived rows with an unknown workspace. `complete === (nextCursor === null)` marks arrival at the oldest available history. The service reads the latest persisted `limit + 1` rows first, then demand-backfills one page at a time from the newest incomplete archived session covered by the cursor; a request performs at most three upstream pages and reuses persisted pages without upstream access. Backfill page commits are independent. Each `message.list` page attempt retries only transient 5xx/network-class SDK failures up to three times with short backoff; persistent failures still throw `upstream_error`. An authoritative OpenCode 404 for an archived or old binding marks that session's backfill complete via the `session-missing` reducer path, deletes only its uncovered event mirrors, preserves covered/admitted rows, and continues scanning other incomplete sessions so one deleted session cannot permanently block Assistant history. Successful backfill pages upsert authoritative rows without clearing provisional mirrors missing from that snapshot. Concurrent ensure that replaces a missing current binding archives the old ID and creates a new one; 404 completion never deletes covered history under either identity. A required-page failure surfaces as an upstream error only when the current request's `pageRows()` is empty; when the current range already has covered or provisional rows, the service returns that partial result and keeps the incomplete cursor retryable. Provisional event mirrors stay readable as `covered=0` fallbacks until a shrink-whitelist signal or a later authoritative upsert elevates them. Current-session live sync overrides the matching SQLite message identity in the UI.
272
+ `GET /api/openchamber/assistants/:assistantID/messages?before=&limit=` is documented under **History coverage** (OpenCode projection source; no SQLite body mirror).
268
273
 
269
274
  Queue admission accepts `{ kind: 'assistant', assistantID }`, captures a structured binding plus provider, model, agent, nullable variant, default prompt/system, compiled text parts, and durable attachment descriptors. Direct messages, shares, and queued Assistant deliveries accept up to 129 strict OpenCode text/file parts, including up to 64 file parts. Queue dispatch uses `sendWithCapturedConfig`, so queued work never reads mutable Assistant configuration. Continuous delivery retains binding fencing and a changed binding produces `stale_target`. Stateless delivery instead belongs to the stable `assistant:<assistantID>` queue lane: it ignores disposable binding replacement, serializes prompt admissions per Assistant, and creates a fresh execution Session for every row.
270
275
 
271
- Share operations persist operation identity, Assistant identity, payload hash, phase, session ID, message ID, state, attempt, lease expiry, and error for seven days. `ShareOperationDTO` exposes top-level `sessionID` and `messageID` with `state` in `submitting`, `running`, `completed`, `failed`, or `unresolved`; it carries no binding. Admission and claims use SQLite transactions and compare-and-swap updates. A `reserving` phase is inserted or read under `BEGIN IMMEDIATE` before any session ensure or creation side effect. Its unique owner chooses and records the session and message IDs before claim and submit; duplicate matching requests reuse and poll that operation, while payload conflicts return `idempotency_conflict` without session work. Reservation-owner session creation failures delete the still-reserving record so a retry can reserve and recover. `running` work has a lease; `submitted` phase reconciles through message-ID lookup; expired submitted work becomes `unresolved`; failed work permits one CAS claimant to retry. `ShareOperationDTO` is available from the share response and `GET /share-operations/:operationID`. Repeated shares reuse that record. Ordinary composer messages create no operation record, but every accepted message creates or refreshes its Assistant message-mirror row.
276
+ Share operations persist operation identity, Assistant identity, payload hash, phase, session ID, message ID, state, attempt, lease expiry, and error for seven days. `ShareOperationDTO` exposes top-level `sessionID` and `messageID` with `state` in `submitting`, `running`, `completed`, `failed`, or `unresolved`; it carries no binding. Admission and claims use SQLite transactions and compare-and-swap updates. A `reserving` phase is inserted or read under `BEGIN IMMEDIATE` before any session ensure or creation side effect. Its unique owner chooses and records the session and message IDs before claim and submit; duplicate matching requests reuse and poll that operation, while payload conflicts return `idempotency_conflict` without session work. Reservation-owner session creation failures delete the still-reserving record so a retry can reserve and recover. `running` work has a lease; `submitted` phase reconciles through message-ID lookup; expired submitted work becomes `unresolved`; failed work permits one CAS claimant to retry. `ShareOperationDTO` is available from the share response and `GET /share-operations/:operationID`. Repeated shares reuse that record. Ordinary composer messages create no operation record. Share/queue admissions do not write Assistant message-mirror bodies; OpenCode projection remains authoritative.
272
277
 
273
278
  Schema v2 migrates legacy Inbox bindings once. It prefers the legacy Inbox session, then the latest successful operation or turn session. Schema v5 makes `workspace_path` nullable and migrates stored managed paths under `dataDir/assistant-workspaces/<assistantID>` to `NULL`. Schema v7 adds `mode` (`continuous` | `stateless`, default `continuous`) and preserves legacy `assistant.mode` during Inbox migration. Schema v10 establishes demand-driven history backfill semantics; existing mirror rows remain partial until a complete backfill state is persisted. Schema v13 adds `assistant_contact_read_state` and seeds existing contact transcripts to the current tip (default-read). New records use `assistant_v2` and carry no legacy topic field. Legacy topic, turn, operation, and assistant tables remain migration sources and compatibility backups. Runtime code never reads or writes them. Migration retains OpenCode sessions and messages.
274
279
 
@@ -276,7 +281,7 @@ Assistant workspaces resolve only under the Assistant workspace root or configur
276
281
 
277
282
  ### Session operations from a contact
278
283
 
279
- Contact tools expose `assign_session` (new or continued session), `steer_session` (insert a user instruction using SDK `session.prompt` with `delivery: steer`, without a model override), `stop_session` (real SDK abort), `archive_session` (SDK update `time.archived`), and `delete_session` (SDK delete). All existing-session mutations resolve directory from authoritative session metadata and apply the same registered-project scope checks. They accept exact session IDs from references, history, or `list_sessions`; ambiguous targets require clarification. Delete is permanent and is only offered for an explicit deletion request. SDK failures remain tool errors, never success confirmations; a failed mutation does not clear its watch. Successful archive/delete cancel assistant watches and pending automatic resumes, just as explicit stop does. Archive itself does not abort OpenCode execution: stopping and archiving are separate operations. Steering preserves the upstream session's model and does not create a second session. Terminal operation tools execute once per successful contact turn. All surfaces use the same server contact tool implementation.
284
+ Contact tools expose `assign_session` (new or continued session), `steer_session` (insert a user instruction using SDK `session.prompt` with `delivery: steer`, without a model override), `stop_session` (real SDK abort), `archive_session` (Host archive via `session-metadata` `openchamber.archive.archivedAt`, not upstream `session.update`), and `delete_session` (SDK delete; Host metadata cleanup follows `session.deleted`). All existing-session mutations resolve directory from authoritative session metadata and apply the same registered-project scope checks. They accept exact session IDs from references, history, or `list_sessions`; ambiguous targets require clarification. Delete is permanent and is only offered for an explicit deletion request. SDK failures remain tool errors, never success confirmations; a failed mutation does not clear its watch. Successful archive/delete cancel assistant watches and pending automatic resumes, just as explicit stop does. Archive itself does not abort OpenCode execution: stopping and archiving are separate operations. Steering preserves the upstream session's model and does not create a second session. Terminal operation tools execute once per successful contact turn. All surfaces use the same server contact tool implementation.
280
285
 
281
286
  ### Contact context and background notification boundary
282
287
 
@@ -291,6 +296,6 @@ Assign/watch tools forward the contact AbortSignal into their service calls and
291
296
 
292
297
  `read_session` is a read-only contact tool available on user turns and background notifications. An `@session:<id>` chip supplies identity only. The model reads contents through this tool before answering about the reference, and treats returned messages as quoted data, never as new user instructions or authorization to resume/watch/mutate. The quoted payload stays on the tool transcript for the model; contact bubbles keep only the spoken preamble and the post-read reply.
293
298
 
294
- The service resolves the exact session and registered project through the existing authoritative session resolver, then calls SDK `message.list` with `limit` (default 20, maximum 50), optional opaque `before`, and the turn AbortSignal. The upstream `x-next-cursor` is returned unchanged as `nextCursor`. Missing sessions, transport errors, malformed message lists and cross-session message identity mismatches fail closed. Empty successful lists remain distinguishable from failures.
299
+ The service resolves the exact session and registered project through the existing authoritative session resolver, then calls SDK `message.list` with `limit` (default 20, maximum 50) and the turn AbortSignal. The first page sends `order=desc`; a continuation sends only the opaque `before` as `cursor` (upstream rejects `order` together with `cursor`). Native v2 rows are projected to role + parts and quoted oldest→newest. The upstream body `cursor.next` (falling back to the `x-next-cursor` header) is returned unchanged as `nextCursor`. Missing sessions, transport errors, malformed message lists and cross-session message identity mismatches fail closed. Empty successful lists remain distinguishable from failures.
295
300
 
296
301
  Results retain message IDs, roles, text and bounded tool-result/file metadata. Text is limited to 4,000 characters per field and 24,000 total; each message contributes at most 20 parts. File URLs/bodies, reasoning and tool inputs are omitted. `partial` explicitly marks truncation, omissions or older pages. Reading creates no watch, prompt or other mutation.
@@ -68,7 +68,17 @@ export const assistantContractFixtures = Object.freeze({
68
68
  messageAdmission: Object.freeze({ binding: Object.freeze({ sessionID: 'ses_fixture', directory: '/workspace', sessionGeneration: 4 }), messageID: 'msg_fixture', admitted: true, revision: 4 }),
69
69
  abortResponse: Object.freeze({ binding: Object.freeze({ sessionID: 'ses_fixture', directory: '/workspace', sessionGeneration: 4 }), aborted: true }),
70
70
  shareOperation: Object.freeze({ operationID: 'share_fixture', assistantID: 'assistant_fixture', sessionID: 'ses_fixture', messageID: 'msg_fixture', state: 'running', phase: 'submitted', attempt: 1, leaseExpiresAt: 1234, errorCode: null }),
71
- historicalMessages: Object.freeze({ entries: Object.freeze([Object.freeze({ sessionID: 'ses_fixture', directory: null, info: Object.freeze({ id: 'msg_fixture', sessionID: 'ses_fixture', role: 'assistant', time: Object.freeze({ created: 1234 }) }), parts: Object.freeze([]) })]), nextCursor: null, complete: true }),
71
+ historicalMessages: Object.freeze({
72
+ entries: Object.freeze([Object.freeze({
73
+ sessionID: 'ses_fixture',
74
+ directory: null,
75
+ info: Object.freeze({ id: 'msg_fixture', sessionID: 'ses_fixture', role: 'assistant', time: Object.freeze({ created: 1234 }) }),
76
+ parts: Object.freeze([]),
77
+ })]),
78
+ nextCursor: null,
79
+ complete: true,
80
+ partial: false,
81
+ }),
72
82
  /** Frozen GET /assistants/:id/contact/messages page envelope (UI contract). */
73
83
  contactMessagesPage: Object.freeze({
74
84
  messages: Object.freeze([]),
@@ -8,14 +8,6 @@ import { createAssistantsService } from './service.js';
8
8
  const require = createRequire(import.meta.url);
9
9
  const root = () => fs.mkdtempSync(path.join(os.tmpdir(), 'assistants-proj-'));
10
10
 
11
- const v2Error = (result) => {
12
- if (!result?.error) return result;
13
- const error = new Error(result.error.code || 'upstream');
14
- error.status = result.error.status ?? result.error.statusCode ?? result.status;
15
- error.cause = { status: error.status };
16
- error.reason = 'UnexpectedStatus';
17
- throw error;
18
- };
19
11
  const createV2Client = (overrides = {}) => {
20
12
  const create = overrides.create ?? (async () => ({ id: crypto.randomUUID() }));
21
13
  const get = overrides.get ?? (async () => ({ id: 'present' }));
@@ -43,7 +35,14 @@ const createV2Client = (overrides = {}) => {
43
35
  },
44
36
  message: {
45
37
  list: async (input) => {
46
- const result = await list({ ...input, before: input?.cursor });
38
+ // Official contract fixture: refuse order+cursor combinations.
39
+ if (input?.cursor && input?.order != null) {
40
+ const error = new Error('InvalidRequestError');
41
+ error.status = 400;
42
+ error._tag = 'InvalidRequestError';
43
+ throw error;
44
+ }
45
+ const result = await list(input);
47
46
  if (result?.error) return result;
48
47
  if (Array.isArray(result?.data) || result?.cursor || result?.response) return result;
49
48
  if (Array.isArray(result)) return { data: result, cursor: {} };
@@ -62,6 +61,8 @@ const setup = (directory = root(), client = {}, options = {}) => {
62
61
  getOpenCodeAuthHeaders: () => ({ authorization: 'Bearer test' }),
63
62
  fetchImpl,
64
63
  clientFactory: () => createV2Client(client),
64
+ // Contact composer send needs a harness stub; body authority is OpenCode projection.
65
+ runContactTurn: serviceOptions.runContactTurn ?? (async ({ userText }) => ({ text: `reply:${userText}`, bubbles: [`reply:${userText}`] })),
65
66
  ...serviceOptions,
66
67
  });
67
68
  if (enabled) {
@@ -72,11 +73,60 @@ const setup = (directory = root(), client = {}, options = {}) => {
72
73
  };
73
74
  const assistantInput = { name: 'A', providerID: 'p', modelID: 'm' };
74
75
 
75
- const projectionEntry = (sessionID, id, created, text = id) => ({
76
- info: { id, sessionID, role: 'assistant', time: { created } },
76
+ const projectionEntry = (sessionID, id, created, text = id, role = 'assistant') => ({
77
+ info: { id, sessionID, role, time: { created } },
77
78
  parts: [{ id: `prt_${id}`, sessionID, messageID: id, type: 'text', text }],
78
79
  });
79
80
 
81
+ /** Official v2 SessionMessageInfo-shaped user/assistant with content (not only parts). */
82
+ const syntheticContentEntry = (sessionID, id, created, role, text) => ({
83
+ id,
84
+ sessionID,
85
+ type: role,
86
+ time: { created },
87
+ content: [{ type: 'text', text }],
88
+ });
89
+
90
+ /**
91
+ * Official-shaped desc pager: non-empty pages always expose cursor.next (even the
92
+ * logical last page); an empty follow-up page is the finite end.
93
+ */
94
+ const createOfficialDescPager = (rowsBySession, pageLimitDefault = null) => {
95
+ const state = new Map();
96
+ for (const [sessionID, rows] of Object.entries(rowsBySession)) {
97
+ state.set(sessionID, rows);
98
+ }
99
+ const calls = [];
100
+ const list = async (input) => {
101
+ const sessionID = input.sessionID;
102
+ const limit = Number(input.limit) || pageLimitDefault || 50;
103
+ const rows = state.get(sessionID) || [];
104
+ calls.push({
105
+ sessionID,
106
+ limit,
107
+ order: input.order ?? null,
108
+ cursor: input.cursor ?? null,
109
+ hasOrder: input.order != null,
110
+ });
111
+ let start = 0;
112
+ if (input.cursor) {
113
+ const marker = String(input.cursor).replace(/^next:/, '');
114
+ const idx = rows.findIndex((row) => (row.info?.id ?? row.id) === marker);
115
+ start = idx >= 0 ? idx + 1 : rows.length;
116
+ }
117
+ const page = rows.slice(start, start + limit);
118
+ // Non-empty ⇒ always next (even past the last real row marker).
119
+ const next = page.length > 0
120
+ ? `next:${page[page.length - 1].info?.id ?? page[page.length - 1].id}`
121
+ : null;
122
+ return {
123
+ data: page,
124
+ cursor: next ? { previous: null, next } : { previous: null, next: null },
125
+ };
126
+ };
127
+ return { list, calls };
128
+ };
129
+
80
130
  describe('ticket 11 assistants read from OpenCode projections', () => {
81
131
  it('does not persist message bodies after send, share, or live events', async () => {
82
132
  const directory = root();
@@ -105,8 +155,8 @@ describe('ticket 11 assistants read from OpenCode projections', () => {
105
155
  const calls = [];
106
156
  const service = setup(directory, {
107
157
  create: async () => ({ data: { id: `ses_${++creates}` } }),
108
- messages: async ({ sessionID, limit, order }) => {
109
- calls.push({ sessionID, limit, order });
158
+ messages: async ({ sessionID, limit, order, cursor }) => {
159
+ calls.push({ sessionID, limit, order, cursor });
110
160
  if (sessionID === 'ses_1') return { data: [projectionEntry('ses_1', 'msg_old', 1, 'from-projection')] };
111
161
  return { data: [projectionEntry(sessionID, 'msg_live', 2, 'current')] };
112
162
  },
@@ -130,8 +180,131 @@ describe('ticket 11 assistants read from OpenCode projections', () => {
130
180
  expect(page.entries.map((entry) => entry.info.id)).toEqual(['msg_old', 'msg_live']);
131
181
  expect(page.entries[0]?.parts[0]?.text).toBe('from-projection');
132
182
  expect(page.entries.some((entry) => entry.info.id === 'msg_mirror')).toBe(false);
133
- expect(calls.every((call) => call.order === 'desc')).toBe(true);
183
+ // First page of each binding may use order=desc; never with a cursor.
184
+ expect(calls.every((call) => call.cursor == null || call.order == null)).toBe(true);
185
+ expect(calls.some((call) => call.order === 'desc' && call.cursor == null)).toBe(true);
134
186
  expect(page.complete).toBe(true);
187
+ expect(page.partial).toBe(false);
188
+ service.close();
189
+ });
190
+
191
+ it('traverses source seq CBA across bindings with limit 1 (not time.created sort)', async () => {
192
+ // ses_old (ordinal lower): source desc page [B created20, C created10]
193
+ // ses_new: [A created30]
194
+ // Public ascending walk with limit 1 must be A → B → C (source seq), not time sort tricks.
195
+ let creates = 0;
196
+ const pager = createOfficialDescPager({
197
+ ses_1: [
198
+ projectionEntry('ses_1', 'msg_B', 20, 'B'),
199
+ projectionEntry('ses_1', 'msg_C', 10, 'C'),
200
+ ],
201
+ ses_2: [
202
+ projectionEntry('ses_2', 'msg_A', 30, 'A'),
203
+ ],
204
+ });
205
+ const service = setup(root(), {
206
+ create: async () => ({ data: { id: `ses_${++creates}` } }),
207
+ messages: pager.list,
208
+ });
209
+ const assistant = service.createAssistant({ ...assistantInput, mode: 'stateless' });
210
+ await service.ensure(assistant.id); // ses_1
211
+ await service.createNew(assistant.id); // ses_2 live
212
+ const seen = [];
213
+ let before;
214
+ for (let step = 0; step < 5; step++) {
215
+ const page = await service.historicalMessages(assistant.id, { before, limit: 1 });
216
+ seen.push(...page.entries.map((entry) => entry.info.id));
217
+ if (page.complete) break;
218
+ before = page.nextCursor;
219
+ expect(before).toEqual(expect.any(String));
220
+ }
221
+ expect(seen).toEqual(['msg_A', 'msg_B', 'msg_C']);
222
+ // Continuations must never send order with cursor.
223
+ expect(pager.calls.filter((call) => call.cursor).every((call) => call.hasOrder === false)).toBe(true);
224
+ expect(pager.calls.filter((call) => !call.cursor).every((call) => call.order === 'desc')).toBe(true);
225
+ service.close();
226
+ });
227
+
228
+ it('pages 8 rows with limit 2 and 3-page budget once each id, finite end (no tip loop)', async () => {
229
+ const rows = Array.from({ length: 8 }, (_, index) => {
230
+ const n = 8 - index;
231
+ return projectionEntry('ses_1', `msg_${n}`, n * 10, `t${n}`);
232
+ });
233
+ const pager = createOfficialDescPager({ ses_1: rows });
234
+ const service = setup(root(), {
235
+ create: async () => ({ data: { id: 'ses_1' } }),
236
+ messages: pager.list,
237
+ });
238
+ const assistant = service.createAssistant(assistantInput);
239
+ await service.ensure(assistant.id);
240
+ const pages = [];
241
+ let before;
242
+ let rounds = 0;
243
+ do {
244
+ const page = await service.historicalMessages(assistant.id, { before, limit: 2 });
245
+ rounds += 1;
246
+ pages.push(page);
247
+ // Each demand page is ascending within itself.
248
+ const ids = page.entries.map((entry) => entry.info.id);
249
+ expect(ids).toEqual([...ids].sort((a, b) => Number(a.slice(4)) - Number(b.slice(4))));
250
+ before = page.complete ? null : page.nextCursor;
251
+ expect(rounds).toBeLessThanOrEqual(8);
252
+ } while (before);
253
+ // Infinite-query flatten (oldest page first) yields full chronological seq once.
254
+ const seen = pages.slice().reverse().flatMap((page) => page.entries.map((entry) => entry.info.id));
255
+ expect(seen).toEqual(['msg_1', 'msg_2', 'msg_3', 'msg_4', 'msg_5', 'msg_6', 'msg_7', 'msg_8']);
256
+ expect(new Set(seen).size).toBe(8);
257
+ // Tip (no cursor) is opened once; later calls only continue via opaque cursor
258
+ // (page-local skip may re-hit the same upstream page, but never restarts the tip).
259
+ const tipCalls = pager.calls.filter((call) => call.cursor == null);
260
+ expect(tipCalls).toHaveLength(1);
261
+ expect(pages.at(-1)?.complete).toBe(true);
262
+ service.close();
263
+ });
264
+
265
+ it('marks middle binding 503 as partial with retryable cursor; recovery fills the gap', async () => {
266
+ let creates = 0;
267
+ let ses2Fail = true;
268
+ const service = setup(root(), {
269
+ create: async () => ({ data: { id: `ses_${++creates}` } }),
270
+ messages: async ({ sessionID }) => {
271
+ if (sessionID === 'ses_2' && ses2Fail) return { error: { status: 503 } };
272
+ return { data: [projectionEntry(sessionID, `msg_${sessionID}`, 1)] };
273
+ },
274
+ });
275
+ const assistant = service.createAssistant({ ...assistantInput, mode: 'stateless' });
276
+ await service.ensure(assistant.id); // ses_1
277
+ await service.createNew(assistant.id); // ses_2
278
+ await service.createNew(assistant.id); // ses_3 live
279
+ const first = await service.historicalMessages(assistant.id, { limit: 10 });
280
+ // Newest success delivered; failed middle is not skipped permanently.
281
+ expect(first.entries.map((entry) => [entry.sessionID, entry.info.id])).toEqual([
282
+ ['ses_3', 'msg_ses_3'],
283
+ ]);
284
+ expect(first.complete).toBe(false);
285
+ expect(first.partial).toBe(true);
286
+ expect(first.failed).toEqual([
287
+ expect.objectContaining({ sessionID: 'ses_2', status: 503 }),
288
+ ]);
289
+ expect(first.nextCursor).toEqual(expect.any(String));
290
+
291
+ ses2Fail = false;
292
+ const recovered = [];
293
+ let before = first.nextCursor;
294
+ for (let step = 0; step < 5; step++) {
295
+ const page = await service.historicalMessages(assistant.id, { before, limit: 10 });
296
+ recovered.push(...page.entries.map((entry) => [entry.sessionID, entry.info.id]));
297
+ if (page.complete) {
298
+ expect(page.partial).toBe(false);
299
+ break;
300
+ }
301
+ before = page.nextCursor;
302
+ }
303
+ // Public pages stay ascending by binding ordinal (older session first).
304
+ expect(recovered).toEqual([
305
+ ['ses_1', 'msg_ses_1'],
306
+ ['ses_2', 'msg_ses_2'],
307
+ ]);
135
308
  service.close();
136
309
  });
137
310
 
@@ -151,10 +324,11 @@ describe('ticket 11 assistants read from OpenCode projections', () => {
151
324
  await service.createNew(assistant.id);
152
325
  const page = await service.historicalMessages(assistant.id, { limit: 10 });
153
326
  expect(page.entries.map((entry) => [entry.sessionID, entry.info.id])).toEqual([
154
- ['ses_1', 'msg_ses_1'],
155
327
  ['ses_3', 'msg_ses_3'],
156
328
  ]);
157
329
  expect(page.entries.some((entry) => entry.sessionID === 'ses_2')).toBe(false);
330
+ expect(page.partial).toBe(true);
331
+ expect(page.complete).toBe(false);
158
332
 
159
333
  const isolated = setup(root(), {
160
334
  create: async () => ({ data: { id: 'ses_only' } }),
@@ -167,7 +341,29 @@ describe('ticket 11 assistants read from OpenCode projections', () => {
167
341
  service.close();
168
342
  });
169
343
 
170
- it('uses ticket 03 projection GET /session/:id/message when fetchImpl is provided', async () => {
344
+ it('projects real user/assistant content synthetic rows from official shape', async () => {
345
+ const pager = createOfficialDescPager({
346
+ ses_1: [
347
+ syntheticContentEntry('ses_1', 'msg_asst', 20, 'assistant', 'assistant-body'),
348
+ syntheticContentEntry('ses_1', 'msg_user', 10, 'user', 'user-body'),
349
+ ],
350
+ });
351
+ const service = setup(root(), {
352
+ create: async () => ({ data: { id: 'ses_1' } }),
353
+ messages: pager.list,
354
+ });
355
+ const assistant = service.createAssistant(assistantInput);
356
+ await service.ensure(assistant.id);
357
+ const page = await service.historicalMessages(assistant.id, { limit: 10 });
358
+ expect(page.entries.map((entry) => [entry.info.id, entry.info.role, entry.parts[0]?.text])).toEqual([
359
+ ['msg_user', 'user', 'user-body'],
360
+ ['msg_asst', 'assistant', 'assistant-body'],
361
+ ]);
362
+ expect(page.complete).toBe(true);
363
+ service.close();
364
+ });
365
+
366
+ it('uses official GET /api/session/:id/message when fetchImpl is provided', async () => {
171
367
  const directory = root();
172
368
  const urls = [];
173
369
  const service = setup(directory, {
@@ -176,7 +372,10 @@ describe('ticket 11 assistants read from OpenCode projections', () => {
176
372
  fetchImpl: async (input) => {
177
373
  const url = new URL(String(input));
178
374
  urls.push(url);
179
- return new Response(JSON.stringify({ data: [projectionEntry('ses_proj', 'msg_fetch', 1)] }), {
375
+ return new Response(JSON.stringify({
376
+ data: [projectionEntry('ses_proj', 'msg_fetch', 1)],
377
+ cursor: { previous: null, next: null },
378
+ }), {
180
379
  status: 200,
181
380
  headers: { 'content-type': 'application/json' },
182
381
  });
@@ -187,13 +386,61 @@ describe('ticket 11 assistants read from OpenCode projections', () => {
187
386
  const page = await service.historicalMessages(assistant.id, { limit: 20 });
188
387
  expect(page.entries.map((entry) => entry.info.id)).toEqual(['msg_fetch']);
189
388
  expect(urls).toHaveLength(1);
190
- expect(urls[0].pathname).toBe('/session/ses_proj/message');
389
+ expect(urls[0].pathname).toBe('/api/session/ses_proj/message');
191
390
  expect(urls[0].searchParams.get('limit')).toBe('20');
192
391
  expect(urls[0].searchParams.get('order')).toBe('desc');
392
+ expect(urls[0].searchParams.has('cursor')).toBe(false);
193
393
  expect(urls[0].searchParams.get('directory')).toEqual(expect.any(String));
194
394
  service.close();
195
395
  });
196
396
 
397
+ it('fetchImpl continuation omits order and only sends cursor', async () => {
398
+ const rows = [
399
+ projectionEntry('ses_1', 'msg_2', 2),
400
+ projectionEntry('ses_1', 'msg_1', 1),
401
+ ];
402
+ const urls = [];
403
+ const service = setup(root(), {
404
+ create: async () => ({ data: { id: 'ses_1' } }),
405
+ }, {
406
+ fetchImpl: async (input) => {
407
+ const url = new URL(String(input));
408
+ urls.push(url);
409
+ const cursor = url.searchParams.get('cursor');
410
+ if (!cursor) {
411
+ return new Response(JSON.stringify({
412
+ data: [rows[0]],
413
+ cursor: { next: 'opaque_next' },
414
+ }), { status: 200, headers: { 'content-type': 'application/json' } });
415
+ }
416
+ if (cursor === 'opaque_next') {
417
+ return new Response(JSON.stringify({
418
+ data: [rows[1]],
419
+ // Non-empty last page still exposes next; empty follow-up ends.
420
+ cursor: { next: 'opaque_end' },
421
+ }), { status: 200, headers: { 'content-type': 'application/json' } });
422
+ }
423
+ return new Response(JSON.stringify({
424
+ data: [],
425
+ cursor: { next: null },
426
+ }), { status: 200, headers: { 'content-type': 'application/json' } });
427
+ },
428
+ });
429
+ const assistant = service.createAssistant(assistantInput);
430
+ await service.ensure(assistant.id);
431
+ const first = await service.historicalMessages(assistant.id, { limit: 1 });
432
+ expect(first.entries.map((e) => e.info.id)).toEqual(['msg_2']);
433
+ expect(first.complete).toBe(false);
434
+ const second = await service.historicalMessages(assistant.id, { before: first.nextCursor, limit: 1 });
435
+ expect(second.entries.map((e) => e.info.id)).toEqual(['msg_1']);
436
+ expect(urls[0].searchParams.get('order')).toBe('desc');
437
+ expect(urls[0].searchParams.has('cursor')).toBe(false);
438
+ const cont = urls.filter((url) => url.searchParams.has('cursor'));
439
+ expect(cont.length).toBeGreaterThanOrEqual(1);
440
+ expect(cont.every((url) => !url.searchParams.has('order'))).toBe(true);
441
+ service.close();
442
+ });
443
+
197
444
  it('exposes V2 sharing as unavailable and never reads mirrors to fake a share', async () => {
198
445
  const service = setup();
199
446
  expect(await service.capability()).toMatchObject({ sharingAvailable: false, archiveMetadataAvailable: false, sessionMetadataAvailable: false });
@@ -20,6 +20,8 @@ export const registerAssistantRoutes = (app, dependencies) => {
20
20
  ensureTempDirectory: ensureLlmTempDirectory,
21
21
  // Attachment-session generate may subscribe for session.text.delta (contact harness).
22
22
  globalEventHub: input?.globalEventHub ?? dependencies.globalEventHub,
23
+ persistSessionMetadata: dependencies.persistSessionMetadata,
24
+ onSystemSessionPersisted: dependencies.onSystemSessionPersisted,
23
25
  });
24
26
  const service = createAssistantsService({
25
27
  dbPath: dependencies.dbPath,
@@ -38,6 +40,10 @@ export const registerAssistantRoutes = (app, dependencies) => {
38
40
  onRevisionTip: dependencies.onRevisionTip,
39
41
  onContactTurnEvent: dependencies.onContactTurnEvent,
40
42
  onContactTurnComplete: dependencies.onContactTurnComplete,
43
+ archiveSessionHost: dependencies.archiveSessionHost,
44
+ forgetSessionHost: dependencies.forgetSessionHost,
45
+ persistSessionMetadata: dependencies.persistSessionMetadata,
46
+ onSystemSessionPersisted: dependencies.onSystemSessionPersisted,
41
47
  createChatCompletion: boundCompletion,
42
48
  });
43
49
  setAssignedSessionSettleHandler(({ sessionId, status }) => service.reportAssignedSessionSettle(sessionId, status));