@openchambery/web 2.0.0-beta.3 → 2.0.0-beta.30

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 (292) hide show
  1. package/README.md +14 -0
  2. package/dist/assets/{AgentAvatar-B_JQoreg.js → AgentAvatar-O2x_1cFU.js} +1 -1
  3. package/dist/assets/AssistantView-pBywAsqy.js +3 -0
  4. package/dist/assets/{DiagramView-BNnda8qO.js → DiagramView-B984aE-R.js} +1 -1
  5. package/dist/assets/{FadeInOnReveal-VYQTSAkr.js → FadeInOnReveal-CMBd5BTd.js} +1 -1
  6. package/dist/assets/Icon-BN87CyS4.js +1 -0
  7. package/dist/assets/MarkdownRenderer-DHiNU8n3.js +2 -0
  8. package/dist/assets/{MarkdownRendererImpl-DwrHFGUp.js → MarkdownRendererImpl-xJHMGHju.js} +3 -3
  9. package/dist/assets/MarkstreamRendererImpl-BZflns6x.js +3 -0
  10. package/dist/assets/{MobileDetailNavigation-BXr6AGLT.js → MobileDetailNavigation-DUvxsvrB.js} +1 -1
  11. package/dist/assets/{MobileShareBridge-D7OE9OP_.js → MobileShareBridge-DWo-J3Vc.js} +3 -3
  12. package/dist/assets/MobileSurface-B-Y6U5xm.js +2 -0
  13. package/dist/assets/{MultiRunWindow-DF3iIFCd.js → MultiRunWindow-rYiVSNKK.js} +1 -1
  14. package/dist/assets/{OnboardingScreen-BjHmlwlw.js → OnboardingScreen-BLjZIBOs.js} +1 -1
  15. package/dist/assets/{OverlayScrollbar-DLtWO_Gp.js → OverlayScrollbar-CoZ4wtau.js} +1 -1
  16. package/dist/assets/SettingsView-pjD0LrHE.js +25 -0
  17. package/dist/assets/{SettingsWindow-BI5swspb.js → SettingsWindow-CFOQRwjA.js} +1 -1
  18. package/dist/assets/TerminalView-Dw5jaYkc.js +8 -0
  19. package/dist/assets/{ToolOutputDialog-BEWhBcRd.js → ToolOutputDialog-B4v-ona2.js} +1 -1
  20. package/dist/assets/appThemeRegistry-BqDCGocy.js +1 -0
  21. package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-sG8YQsVs.js} +1 -1
  22. package/dist/assets/{button-DKuz3L8l.js → button-Ck8ipzBm.js} +1 -1
  23. package/dist/assets/collapsible-nU99prkD.js +1 -0
  24. package/dist/assets/context-menu-DMzZN1oi.js +1 -0
  25. package/dist/assets/{desktopHosts-CoDkOnyS.js → desktopHosts-DuftEj8B.js} +1 -1
  26. package/dist/assets/desktopRelayRestore-g378BdZf.js +1 -0
  27. package/dist/assets/{diagram-D7T8HQgj.js → diagram-CvO4P8ds.js} +1 -1
  28. package/dist/assets/dialog-ByGUkS3c.js +1 -0
  29. package/dist/assets/dropdown-menu.styles-ayLWk2gf.js +1 -0
  30. package/dist/assets/es-DCjl24dV.js +15 -0
  31. package/dist/assets/fileMentionTouchSelection-Cgkxwzqb.js +2 -0
  32. package/dist/assets/{fileQueries-JrpcsOCs.js → fileQueries-DuF89nFO.js} +1 -1
  33. package/dist/assets/fr-DfJ5j0t5.js +13 -0
  34. package/dist/assets/{gitApi-CM42qUed.js → gitApi-Dwqkc2nZ.js} +3 -3
  35. package/dist/assets/{imageSource-CIl0FL10.js → imageSource-BpUsiwFd.js} +1 -1
  36. package/dist/assets/{input-CzQy8UhV.js → input-CXhFmHcq.js} +1 -1
  37. package/dist/assets/insertionBoundaries-BhWfAOPY.js +1 -0
  38. package/dist/assets/ja-B_9_w2Xf.js +15 -0
  39. package/dist/assets/ko-DlGbo3kV.js +15 -0
  40. package/dist/assets/main-CGMinhgQ.js +169 -0
  41. package/dist/assets/main-pMpcLJha.js +2 -0
  42. package/dist/assets/{markdownHeightCache-Cj1I5NDw.js → markdownHeightCache-g_UsWzbA.js} +1 -1
  43. package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
  44. package/dist/assets/{mcpQueries-D5mQymgM.js → mcpQueries-DfrW3VHF.js} +1 -1
  45. package/dist/assets/miniChat-YOt30ECc.js +2 -0
  46. package/dist/assets/mobile-BK1bBCYD.js +2 -0
  47. package/dist/assets/multirun-Dua24A69.js +3 -0
  48. package/dist/assets/persistence-Dnj1QOdX.js +3 -0
  49. package/dist/assets/pl-DjUsS8Ei.js +15 -0
  50. package/dist/assets/platform-X3Y3GyM5.js +6 -0
  51. package/dist/assets/projectMeta-DXdoPmbz.js +1 -0
  52. package/dist/assets/pt-BR-B8sV7qo1.js +15 -0
  53. package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-D2XooXKZ.js} +1 -1
  54. package/dist/assets/renderElectronMiniChatApp-BOuEz_oT.js +2 -0
  55. package/dist/assets/renderMobileApp-C8Alpv-B.js +28 -0
  56. package/dist/assets/runtime-fetch-CiqQd__Y.js +1 -0
  57. package/dist/assets/runtime-switch-BJM2k_uH.js +1 -0
  58. package/dist/assets/runtimeConfig-DX_NP2Zm.js +2 -0
  59. package/dist/assets/runtimeEndpointReset-BSQU3e5U.js +148 -0
  60. package/dist/assets/runtimeEndpointReset-DsuP5FPT.css +1 -0
  61. package/dist/assets/sessionChangeSummary-8AIBkExf.js +2 -0
  62. package/dist/assets/{sessionLookup-myHe5ltu.js → sessionLookup-Bdwd1Fit.js} +1 -1
  63. package/dist/assets/{sortable-tabs-strip-BQtmHx9r.js → sortable-tabs-strip-T-dk_L69.js} +1 -1
  64. package/dist/assets/src-OzbnJsph.css +2 -0
  65. package/dist/assets/store-Cuktref-.js +15 -0
  66. package/dist/assets/{terminalApi-BxcdiduX.js → terminalApi-C_4joKUG.js} +1 -1
  67. package/dist/assets/textarea-biAqiqnJ.js +1 -0
  68. package/dist/assets/{tooltip-Cx5gTbdY.js → tooltip-BKL7J5hx.js} +1 -1
  69. package/dist/assets/uk-Bh7HXyLp.js +15 -0
  70. package/dist/assets/{url-CDHnjlsm.js → url-DNRMq_0R.js} +1 -1
  71. package/dist/assets/{useAppFontEffects-CNeo0aZs.js → useAppFontEffects-bafOEpgK.js} +57 -52
  72. package/dist/assets/{useAssistantUIStore-asNG6K7n.js → useAssistantUIStore-DBwfg9nX.js} +1 -1
  73. package/dist/assets/useDesktopWindowControlsLayout-CkCVOkxa.js +1 -0
  74. package/dist/assets/{useEffectiveDirectory-B4LvR8Gt.js → useEffectiveDirectory-B7htpJfU.js} +1 -1
  75. package/dist/assets/{useFontPreferences-CUnQ4TVm.js → useFontPreferences-CMrbTA0r.js} +1 -1
  76. package/dist/assets/useMobileNavigationStore-D2tnhKYV.js +415 -0
  77. package/dist/assets/{useRuntimeAPIs-DwLtO0c_.js → useRuntimeAPIs-DSLXJybW.js} +1 -1
  78. package/dist/assets/useSessionAutoCleanup-BC5OOH3M.js +2 -0
  79. package/dist/assets/{useThemeSystem-BlNUWH47.js → useThemeSystem-DMzWVJaW.js} +1 -1
  80. package/dist/assets/useUIStore-Dfx6TKvi.js +1 -0
  81. package/dist/assets/{useWorkerHighlightedLines-CJZbJDW3.js → useWorkerHighlightedLines-BNQUAMEB.js} +1 -1
  82. package/dist/assets/{vendor-base-ui-CDjyofeD.js → vendor-base-ui-CnHfUmDN.js} +1 -1
  83. package/dist/assets/{vendor-cmdk-BFH0UpXX.js → vendor-cmdk-CeAGfrLo.js} +1 -1
  84. package/dist/assets/{vendor-dnd-kit-accessibility-BrR3AKvM.js → vendor-dnd-kit-accessibility-BY5LhhUB.js} +1 -1
  85. package/dist/assets/{vendor-dnd-kit-core-CNfdvk6H.js → vendor-dnd-kit-core-CvWxUPdb.js} +1 -1
  86. package/dist/assets/{vendor-dnd-kit-sortable-DQUaTeGj.js → vendor-dnd-kit-sortable-hHdP9lLb.js} +1 -1
  87. package/dist/assets/{vendor-framer-motion-4dogsUk3.js → vendor-framer-motion-DFy_BSVd.js} +1 -1
  88. package/dist/assets/{vendor-legendapp-list-BU7MdPZR.js → vendor-legendapp-list-WE32lwNk.js} +1 -1
  89. package/dist/assets/{vendor-markstream-react-qHC2FykU.js → vendor-markstream-react-DX55XLGO.js} +3 -3
  90. package/dist/assets/vendor-opencode-client-MF2GBrFx.js +12 -0
  91. package/dist/assets/{vendor-pierre-diffs-CUfqC003.js → vendor-pierre-diffs-DX7XTPG3.js} +1 -1
  92. package/dist/assets/{vendor-react-C94A0AHl.js → vendor-react-DLPElpoQ.js} +1 -1
  93. package/dist/assets/{vendor-react-drawio-FVlWqKVE.js → vendor-react-drawio-ECtEwvbc.js} +1 -1
  94. package/dist/assets/{vendor-reactuses-core-C2A3EjUi.js → vendor-reactuses-core-Dq5ocTF8.js} +1 -1
  95. package/dist/assets/{vendor-sonner-DNqHmUQs.js → vendor-sonner-DVzioF3d.js} +1 -1
  96. package/dist/assets/{vendor-stream-diffs-CwbKsmlv.js → vendor-stream-diffs-DWy3xSmp.js} +2 -2
  97. package/dist/assets/{vendor-tanstack-react-query-D0iFK57C.js → vendor-tanstack-react-query-D7pys_c9.js} +1 -1
  98. package/dist/assets/{vendor-tanstack-react-virtual-B2kV6cnT.js → vendor-tanstack-react-virtual-tlDgH6PG.js} +1 -1
  99. package/dist/assets/{vendor-zustand-BqLKf6ZY.js → vendor-zustand-DzRqcymP.js} +2 -2
  100. package/dist/assets/zh-CN-Uzia0gk4.js +15 -0
  101. package/dist/assets/zh-TW-CiI7DLdB.js +15 -0
  102. package/dist/index.html +15 -15
  103. package/dist/mini-chat.html +15 -15
  104. package/dist/mobile.html +15 -15
  105. package/package.json +3 -2
  106. package/server/index.js +46 -27
  107. package/server/lib/browser-provider/DOCUMENTATION.md +141 -0
  108. package/server/lib/browser-provider/contract.js +312 -0
  109. package/server/lib/browser-provider/host.js +421 -0
  110. package/server/lib/browser-provider/host.test.js +304 -0
  111. package/server/lib/browser-provider/routes.js +122 -0
  112. package/server/lib/browser-provider/routes.test.js +120 -0
  113. package/server/lib/browser-provider/selection-store.js +43 -0
  114. package/server/lib/browser-provider/selection-store.test.js +35 -0
  115. package/server/lib/event-stream/DOCUMENTATION.md +3 -1
  116. package/server/lib/event-stream/reasoning-projection.js +16 -1
  117. package/server/lib/event-stream/reasoning-projection.test.js +15 -0
  118. package/server/lib/git/DOCUMENTATION.md +1 -0
  119. package/server/lib/git/routes.js +7 -4
  120. package/server/lib/git/routes.test.js +34 -0
  121. package/server/lib/git/service.js +3 -1
  122. package/server/lib/llm/DOCUMENTATION.md +6 -4
  123. package/server/lib/llm/generate.js +8 -9
  124. package/server/lib/llm/generate.test.js +33 -6
  125. package/server/lib/message-queue/service.js +1 -1
  126. package/server/lib/notifications/DOCUMENTATION.md +3 -1
  127. package/server/lib/notifications/runtime.js +128 -31
  128. package/server/lib/notifications/runtime.test.js +103 -0
  129. package/server/lib/notifications/template-runtime.js +130 -38
  130. package/server/lib/notifications/template-runtime.test.js +96 -0
  131. package/server/lib/opencode/DOCUMENTATION.md +36 -19
  132. package/server/lib/opencode/agents.js +13 -22
  133. package/server/lib/opencode/auth-state-runtime.js +3 -3
  134. package/server/lib/opencode/auth-state-runtime.test.js +59 -0
  135. package/server/lib/opencode/auth.d.ts +13 -0
  136. package/server/lib/opencode/auth.js +74 -64
  137. package/server/lib/opencode/auth.test.js +57 -0
  138. package/server/lib/opencode/commands.js +1 -1
  139. package/server/lib/opencode/config-entities-layering.test.js +72 -0
  140. package/server/lib/opencode/config-entity-routes.js +16 -124
  141. package/server/lib/opencode/config-entity-routes.test.js +48 -11
  142. package/server/lib/opencode/config-files.d.ts +17 -0
  143. package/server/lib/opencode/config-files.js +187 -0
  144. package/server/lib/opencode/config-files.test.js +201 -0
  145. package/server/lib/opencode/core-routes.js +0 -1
  146. package/server/lib/opencode/core-routes.test.js +0 -15
  147. package/server/lib/opencode/credential-boundary.d.ts +1 -0
  148. package/server/lib/opencode/credential-boundary.js +11 -0
  149. package/server/lib/opencode/ensure-cli.d.ts +8 -0
  150. package/server/lib/opencode/ensure-cli.js +4 -1
  151. package/server/lib/opencode/env-config.js +4 -1
  152. package/server/lib/opencode/env-config.test.js +28 -0
  153. package/server/lib/opencode/env-runtime.test.js +18 -1
  154. package/server/lib/opencode/feature-routes-runtime.js +19 -7
  155. package/server/lib/opencode/feature-routes-runtime.test.js +7 -0
  156. package/server/lib/opencode/hmr-state-runtime.js +4 -4
  157. package/server/lib/opencode/lifecycle.js +1 -49
  158. package/server/lib/opencode/lifecycle.test.js +30 -4
  159. package/server/lib/opencode/location-release.d.ts +8 -0
  160. package/server/lib/opencode/location-release.js +32 -0
  161. package/server/lib/opencode/location-release.test.js +43 -0
  162. package/server/lib/opencode/mcp.js +1 -1
  163. package/server/lib/opencode/opencode2-pin.js +2 -1
  164. package/server/lib/opencode/owned-runtime-upgrade.test.js +5 -5
  165. package/server/lib/opencode/plugin-routes.test.js +4 -2
  166. package/server/lib/opencode/plugins.js +12 -60
  167. package/server/lib/opencode/plugins.test.js +5 -3
  168. package/server/lib/opencode/providers.js +6 -3
  169. package/server/lib/opencode/proxy.js +13 -1
  170. package/server/lib/opencode/proxy.test.js +31 -3
  171. package/server/lib/opencode/remote-upgrade.integration.test.js +66 -0
  172. package/server/lib/opencode/routes.js +40 -8
  173. package/server/lib/opencode/routes.test.js +51 -10
  174. package/server/lib/opencode/runtime-contract.js +3 -2
  175. package/server/lib/opencode/runtime-contract.test.js +12 -12
  176. package/server/lib/opencode/shared.js +6 -175
  177. package/server/lib/opencode/update-discovery.js +39 -0
  178. package/server/lib/opencode/update-discovery.test.js +39 -0
  179. package/server/lib/opencode/upgrade-screen.d.ts +30 -0
  180. package/server/lib/opencode/upgrade-screen.js +292 -0
  181. package/server/lib/opencode/upgrade-screen.test.js +196 -0
  182. package/server/lib/opencode/version-alignment.test.js +40 -0
  183. package/server/lib/question-auto-delegate/DOCUMENTATION.md +21 -6
  184. package/server/lib/question-auto-delegate/core.d.ts +5 -1
  185. package/server/lib/question-auto-delegate/core.js +21 -13
  186. package/server/lib/question-auto-delegate/form-io.d.ts +6 -0
  187. package/server/lib/question-auto-delegate/form-io.js +39 -0
  188. package/server/lib/question-auto-delegate/runtime.js +4 -24
  189. package/server/lib/question-auto-delegate/runtime.test.js +138 -0
  190. package/server/lib/quota/DOCUMENTATION.md +9 -0
  191. package/server/lib/quota/provider-credentials.test.js +29 -0
  192. package/server/lib/quota/providers/claude.js +4 -4
  193. package/server/lib/quota/providers/codex.js +4 -4
  194. package/server/lib/quota/providers/codex.test.js +1 -1
  195. package/server/lib/quota/providers/copilot.js +5 -5
  196. package/server/lib/quota/providers/google/auth.js +3 -3
  197. package/server/lib/quota/providers/google/index.js +2 -2
  198. package/server/lib/quota/providers/index.js +3 -7
  199. package/server/lib/quota/providers/index.test.js +2 -2
  200. package/server/lib/quota/providers/kimi.js +4 -4
  201. package/server/lib/quota/providers/minimax-shared.js +4 -4
  202. package/server/lib/quota/providers/nanogpt.js +4 -4
  203. package/server/lib/quota/providers/openai.js +4 -4
  204. package/server/lib/quota/providers/openrouter.js +4 -4
  205. package/server/lib/quota/providers/wafer.js +4 -4
  206. package/server/lib/quota/providers/zai.js +4 -4
  207. package/server/lib/quota/providers/zhipuai-coding-plan.js +6 -20
  208. package/server/lib/quota/routes.js +1 -1
  209. package/server/lib/relay/DOCUMENTATION.md +3 -1
  210. package/server/lib/relay/service.js +1 -0
  211. package/server/lib/relay/service.test.js +13 -0
  212. package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -1
  213. package/server/lib/scheduled-tasks/runtime.js +12 -4
  214. package/server/lib/scheduled-tasks/runtime.test.js +81 -6
  215. package/server/lib/session-goal/DOCUMENTATION.md +11 -0
  216. package/server/lib/session-goal/runtime.js +4 -4
  217. package/server/lib/session-goal/runtime.test.js +32 -3
  218. package/server/lib/session-index/DOCUMENTATION.md +7 -0
  219. package/server/lib/session-index/event-ingest.js +13 -0
  220. package/server/lib/session-index/event-ingest.test.js +7 -0
  221. package/server/lib/session-index/service.js +45 -0
  222. package/server/lib/session-index/service.test.js +47 -0
  223. package/server/lib/session-metadata/DOCUMENTATION.md +33 -17
  224. package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
  225. package/server/lib/session-metadata/opencode-session-record.js +316 -0
  226. package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
  227. package/server/lib/session-metadata/routes.js +5 -1
  228. package/server/lib/session-metadata/session-archive.js +1 -1
  229. package/server/lib/session-metadata/session-metadata-store.d.ts +2 -0
  230. package/server/lib/session-metadata/session-metadata-store.js +63 -16
  231. package/server/lib/session-metadata/session-metadata-store.test.js +23 -0
  232. package/server/lib/session-metadata/session-projection.d.ts +1 -0
  233. package/server/lib/session-metadata/session-projection.js +49 -2
  234. package/server/lib/session-metadata/session-projection.test.js +35 -0
  235. package/server/lib/session-title/DOCUMENTATION.md +52 -14
  236. package/server/lib/session-title/native-runtime.test.js +59 -5
  237. package/server/lib/session-title/runtime.js +65 -19
  238. package/server/lib/session-title/session-access.js +108 -3
  239. package/server/lib/session-title/session-access.test.js +125 -7
  240. package/server/lib/tts/DOCUMENTATION.md +7 -5
  241. package/server/lib/tts/credential-source.test.js +23 -0
  242. package/server/lib/tts/routes.js +2 -2
  243. package/server/lib/tts/service.js +16 -37
  244. package/dist/assets/AssistantView-ClQAfGOT.js +0 -3
  245. package/dist/assets/Icon-BiEQEcih.js +0 -1
  246. package/dist/assets/MarkdownRenderer-CJHSsWin.js +0 -2
  247. package/dist/assets/MarkstreamRendererImpl-CSnbQMu3.js +0 -3
  248. package/dist/assets/MobileSurface-D2jUkX9l.js +0 -2
  249. package/dist/assets/SettingsView-BRxELjow.js +0 -25
  250. package/dist/assets/TerminalView-BU3x8-fr.js +0 -8
  251. package/dist/assets/appThemeRegistry-4MR0F7Jz.js +0 -1
  252. package/dist/assets/collapsible-DXbDynN4.js +0 -3
  253. package/dist/assets/context-menu-CqolsLCm.js +0 -1
  254. package/dist/assets/desktopRelayRestore-DDlVKyOE.js +0 -1
  255. package/dist/assets/dialog-DQYl3W_o.js +0 -1
  256. package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +0 -1
  257. package/dist/assets/es-CN3E_zXj.js +0 -15
  258. package/dist/assets/fileMentionTouchSelection-BZ6ovBYH.js +0 -4
  259. package/dist/assets/fr-9zZvQZD2.js +0 -13
  260. package/dist/assets/insertionBoundaries-CtkZ9720.js +0 -1
  261. package/dist/assets/ja-Dnjk_82N.js +0 -15
  262. package/dist/assets/ko-BxCEE99C.js +0 -15
  263. package/dist/assets/main-BILgBI3B.js +0 -169
  264. package/dist/assets/main-BeveiW6F.js +0 -2
  265. package/dist/assets/markdownTheme-CKac3wKC.js +0 -1
  266. package/dist/assets/miniChat-BgrzM3u7.js +0 -2
  267. package/dist/assets/mobile-DtYoQE8C.js +0 -2
  268. package/dist/assets/multirun-jy8dEI96.js +0 -4
  269. package/dist/assets/persistence-C9LQpiOS.js +0 -3
  270. package/dist/assets/pl-CNNsWnPM.js +0 -15
  271. package/dist/assets/platform-BQghHRfp.js +0 -6
  272. package/dist/assets/pt-BR-BZB2DJMp.js +0 -15
  273. package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +0 -2
  274. package/dist/assets/renderMobileApp-u3c4GtFC.js +0 -28
  275. package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
  276. package/dist/assets/runtime-switch-LvWDU-SL.js +0 -1
  277. package/dist/assets/runtimeConfig-BIGBRGQI.js +0 -2
  278. package/dist/assets/runtimeEndpointReset-D2savkuw.js +0 -147
  279. package/dist/assets/sessionChangeSummary-eIbpSfrO.js +0 -2
  280. package/dist/assets/src-CLN7Z-zx.css +0 -2
  281. package/dist/assets/store-DLCiZaA6.js +0 -15
  282. package/dist/assets/textarea-CakKvqFw.js +0 -1
  283. package/dist/assets/uk-rwQrxeYM.js +0 -15
  284. package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +0 -1
  285. package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +0 -414
  286. package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +0 -2
  287. package/dist/assets/useUIStore-CQYKJnrg.js +0 -1
  288. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +0 -12
  289. package/dist/assets/zh-CN-BvmBDnkD.js +0 -15
  290. package/dist/assets/zh-TW-9wcHTX0y.js +0 -15
  291. package/server/lib/opencode/snippets.js +0 -242
  292. package/server/lib/opencode/snippets.test.js +0 -97
@@ -13,6 +13,21 @@ import {
13
13
  } from './reasoning-projection.js';
14
14
 
15
15
  describe('shouldIncludeReasoning / query helpers', () => {
16
+ it('projects v2 list/context and exact snapshots without dropping rows or cursor', () => {
17
+ const row = { id: 'm', type: 'assistant', tokens: { reasoning: 12 }, content: [
18
+ { type: 'reasoning', text: 'hidden' }, { type: 'text', text: 'visible' },
19
+ ] };
20
+ const payload = { data: [row], cursor: { next: 'older' } };
21
+ const projected = projectMessagesPayloadForReasoning(payload, false);
22
+ expect(projected).toEqual({ data: [{ ...row, content: [row.content[1]] }], cursor: payload.cursor });
23
+ expect(projectMessagesPayloadForReasoning(row, false)).toEqual(projected.data[0]);
24
+ expect(projectMessagesPayloadForReasoning(payload, true)).toBe(payload);
25
+ expect(row.content).toHaveLength(2);
26
+ const filter = createReasoningOutboundFilter();
27
+ expect(filter.projectEvent({ type: 'session.reasoning.delta.1', data: { delta: 'hidden' } })).toBeNull();
28
+ const text = { type: 'session.text.delta', data: { delta: 'visible' } };
29
+ expect(filter.projectEvent(text)).toBe(text);
30
+ });
16
31
  it('only treats the strict string false as disabled', () => {
17
32
  expect(shouldIncludeReasoning(undefined)).toBe(true);
18
33
  expect(shouldIncludeReasoning(null)).toBe(true);
@@ -54,6 +54,7 @@ The following functions are exported and used by the web server:
54
54
  - `validateWorktreeCreate(directory, input)`: Validate worktree creation parameters (mode, branchName, startRef, upstream config).
55
55
  - `createWorktree(directory, input)`: Create a new worktree (supports 'new' and 'existing' modes, upstream setup).
56
56
  - `removeWorktree(directory, input)`: Remove a worktree (optionally delete local branch).
57
+ - `removeWorktree(directory, input, { beforeRemove })`: optional host hook after primary-workspace/catalog validation, immediately before removal of a matched worktree or managed orphan. The hook never runs for unrelated orphan paths. Production hosts use the official v2 `debug.location.evict` with the validated target and current upstream URL/auth; the 2s budget returns `released`, `failed`, `timeout`, or `unavailable`. Eviction is best effort and never counts as successful Git removal. DELETE responses include `locationRelease` when attempted. Shared-service explicit removal retains the existing force-delete semantics for that target; it is not automatic idle cleanup and does not interrupt unrelated sessions. An eviction acknowledgement is cache detachment, not a guarantee that active borrowers released OS handles. Queue rollback and topology publication remain tied to the Git result.
57
58
  - `isLinkedWorktree(directory)`: Check if directory is a linked worktree (not primary).
58
59
 
59
60
  ### Commit and Remote Operations
@@ -1,4 +1,4 @@
1
- export function registerGitRoutes(app, { messageQueueService = null, createRequestID = () => globalThis.crypto.randomUUID(), broadcastWorktreeTopologyChanged = () => {} } = {}) {
1
+ export function registerGitRoutes(app, { messageQueueService = null, createRequestID = () => globalThis.crypto.randomUUID(), broadcastWorktreeTopologyChanged = () => {}, releaseWorktreeLocation } = {}) {
2
2
  const normalizeWorktreePath = (value) => typeof value === 'string' ? value.trim().replace(/\\/g, '/').replace(/\/+$/, '') : '';
3
3
  const lifecycleOperations = new Map();
4
4
  const acquireWorktreeLifecycleOperation = (runtimeKey, directory) => {
@@ -1282,11 +1282,14 @@ export function registerGitRoutes(app, { messageQueueService = null, createReque
1282
1282
  }
1283
1283
 
1284
1284
  let result;
1285
+ let locationRelease;
1285
1286
  try {
1286
1287
  result = await removeWorktree(directory, {
1287
1288
  directory: worktreeDirectory,
1288
1289
  deleteLocalBranch: req.body?.deleteLocalBranch === true,
1289
- });
1290
+ }, ...(releaseWorktreeLocation ? [{ beforeRemove: async (target) => {
1291
+ locationRelease = await releaseWorktreeLocation(target);
1292
+ } }] : []));
1290
1293
  } catch (error) {
1291
1294
  if (messageQueueService) {
1292
1295
  try {
@@ -1328,7 +1331,7 @@ export function registerGitRoutes(app, { messageQueueService = null, createReque
1328
1331
  console.warn('Failed to roll back message queue worktree deletion');
1329
1332
  }
1330
1333
  }
1331
- return res.json({ success: false });
1334
+ return res.json({ success: false, ...(locationRelease ? { locationRelease } : {}) });
1332
1335
  }
1333
1336
 
1334
1337
  notifyWorktreeTopologyChanged(directory, worktreeDirectory, 'removed');
@@ -1346,7 +1349,7 @@ export function registerGitRoutes(app, { messageQueueService = null, createReque
1346
1349
  return res.status(500).json({ error: 'Failed to commit worktree queue deletion' });
1347
1350
  }
1348
1351
  }
1349
- res.json({ success: true });
1352
+ res.json({ success: true, ...(locationRelease ? { locationRelease } : {}) });
1350
1353
  } finally {
1351
1354
  releaseWorktreeLifecycleOperation(operation);
1352
1355
  }
@@ -69,6 +69,40 @@ const createMockResponse = () => {
69
69
  };
70
70
  };
71
71
 
72
+ describe('worktree eviction best effort', () => {
73
+ it.each(['released', 'failed', 'timeout', 'unavailable'])('preserves successful deletion with release outcome %s', async (state) => {
74
+ const { app, getRoute } = createRouteRegistry();
75
+ const releaseWorktreeLocation = vi.fn(async () => ({ state }));
76
+ const broadcastWorktreeTopologyChanged = vi.fn();
77
+ registerGitRoutes(app, { releaseWorktreeLocation, broadcastWorktreeTopologyChanged });
78
+ gitLibraries.removeWorktree.mockImplementation(async (_root, _input, options) => {
79
+ await options.beforeRemove('/validated/worktree');
80
+ return true;
81
+ });
82
+ const response = createMockResponse();
83
+ await getRoute('DELETE', '/api/git/worktrees')({ query: { directory: '/repo' }, body: { directory: '/requested/worktree' } }, response);
84
+ expect(releaseWorktreeLocation).toHaveBeenCalledWith('/validated/worktree');
85
+ expect(response.body).toEqual({ success: true, locationRelease: { state } });
86
+ expect(broadcastWorktreeTopologyChanged).toHaveBeenCalledTimes(1);
87
+ });
88
+ it('does not publish topology when removal fails after successful release', async () => {
89
+ const { app, getRoute } = createRouteRegistry();
90
+ const broadcastWorktreeTopologyChanged = vi.fn();
91
+ registerGitRoutes(app, { releaseWorktreeLocation: async () => ({ state: 'released' }), broadcastWorktreeTopologyChanged });
92
+ gitLibraries.removeWorktree.mockImplementation(async (_root, _input, options) => {
93
+ await options.beforeRemove('/repo/worktree');
94
+ throw new Error('deletion failed');
95
+ });
96
+ const response = createMockResponse();
97
+ const log = vi.spyOn(console, 'error').mockImplementation(() => {});
98
+ try {
99
+ await getRoute('DELETE', '/api/git/worktrees')({ query: { directory: '/repo' }, body: { directory: '/repo/worktree' } }, response);
100
+ } finally { log.mockRestore(); }
101
+ expect(response.statusCode).toBe(500);
102
+ expect(broadcastWorktreeTopologyChanged).not.toHaveBeenCalled();
103
+ });
104
+ });
105
+
72
106
  describe('git routes batch discover', () => {
73
107
  beforeEach(() => {
74
108
  gitLibraries.isGitRepository.mockReset();
@@ -3851,7 +3851,7 @@ export async function getWorktreeBootstrapStatus(directory) {
3851
3851
  };
3852
3852
  }
3853
3853
 
3854
- export async function removeWorktree(directory, input = {}) {
3854
+ export async function removeWorktree(directory, input = {}, { beforeRemove } = {}) {
3855
3855
  const targetDirectory = normalizeDirectoryPath(input?.directory);
3856
3856
  if (!targetDirectory) {
3857
3857
  throw new Error('Worktree directory is required');
@@ -3887,6 +3887,7 @@ export async function removeWorktree(directory, input = {}) {
3887
3887
 
3888
3888
  const targetExists = await checkPathExists(targetDirectory);
3889
3889
  if (targetExists && isManagedOrphan) {
3890
+ await beforeRemove?.(targetDirectory);
3890
3891
  await fsp.rm(targetDirectory, { recursive: true, force: true });
3891
3892
  }
3892
3893
 
@@ -3901,6 +3902,7 @@ export async function removeWorktree(directory, input = {}) {
3901
3902
  return true;
3902
3903
  }
3903
3904
 
3905
+ await beforeRemove?.(matchedEntry.worktree);
3904
3906
  await runGitCommandOrThrow(
3905
3907
  context.primaryWorktree,
3906
3908
  ['worktree', 'remove', '--force', matchedEntry.worktree],
@@ -39,8 +39,8 @@ call Anthropic/OpenAI/plugin SDKs, and does not use the `openai` npm package.
39
39
  `openchamber-text` agent (same temp root, same deny-all verification and
40
40
  `session.create` / `session.remove` lifecycle as the attachment path below):
41
41
 
42
- 1. Ensure `.opencode/agent/openchamber-text.md` and verify its final
43
- permission rule is deny-all before creating the session.
42
+ 1. Ensure `.opencode/agent/openchamber-text.md` and verify its last catch-all
43
+ permission rule is deny-all, with only deny rules after it, before creating the session.
44
44
  2. `session.create({ location, agent: 'openchamber-text', model, metadata })`.
45
45
  3. `session.generate({ sessionID, prompt })` → `{ text }`. One model call with
46
46
  the session's provider context, no tool loop, no messages written. System
@@ -62,8 +62,10 @@ data-URL file parts:
62
62
 
63
63
  1. Ensure the throwaway temp directory agent markdown (`openchamber-llm`,
64
64
  permissions array ending in `action:* resource:* effect:deny`).
65
- 2. `agent.get({ agentID: 'openchamber-llm', location })` and require the **final**
66
- permissions rule to be deny-all. Failure → `llm_attachment_generation_unavailable`
65
+ 2. `agent.get({ agentID: 'openchamber-llm', location })` and require the **last catch-all**
66
+ permissions rule to be deny-all, with only deny rules after it. This accepts
67
+ OpenCode's appended `browser` deny while rejecting trailing allow/ask rules.
68
+ Failure → `llm_attachment_generation_unavailable`
67
69
  **before** any `session.prompt`. Title is never treated as permission isolation.
68
70
  3. `session.create({ location, agent, model: { id, providerID, variant? },
69
71
  metadata: { openchamber: { llm: { purpose: 'chat-completions' } } } })`
@@ -18,7 +18,7 @@ const positiveMs = (value, fallback) => (
18
18
 
19
19
  /**
20
20
  * Agent frontmatter for official client agent permissions (action/resource/effect).
21
- * The final rule must be deny-all; assertLlmAgentDenyAll verifies that before prompt.
21
+ * The final catch-all must deny, with only denies after it; verified before prompt.
22
22
  */
23
23
  const AGENT_MARKDOWN = `---
24
24
  mode: primary
@@ -308,7 +308,7 @@ const readGeneratorAgent = async ({ client, location, signal, agentID, waitMs, p
308
308
  };
309
309
 
310
310
  /**
311
- * Verify the generator agent ends with deny-all permissions.
311
+ * Verify the generator agent has a deny-all rule followed only by denies.
312
312
  * Title alone is never treated as isolation. Failure must happen before prompt.
313
313
  */
314
314
  export async function assertLlmAgentDenyAll({
@@ -336,13 +336,12 @@ export async function assertLlmAgentDenyAll({
336
336
  'llm_attachment_generation_unavailable',
337
337
  );
338
338
  }
339
- const last = permissions[permissions.length - 1];
340
- if (
341
- !last
342
- || last.action !== '*'
343
- || last.resource !== '*'
344
- || last.effect !== 'deny'
345
- ) {
339
+ const lastCatchAll = permissions.findLastIndex(
340
+ (rule) => rule?.action === '*' && rule?.resource === '*',
341
+ );
342
+ const isolated = lastCatchAll > -1
343
+ && permissions.slice(lastCatchAll).every((rule) => rule?.effect === 'deny');
344
+ if (!isolated) {
346
345
  failGenerate(
347
346
  'OpenCode LLM agent final permission is not deny-all; generation blocked',
348
347
  'llm_attachment_generation_unavailable',
@@ -127,8 +127,16 @@ function textSessionClient({ generate = vi.fn(async () => ({ text: 'reply' })),
127
127
  }
128
128
 
129
129
  describe('generateOpenCodeText — text path', () => {
130
- it('runs session.generate in a verified deny-all throwaway session and removes it', async () => {
131
- const { client, agentGet, create, generate, remove, prompt } = textSessionClient()
130
+ it.each([
131
+ { name: 'deny-all alone', suffix: [] },
132
+ { name: 'appended browser deny', suffix: [{ action: 'browser', resource: '*', effect: 'deny' }] },
133
+ ])('runs session.generate in a verified throwaway session ($name) and removes it', async ({ suffix }) => {
134
+ const { client, agentGet, create, generate, remove, prompt } = textSessionClient({
135
+ agentGet: vi.fn(async () => ({
136
+ ...denyAllTextAgent,
137
+ data: { ...denyAllTextAgent.data, permissions: [...denyAllTextAgent.data.permissions, ...suffix] },
138
+ })),
139
+ })
132
140
  const ensureTempDirectory = vi.fn(async ({ agentName, agentMarkdown }) => {
133
141
  expect(agentName).toBe(_test.TEXT_AGENT_NAME)
134
142
  expect(agentMarkdown).toBe(_test.TEXT_AGENT_MARKDOWN)
@@ -188,10 +196,23 @@ describe('generateOpenCodeText — text path', () => {
188
196
  }
189
197
  })
190
198
 
191
- it('blocks before creating a session when the text agent is not deny-all', async () => {
199
+ it.each([
200
+ { name: 'no catch-all', permissions: [{ action: 'browser', resource: '*', effect: 'deny' }] },
201
+ ...['allow', 'ask'].flatMap((effect) => ['*', 'edit'].map((action) => ({
202
+ name: `trailing ${action} ${effect} before browser deny`,
203
+ permissions: [
204
+ ...denyAllTextAgent.data.permissions,
205
+ { action, resource: '*', effect },
206
+ { action: 'browser', resource: '*', effect: 'deny' },
207
+ ],
208
+ }))),
209
+ { name: 'missing permissions', permissions: undefined },
210
+ { name: 'empty permissions', permissions: [] },
211
+ { name: 'invalid trailing rule', permissions: [...denyAllTextAgent.data.permissions, null] },
212
+ ])('blocks before creating a session: $name', async ({ permissions }) => {
192
213
  const { client, create, generate } = textSessionClient({
193
214
  agentGet: vi.fn(async () => ({
194
- data: { id: 'openchamber-text', permissions: [{ action: 'bash', resource: '*', effect: 'allow' }] },
215
+ data: { id: 'openchamber-text', permissions },
195
216
  })),
196
217
  })
197
218
  await expect(generateOpenCodeText({
@@ -316,8 +337,14 @@ describe('generateOpenCodeText — text path', () => {
316
337
  })
317
338
 
318
339
  describe('generateOpenCodeText — attachment session path', () => {
319
- it('verifies deny-all before prompt, prompts with files, waits, lists, and removes', async () => {
320
- const agentGet = vi.fn(async () => denyAllAgent)
340
+ it.each([
341
+ { name: 'deny-all alone', suffix: [] },
342
+ { name: 'appended browser deny', suffix: [{ action: 'browser', resource: '*', effect: 'deny' }] },
343
+ ])('verifies deny-all ($name) before prompt, prompts with files, waits, lists, and removes', async ({ suffix }) => {
344
+ const agentGet = vi.fn(async () => ({
345
+ ...denyAllAgent,
346
+ data: { ...denyAllAgent.data, permissions: [...denyAllAgent.data.permissions, ...suffix] },
347
+ }))
321
348
  const create = vi.fn(async () => ({ id: 'ses_tmp' }))
322
349
  const put = vi.fn(async () => undefined)
323
350
  const prompt = vi.fn(async () => ({ id: 'inbox_1', type: 'user' }))
@@ -246,7 +246,7 @@ export const createMessageQueueService = ({ dbPath, getRuntimeConfig = () => nul
246
246
  totalPasteLength += reference.text.length;
247
247
  if (totalPasteLength > MAX_COMPOSER_PASTE_TOTAL_LENGTH) fail('validation_error');
248
248
  canonical += reference.text;
249
- } else if (reference.kind === 'skill' && Object.keys(reference).every((key) => ['id', 'kind', 'start', 'end', 'display', 'skillName'].includes(key)) && nonEmptyString(reference.skillName, MAX_COMPOSER_SKILL_NAME) && /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(reference.skillName) && (reference.display === `/${reference.skillName}` || reference.display === `/\u2003${reference.skillName}`)) canonical += `[skill:${reference.skillName}]`;
249
+ } else if (reference.kind === 'skill' && Object.keys(reference).every((key) => ['id', 'kind', 'start', 'end', 'display', 'skillName'].includes(key)) && nonEmptyString(reference.skillName, MAX_COMPOSER_SKILL_NAME) && /^[^\s/\\[\]\r\n]+$/u.test(reference.skillName) && [`/${reference.skillName}`, `/\u2003${reference.skillName}`, `@${reference.skillName}`, `@\u2003${reference.skillName}`].includes(reference.display)) canonical += `[skill:${reference.skillName}]`;
250
250
  else if (reference.kind === 'command' && Object.keys(reference).every((key) => ['id', 'kind', 'start', 'end', 'display', 'commandName', 'reference'].includes(key)) && nonEmptyString(reference.commandName, MAX_COMPOSER_COMMAND_NAME) && /^[\p{L}\p{N}._/-]+$/u.test(reference.commandName) && nonEmptyString(reference.reference, MAX_COMPOSER_COMMAND_REFERENCE) && !/[\]\r\n]/u.test(reference.reference) && (reference.display === `/${reference.commandName}` || reference.display === `/\u2003${reference.commandName}`)) canonical += `[command:${reference.reference}]`;
251
251
  else fail('validation_error');
252
252
  previousEnd = reference.end;
@@ -107,7 +107,9 @@ This module provides notification message preparation utilities for the web serv
107
107
  - `extractLastMessageText(payload, maxLength?)`
108
108
  - `fetchLastAssistantMessageText(sessionId, messageId, maxLength?)`
109
109
  - `maybeCacheSessionInfoFromEvent(payload)`
110
- - `buildTemplateVariables(payload, sessionId)`
110
+ - `buildTemplateVariables(payload, sessionId)` — `session_name` is the session title. OpenCode 2 returns that title on `GET /api/session/:id` as `{ data: { title } }` and on `session.created` / `session.renamed` as `data.title`. Legacy `properties.info.title` still counts. An empty title stays empty so push can fall back to the localized "Session" label. `agent_name` / `model_name` fall back to that same session record when the triggering event has none. `fetchLastAssistantMessageText` accepts a legacy message array and an OpenCode 2 `{ data }` page (`order=desc`).
111
+ - Question prompts come from legacy `question.asked` or OpenCode 2 `form.created` (`data.form`). Permission prompts read `permission.asked` from `properties` or `data` (`action`, `message`, `id`). `form.replied` / `form.cancelled` cancel a pending question notification the same way `permission.replied` cancels a permission notification.
112
+ - iOS APNs and Android FCM share this payload: native banners stay scenario title + session name. Desktop, in-app, and web-push use the resolved template title and body.
111
113
  - `getCachedZenModels()`
112
114
 
113
115
  ## Constants
@@ -1,3 +1,41 @@
1
+ import { readNotificationSessionRecord } from './template-runtime.js';
2
+
3
+ const trimmedText = (value) => (typeof value === 'string' && value.trim() ? value.trim() : '');
4
+
5
+ const readNotificationEventFields = (payload) => {
6
+ const properties = payload?.properties && typeof payload.properties === 'object' && !Array.isArray(payload.properties)
7
+ ? payload.properties
8
+ : {};
9
+ const data = payload?.data && typeof payload.data === 'object' && !Array.isArray(payload.data)
10
+ ? payload.data
11
+ : {};
12
+ const form = (properties.form && typeof properties.form === 'object' ? properties.form : null)
13
+ || (data.form && typeof data.form === 'object' ? data.form : null);
14
+ const questions = Array.isArray(properties.questions)
15
+ ? properties.questions
16
+ : (Array.isArray(data.questions) ? data.questions : []);
17
+ const firstQuestion = questions[0] && typeof questions[0] === 'object' ? questions[0] : null;
18
+ const field = Array.isArray(form?.fields)
19
+ ? form.fields.find((item) => item && typeof item === 'object' && item.hidden !== true)
20
+ : null;
21
+ return {
22
+ requestId: trimmedText(properties.id)
23
+ || trimmedText(properties.requestID)
24
+ || trimmedText(properties.requestId)
25
+ || trimmedText(data.id)
26
+ || trimmedText(data.requestID)
27
+ || trimmedText(data.requestId)
28
+ || trimmedText(form?.id),
29
+ permission: trimmedText(properties.permission) || trimmedText(data.permission) || trimmedText(data.action),
30
+ message: trimmedText(data.message) || trimmedText(properties.message),
31
+ header: trimmedText(firstQuestion?.header) || trimmedText(field?.title) || trimmedText(form?.title),
32
+ questionText: trimmedText(firstQuestion?.question)
33
+ || trimmedText(field?.description)
34
+ || trimmedText(field?.title)
35
+ || trimmedText(form?.title),
36
+ };
37
+ };
38
+
1
39
  export const createNotificationTriggerRuntime = (deps) => {
2
40
  const {
3
41
  readSettingsFromDisk,
@@ -255,14 +293,14 @@ export const createNotificationTriggerRuntime = (deps) => {
255
293
  if (!response.ok) {
256
294
  return cached;
257
295
  }
258
- const session = await response.json().catch(() => null);
259
- if (!session || typeof session !== 'object') {
296
+ const session = readNotificationSessionRecord(await response.json().catch(() => null));
297
+ if (!session) {
260
298
  return cached;
261
299
  }
262
300
 
263
301
  const meta = {
264
302
  parentID: normalizeParentID(session.parentID),
265
- title: typeof session.title === 'string' ? session.title : null,
303
+ title: trimmedText(session.title) || null,
266
304
  complete: true,
267
305
  ...metaFromOpenchamber(session.metadata?.openchamber),
268
306
  };
@@ -346,13 +384,24 @@ export const createNotificationTriggerRuntime = (deps) => {
346
384
 
347
385
  const extractSessionIdFromPayload = (payload) => {
348
386
  if (!payload || typeof payload !== 'object') return null;
349
- const props = payload.properties;
387
+ const props = payload.properties && typeof payload.properties === 'object'
388
+ ? payload.properties
389
+ : (payload.data && typeof payload.data === 'object' ? payload.data : null);
350
390
  const info = props?.info;
391
+ const data = payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)
392
+ ? payload.data
393
+ : null;
351
394
  const sessionId =
352
395
  info?.sessionID ??
353
396
  info?.sessionId ??
354
397
  props?.sessionID ??
355
398
  props?.sessionId ??
399
+ props?.form?.sessionID ??
400
+ props?.form?.sessionId ??
401
+ data?.sessionID ??
402
+ data?.sessionId ??
403
+ data?.form?.sessionID ??
404
+ data?.form?.sessionId ??
356
405
  props?.session ??
357
406
  null;
358
407
  return typeof sessionId === 'string' && sessionId.length > 0 ? sessionId : null;
@@ -360,11 +409,32 @@ export const createNotificationTriggerRuntime = (deps) => {
360
409
 
361
410
  const extractDirectoryFromPayload = (payload) => {
362
411
  if (!payload || typeof payload !== 'object') return undefined;
363
- const props = payload.properties;
364
- const directory = props?.directory ?? props?.info?.directory;
365
- if (typeof directory !== 'string') return undefined;
366
- const trimmed = directory.trim();
367
- return trimmed.length > 0 ? trimmed : undefined;
412
+ const props = payload.properties && typeof payload.properties === 'object' ? payload.properties : {};
413
+ const data = payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data) ? payload.data : {};
414
+ const info = props.info && typeof props.info === 'object' ? props.info : {};
415
+ const dataInfo = data.info && typeof data.info === 'object' ? data.info : {};
416
+ const candidates = [
417
+ props.directory,
418
+ info.directory,
419
+ info.location?.directory,
420
+ payload.location?.directory,
421
+ data.directory,
422
+ data.location?.directory,
423
+ dataInfo.directory,
424
+ dataInfo.location?.directory,
425
+ ];
426
+ for (const value of candidates) {
427
+ const directory = trimmedText(value);
428
+ if (directory) return directory;
429
+ }
430
+ return undefined;
431
+ };
432
+
433
+ const resolvePushSessionName = async (variables, sessionId, directory) => {
434
+ const fromTemplate = trimmedText(variables?.session_name);
435
+ if (fromTemplate) return fromTemplate;
436
+ const meta = await fetchSessionMeta(sessionId, directory);
437
+ return trimmedText(meta?.title);
368
438
  };
369
439
 
370
440
  // A session with an ACTIVE goal suppresses per-turn ready notifications;
@@ -381,7 +451,7 @@ export const createNotificationTriggerRuntime = (deps) => {
381
451
  signal: AbortSignal.timeout(2000),
382
452
  });
383
453
  if (!response.ok) return false;
384
- const session = await response.json().catch(() => null);
454
+ const session = readNotificationSessionRecord(await response.json().catch(() => null));
385
455
  const goal = session?.metadata?.openchamber?.goal;
386
456
  return Boolean(goal && typeof goal === 'object' && goal.status === 'active');
387
457
  } catch {
@@ -417,20 +487,31 @@ export const createNotificationTriggerRuntime = (deps) => {
417
487
  const reason = payload.properties?.reason ?? payload.data?.reason;
418
488
  if (reason === 'shutdown') return;
419
489
  }
420
- if ((payload.type === 'session.idle' || payload.type === 'session.error') && sessionId) {
421
- const error = payload.properties?.error;
490
+ // v2 completion is session.execution.succeeded. session.idle is a deprecated
491
+ // fallback; the ready-notification cooldown below collapses the pair.
492
+ if ((
493
+ payload.type === 'session.idle'
494
+ || payload.type === 'session.execution.succeeded'
495
+ || payload.type === 'session.error'
496
+ || payload.type === 'session.execution.failed'
497
+ ) && sessionId) {
498
+ const body = payload.properties && typeof payload.properties === 'object'
499
+ ? payload.properties
500
+ : (payload.data && typeof payload.data === 'object' ? payload.data : {});
501
+ const error = body?.error;
422
502
  const errorText = typeof error?.message === 'string'
423
503
  ? error.message
424
504
  : typeof error === 'string' ? error : '';
505
+ const failed = payload.type === 'session.error' || payload.type === 'session.execution.failed';
425
506
  await maybeSendPushForTrigger({
426
507
  ...payload,
427
508
  type: 'message.updated',
428
509
  properties: {
429
- ...payload.properties,
510
+ ...(payload.properties && typeof payload.properties === 'object' ? payload.properties : body),
430
511
  info: {
431
512
  sessionID: sessionId,
432
513
  role: 'assistant',
433
- finish: payload.type === 'session.error' ? 'error' : 'stop',
514
+ finish: failed ? 'error' : 'stop',
434
515
  ...(errorText ? { parts: [{ type: 'text', text: errorText }] } : {}),
435
516
  },
436
517
  },
@@ -492,12 +573,12 @@ export const createNotificationTriggerRuntime = (deps) => {
492
573
  const completionTemplate = templates.completion || { title: 'Task completed', message: '{session_name}' };
493
574
 
494
575
  const variables = await buildTemplateVariables(payload, sessionId);
495
- sessionName = typeof variables.session_name === 'string' ? variables.session_name : sessionName;
576
+ sessionName = await resolvePushSessionName(variables, sessionId, notificationDirectory);
496
577
 
497
578
  const messageId = info?.id;
498
579
  let lastMessage = extractLastMessageText(payload);
499
580
  if (!lastMessage) {
500
- lastMessage = await fetchLastAssistantMessageText(sessionId, messageId);
581
+ lastMessage = await fetchLastAssistantMessageText(sessionId, messageId, undefined, notificationDirectory);
501
582
  }
502
583
 
503
584
  variables.last_message = await prepareNotificationLastMessage({
@@ -548,7 +629,21 @@ export const createNotificationTriggerRuntime = (deps) => {
548
629
  return;
549
630
  }
550
631
 
551
- if (payload.type === 'question.asked' && sessionId) {
632
+ if ((
633
+ payload.type === 'form.replied'
634
+ || payload.type === 'form.cancelled'
635
+ || payload.type === 'question.replied'
636
+ || payload.type === 'question.rejected'
637
+ ) && sessionId) {
638
+ const existingTimer = pushQuestionDebounceTimers.get(sessionId);
639
+ if (existingTimer) {
640
+ clearTimeout(existingTimer);
641
+ pushQuestionDebounceTimers.delete(sessionId);
642
+ }
643
+ return;
644
+ }
645
+
646
+ if ((payload.type === 'question.asked' || payload.type === 'form.created') && sessionId) {
552
647
  if (await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)) {
553
648
  return;
554
649
  }
@@ -569,9 +664,9 @@ export const createNotificationTriggerRuntime = (deps) => {
569
664
  return;
570
665
  }
571
666
 
572
- const firstQuestion = payload.properties?.questions?.[0];
573
- const header = typeof firstQuestion?.header === 'string' ? firstQuestion.header.trim() : '';
574
- const questionText = typeof firstQuestion?.question === 'string' ? firstQuestion.question.trim() : '';
667
+ const prompt = readNotificationEventFields(payload);
668
+ const header = prompt.header;
669
+ const questionText = prompt.questionText;
575
670
 
576
671
  let title = 'Needs your answer';
577
672
  let body = '';
@@ -579,7 +674,7 @@ export const createNotificationTriggerRuntime = (deps) => {
579
674
 
580
675
  try {
581
676
  const variables = await buildTemplateVariables(payload, sessionId);
582
- sessionName = typeof variables.session_name === 'string' ? variables.session_name : sessionName;
677
+ sessionName = await resolvePushSessionName(variables, sessionId, notificationDirectory);
583
678
  variables.last_message = questionText || header || '';
584
679
 
585
680
  const templates = settings.notificationTemplates || {};
@@ -630,8 +725,8 @@ export const createNotificationTriggerRuntime = (deps) => {
630
725
  }
631
726
 
632
727
  if (payload.type === 'permission.replied' && sessionId) {
633
- const requestId = payload.properties?.requestID ?? payload.properties?.requestId ?? payload.properties?.id;
634
- const requestKey = typeof requestId === 'string' ? `${sessionId}:${requestId}` : null;
728
+ const requestId = readNotificationEventFields(payload).requestId;
729
+ const requestKey = requestId ? `${sessionId}:${requestId}` : null;
635
730
  const pendingNotification = pushPermissionDebounceTimers.get(sessionId);
636
731
  if (!pendingNotification) {
637
732
  return;
@@ -651,9 +746,10 @@ export const createNotificationTriggerRuntime = (deps) => {
651
746
  if (await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)) {
652
747
  return;
653
748
  }
654
- const requestId = payload.properties?.id ?? payload.properties?.requestID ?? payload.properties?.requestId;
655
- const permission = payload.properties?.permission;
656
- const requestKey = typeof requestId === 'string' ? `${sessionId}:${requestId}` : null;
749
+ const permissionFields = readNotificationEventFields(payload);
750
+ const requestId = permissionFields.requestId;
751
+ const permission = permissionFields.permission;
752
+ const requestKey = requestId ? `${sessionId}:${requestId}` : null;
657
753
  if (requestKey && notifiedPermissionRequests.has(requestKey)) {
658
754
  return;
659
755
  }
@@ -691,11 +787,12 @@ export const createNotificationTriggerRuntime = (deps) => {
691
787
  return;
692
788
  }
693
789
 
694
- const sessionTitle = payload.properties?.sessionTitle;
790
+ const sessionTitle = payload.properties?.sessionTitle ?? payload.data?.sessionTitle;
695
791
  const permissionText = typeof permission === 'string' && permission.length > 0 ? permission : '';
792
+ const detail = permissionFields.message;
696
793
  const fallbackMessage = typeof sessionTitle === 'string' && sessionTitle.trim().length > 0
697
794
  ? sessionTitle.trim()
698
- : permissionText || 'Agent is waiting for your approval';
795
+ : detail || permissionText || 'Agent is waiting for your approval';
699
796
 
700
797
  let title = 'Needs permission';
701
798
  let body = '';
@@ -703,7 +800,7 @@ export const createNotificationTriggerRuntime = (deps) => {
703
800
 
704
801
  try {
705
802
  const variables = await buildTemplateVariables(payload, sessionId);
706
- sessionName = typeof variables.session_name === 'string' ? variables.session_name : sessionName;
803
+ sessionName = await resolvePushSessionName(variables, sessionId, notificationDirectory);
707
804
  variables.last_message = fallbackMessage;
708
805
 
709
806
  const templates = settings.notificationTemplates || {};
@@ -771,8 +868,8 @@ export const createNotificationTriggerRuntime = (deps) => {
771
868
  signal: AbortSignal.timeout(2000),
772
869
  });
773
870
  if (response.ok) {
774
- const session = await response.json().catch(() => null);
775
- if (typeof session?.title === 'string') sessionName = session.title.trim();
871
+ const session = readNotificationSessionRecord(await response.json().catch(() => null));
872
+ sessionName = trimmedText(session?.title);
776
873
  }
777
874
  } catch {
778
875
  // Session name is presentation sugar for the mobile push — never block on it.