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

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 (296) 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-pLaqWWmB.js} +1 -1
  7. package/dist/assets/AssistantView-C4GkABnI.js +3 -0
  8. package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-ByYLIfWo.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-MuJAzJq_.js} +1 -1
  10. package/dist/assets/Icon-Dz2kO9RN.js +1 -0
  11. package/dist/assets/MarkdownRenderer-CPU6kSHd.js +2 -0
  12. package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-2hw5cdqQ.js} +3 -3
  13. package/dist/assets/MarkstreamRendererImpl-DXZQkLTa.js +3 -0
  14. package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-C9BzieQY.js} +1 -1
  15. package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-2_iOfzqO.js} +2 -2
  16. package/dist/assets/MobileSurface-DNVA7K_1.js +2 -0
  17. package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-QA6pMrJ9.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-XWkr7MjT.js} +1 -1
  19. package/dist/assets/OverlayScrollbar-DCWyGseO.js +2 -0
  20. package/dist/assets/SettingsView-dWuu0VIb.js +25 -0
  21. package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-CetgiJMX.js} +1 -1
  22. package/dist/assets/TerminalView-BuXuo7gm.js +8 -0
  23. package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-DNSSOaCt.js} +2 -2
  24. package/dist/assets/appThemeRegistry-Cb7qFBsc.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-Doq9FI1I.js} +1 -1
  27. package/dist/assets/{collapsible-BXRd5_Hr.js → collapsible-CYUeu3_N.js} +1 -1
  28. package/dist/assets/{context-menu-jGtH6fR9.js → context-menu-C8kOMVtC.js} +1 -1
  29. package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-DbxnIQdS.js} +1 -1
  30. package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-dvxF4V2p.js} +1 -1
  31. package/dist/assets/{diagram-DQf7Y_uR.js → diagram-B1p9Yn1h.js} +1 -1
  32. package/dist/assets/{dialog-bfbvR4VH.js → dialog-D08-Lg-e.js} +1 -1
  33. package/dist/assets/es-Dsmc0A_3.js +15 -0
  34. package/dist/assets/fileMentionTouchSelection-BCAKb-NS.js +3 -0
  35. package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-X773xxvX.js} +1 -1
  36. package/dist/assets/fr-C_ZlZDI2.js +13 -0
  37. package/dist/assets/gitApi-3VBj3fbR.js +10 -0
  38. package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-BMztRu0c.js} +1 -1
  39. package/dist/assets/{input-8r6F_Fza.js → input-DZZuQ1IL.js} +1 -1
  40. package/dist/assets/insertionBoundaries-BMuCcvPy.js +1 -0
  41. package/dist/assets/ja-BqqTaWTi.js +15 -0
  42. package/dist/assets/ko-DP2XPM37.js +15 -0
  43. package/dist/assets/main-COEILjRF.js +169 -0
  44. package/dist/assets/main-D2BzpnUt.css +1 -0
  45. package/dist/assets/{main-CCE_4y5F.js → main-DsUewWPt.js} +2 -2
  46. package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
  47. package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-DBgXGze8.js} +1 -1
  48. package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
  49. package/dist/assets/mcpQueries-B05QR67V.js +1 -0
  50. package/dist/assets/miniChat-D7IPMm-k.js +2 -0
  51. package/dist/assets/mobile-BnCoDxnv.js +2 -0
  52. package/dist/assets/multirun-BDmFjX9N.js +4 -0
  53. package/dist/assets/persistence-DznVIKwz.js +3 -0
  54. package/dist/assets/pl-BCf5uLlK.js +15 -0
  55. package/dist/assets/platform-DSTtx-DI.js +6 -0
  56. package/dist/assets/projectMeta-C5gcAd5f.js +3 -0
  57. package/dist/assets/pt-BR-W09Q2pvn.js +15 -0
  58. package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
  59. package/dist/assets/renderElectronMiniChatApp-DsAaWCss.js +2 -0
  60. package/dist/assets/renderMobileApp-DOaoohlp.js +28 -0
  61. package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
  62. package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-CzPK1uMj.js} +1 -1
  63. package/dist/assets/runtimeEndpointReset-DKjVJCdt.js +149 -0
  64. package/dist/assets/sessionChangeSummary-CktSWasa.js +2 -0
  65. package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-CDA1aAcI.js} +1 -1
  66. package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-Q3bb6MVj.js} +1 -1
  67. package/dist/assets/src-lS1keDVV.css +2 -0
  68. package/dist/assets/store-jaP79mqI.js +15 -0
  69. package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-Da_DO_Uh.js} +1 -1
  70. package/dist/assets/textarea--CYehp07.js +1 -0
  71. package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-CGLzBwEK.js} +1 -1
  72. package/dist/assets/uk-BgFzz8m2.js +15 -0
  73. package/dist/assets/{url-_z7evzdK.js → url-Byz3kd29.js} +1 -1
  74. package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-2zzPMQF2.js} +64 -45
  75. package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-Bz4tRflF.js} +1 -1
  76. package/dist/assets/useDesktopWindowControlsLayout-B-2YooUd.js +1 -0
  77. package/dist/assets/useEffectiveDirectory-K3U4eL5F.js +1 -0
  78. package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-D9OIZs1p.js} +1 -1
  79. package/dist/assets/useMobileNavigationStore-B0zSdCwP.js +414 -0
  80. package/dist/assets/useSessionAutoCleanup-BCXHAcbX.js +2 -0
  81. package/dist/assets/useUIStore-syVmbuDA.js +1 -0
  82. package/dist/assets/{useWorkerHighlightedLines-Bqn4AiBL.js → useWorkerHighlightedLines-CsoMQNik.js} +2 -2
  83. package/dist/assets/vendor-opencode-client-C12gfjag.js +12 -0
  84. package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-Dq5ocTF8.js} +1 -1
  85. package/dist/assets/zh-CN-BhqSLgUY.js +15 -0
  86. package/dist/assets/zh-TW-BsaBgY-p.js +15 -0
  87. package/dist/index.html +12 -12
  88. package/dist/mini-chat.html +12 -12
  89. package/dist/mobile.html +12 -12
  90. package/package.json +3 -2
  91. package/server/index.js +359 -63
  92. package/server/lib/assistants/DOCUMENTATION.md +14 -9
  93. package/server/lib/assistants/contracts.js +11 -1
  94. package/server/lib/assistants/projection-history.test.js +265 -18
  95. package/server/lib/assistants/routes.js +6 -0
  96. package/server/lib/assistants/service.js +417 -231
  97. package/server/lib/assistants/service.test.js +392 -148
  98. package/server/lib/browser-provider/DOCUMENTATION.md +141 -0
  99. package/server/lib/browser-provider/contract.js +312 -0
  100. package/server/lib/browser-provider/host.js +421 -0
  101. package/server/lib/browser-provider/host.test.js +304 -0
  102. package/server/lib/browser-provider/routes.js +122 -0
  103. package/server/lib/browser-provider/routes.test.js +120 -0
  104. package/server/lib/browser-provider/selection-store.js +43 -0
  105. package/server/lib/browser-provider/selection-store.test.js +35 -0
  106. package/server/lib/conversations/DOCUMENTATION.md +1 -1
  107. package/server/lib/conversations/service.js +1 -0
  108. package/server/lib/conversations/service.test.js +16 -0
  109. package/server/lib/conversations/validation.js +11 -1
  110. package/server/lib/event-stream/DOCUMENTATION.md +3 -1
  111. package/server/lib/event-stream/directory-ws-bridge.js +13 -2
  112. package/server/lib/event-stream/global-hub.js +49 -4
  113. package/server/lib/event-stream/reasoning-projection.js +16 -1
  114. package/server/lib/event-stream/reasoning-projection.test.js +15 -0
  115. package/server/lib/event-stream/runtime.js +57 -20
  116. package/server/lib/event-stream/runtime.test.js +112 -0
  117. package/server/lib/llm/DOCUMENTATION.md +33 -9
  118. package/server/lib/llm/catalog.js +1 -1
  119. package/server/lib/llm/completions.js +5 -1
  120. package/server/lib/llm/completions.test.js +3 -3
  121. package/server/lib/llm/generate.js +237 -145
  122. package/server/lib/llm/generate.test.js +201 -46
  123. package/server/lib/llm/routes.js +2 -0
  124. package/server/lib/message-queue/runtime.js +15 -2
  125. package/server/lib/notifications/DOCUMENTATION.md +3 -1
  126. package/server/lib/notifications/runtime.js +133 -31
  127. package/server/lib/notifications/runtime.test.js +103 -0
  128. package/server/lib/notifications/template-runtime.js +130 -38
  129. package/server/lib/notifications/template-runtime.test.js +96 -0
  130. package/server/lib/opencode/DOCUMENTATION.md +89 -17
  131. package/server/lib/opencode/agent-document.d.ts +28 -0
  132. package/server/lib/opencode/agent-document.js +280 -0
  133. package/server/lib/opencode/agent-document.test.js +137 -0
  134. package/server/lib/opencode/agents.js +96 -0
  135. package/server/lib/opencode/auth-state-runtime.js +5 -0
  136. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  137. package/server/lib/opencode/config-entity-routes.js +42 -99
  138. package/server/lib/opencode/config-entity-routes.test.js +107 -0
  139. package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
  140. package/server/lib/opencode/ensure-cli.d.ts +8 -0
  141. package/server/lib/opencode/ensure-cli.js +1 -2
  142. package/server/lib/opencode/env-runtime.js +40 -26
  143. package/server/lib/opencode/env-runtime.test.js +10 -17
  144. package/server/lib/opencode/feature-routes-runtime.js +53 -4
  145. package/server/lib/opencode/feature-routes-runtime.test.js +25 -0
  146. package/server/lib/opencode/http-server-connections.js +98 -0
  147. package/server/lib/opencode/http-server-connections.test.js +321 -0
  148. package/server/lib/opencode/lifecycle.js +349 -39
  149. package/server/lib/opencode/lifecycle.test.js +322 -0
  150. package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
  151. package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
  152. package/server/lib/opencode/openchamber-routes.js +7 -1
  153. package/server/lib/opencode/opencode2-pin.js +4 -4
  154. package/server/lib/opencode/opencode2-pin.test.js +1 -0
  155. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +127 -4
  156. package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
  157. package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
  158. package/server/lib/opencode/plugin-routes.js +3 -18
  159. package/server/lib/opencode/plugin-routes.test.js +9 -9
  160. package/server/lib/opencode/provider-catalog.js +58 -12
  161. package/server/lib/opencode/provider-catalog.test.js +48 -0
  162. package/server/lib/opencode/proxy.js +254 -46
  163. package/server/lib/opencode/proxy.test.js +238 -5
  164. package/server/lib/opencode/routes.js +326 -62
  165. package/server/lib/opencode/routes.test.js +278 -12
  166. package/server/lib/opencode/runtime-contract.js +401 -0
  167. package/server/lib/opencode/runtime-contract.test.js +214 -0
  168. package/server/lib/opencode/server-opencode-fetch.js +242 -0
  169. package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
  170. package/server/lib/opencode/server-utils-runtime.js +33 -0
  171. package/server/lib/opencode/shared-service.integration.test.js +66 -0
  172. package/server/lib/opencode/shared-service.js +147 -0
  173. package/server/lib/opencode/shared-service.test.js +93 -0
  174. package/server/lib/opencode/shared.js +3 -3
  175. package/server/lib/opencode/shutdown-runtime.js +15 -6
  176. package/server/lib/opencode/shutdown-runtime.test.js +49 -1
  177. package/server/lib/opencode/skill-routes.js +20 -33
  178. package/server/lib/opencode/skill-routes.test.js +35 -2
  179. package/server/lib/opencode/skills.test.js +15 -0
  180. package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
  181. package/server/lib/opencode/static-routes-runtime.js +3 -1
  182. package/server/lib/opencode/static-routes-runtime.test.js +33 -0
  183. package/server/lib/opencode/upgrade-screen.d.ts +30 -0
  184. package/server/lib/opencode/upgrade-screen.js +299 -0
  185. package/server/lib/opencode/upgrade-screen.test.js +196 -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 +73 -18
  211. package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
  212. package/server/lib/session-metadata/opencode-session-record.js +316 -0
  213. package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
  214. package/server/lib/session-metadata/routes.js +112 -9
  215. package/server/lib/session-metadata/routes.test.js +47 -2
  216. package/server/lib/session-metadata/session-archive.d.ts +73 -0
  217. package/server/lib/session-metadata/session-archive.js +654 -0
  218. package/server/lib/session-metadata/session-archive.test.js +359 -0
  219. package/server/lib/session-metadata/session-metadata-store.d.ts +38 -0
  220. package/server/lib/session-metadata/session-metadata-store.js +270 -83
  221. package/server/lib/session-metadata/session-metadata-store.test.js +205 -10
  222. package/server/lib/session-metadata/session-projection.d.ts +41 -0
  223. package/server/lib/session-metadata/session-projection.js +279 -0
  224. package/server/lib/session-metadata/session-projection.test.js +64 -0
  225. package/server/lib/session-metadata/system-session.js +55 -0
  226. package/server/lib/session-metadata/system-session.test.js +55 -0
  227. package/server/lib/session-title/DOCUMENTATION.md +40 -7
  228. package/server/lib/session-title/native-runtime.test.js +167 -0
  229. package/server/lib/session-title/runtime.js +69 -44
  230. package/server/lib/session-title/session-access.js +46 -0
  231. package/server/lib/session-title/session-access.test.js +91 -0
  232. package/server/lib/session-turn-pages/DOCUMENTATION.md +9 -8
  233. package/server/lib/session-turn-pages/routes.js +17 -102
  234. package/server/lib/session-turn-pages/routes.test.js +281 -0
  235. package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
  236. package/server/lib/session-turn-pages/session-message-projection.js +281 -0
  237. package/server/lib/small-model/DOCUMENTATION.md +101 -147
  238. package/server/lib/small-model/catalog.js +31 -51
  239. package/server/lib/small-model/catalog.test.js +65 -10
  240. package/server/lib/small-model/custom-api.js +68 -0
  241. package/server/lib/small-model/custom-api.test.js +64 -0
  242. package/server/lib/small-model/index.js +124 -240
  243. package/server/lib/small-model/resolve.js +35 -120
  244. package/server/lib/small-model/resolve.test.js +20 -110
  245. package/server/lib/small-model/routes.js +1 -3
  246. package/server/lib/small-model/summary-settings.test.js +157 -253
  247. package/server/opencode-proxy.test.js +2 -0
  248. package/dist/assets/AssistantView-BTCkm8yk.js +0 -3
  249. package/dist/assets/Icon-DD-BuMfZ.js +0 -1
  250. package/dist/assets/MarkdownRenderer-bc_p1H7l.js +0 -2
  251. package/dist/assets/MarkstreamRendererImpl-CnkodGJW.js +0 -3
  252. package/dist/assets/MobileSurface-BXLeiZ4d.js +0 -2
  253. package/dist/assets/OverlayScrollbar-pMwxaCca.js +0 -2
  254. package/dist/assets/SettingsView-B5I0KDTZ.js +0 -25
  255. package/dist/assets/TerminalView-Dh9MWCpS.js +0 -8
  256. package/dist/assets/appThemeRegistry-COGfxTFV.js +0 -1
  257. package/dist/assets/es-c52LTHHe.js +0 -15
  258. package/dist/assets/fileMentionTouchSelection-BvrNCroG.js +0 -4
  259. package/dist/assets/fr-PisFbUE8.js +0 -13
  260. package/dist/assets/gitApi-ButwzQ0N.js +0 -9
  261. package/dist/assets/insertionBoundaries-DGs7S5zv.js +0 -1
  262. package/dist/assets/ja-C8Ikun3Z.js +0 -15
  263. package/dist/assets/ko-ClFys5o2.js +0 -15
  264. package/dist/assets/main-Dn9rpsgl.js +0 -169
  265. package/dist/assets/main-qSeeJRW1.css +0 -1
  266. package/dist/assets/markdown-worker-CptoICmq.js +0 -2
  267. package/dist/assets/markdownTheme-DOfGg-A7.js +0 -1
  268. package/dist/assets/miniChat-BpjxVqgg.js +0 -2
  269. package/dist/assets/mobile-nMJCaNq1.js +0 -2
  270. package/dist/assets/multirun-BvSG94mW.js +0 -4
  271. package/dist/assets/persistence-Ce3AnpEr.js +0 -3
  272. package/dist/assets/pl-CnaKWN5F.js +0 -15
  273. package/dist/assets/platform-CWTIp02q.js +0 -4
  274. package/dist/assets/projectMeta-DOe49dM_.js +0 -3
  275. package/dist/assets/pt-BR-s8OpfRI6.js +0 -15
  276. package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +0 -2
  277. package/dist/assets/renderMobileApp-y-S8y8v_.js +0 -28
  278. package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
  279. package/dist/assets/runtimeEndpointReset-B6BxhvR0.js +0 -147
  280. package/dist/assets/sessionChangeSummary-VVBlEXuh.js +0 -2
  281. package/dist/assets/src-BzX5UI-c.css +0 -2
  282. package/dist/assets/store-DJczmczv.js +0 -15
  283. package/dist/assets/textarea-3XR8sUfy.js +0 -1
  284. package/dist/assets/uk-CaX0iqkB.js +0 -15
  285. package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +0 -1
  286. package/dist/assets/useEffectiveDirectory-CN6JoCFw.js +0 -1
  287. package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +0 -414
  288. package/dist/assets/useSessionAutoCleanup-BLjymaFt.js +0 -2
  289. package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
  290. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +0 -12
  291. package/dist/assets/zh-CN-20MYEjha.js +0 -15
  292. package/dist/assets/zh-TW-DiN04qTw.js +0 -15
  293. package/server/lib/small-model/call.js +0 -427
  294. package/server/lib/small-model/call.test.js +0 -382
  295. package/server/lib/small-model/opencode-session.js +0 -183
  296. package/server/lib/small-model/opencode-session.test.js +0 -147
@@ -8,13 +8,13 @@ This module owns the authoritative permission auto-accept policy for web, deskto
8
8
 
9
9
  `permissionAutoAccept.sessions` contains explicit per-session boolean policies.
10
10
 
11
- Policy inheritance uses the nearest explicit session value. A child `false` therefore overrides a parent `true`; descendants without an explicit value inherit from their nearest configured ancestor.
11
+ Policy inheritance uses the nearest explicit session value. A child `false` therefore overrides a parent `true`. Sessions with no explicit value, including a missing ancestor lookup, default to allow. A failed settings load still fails closed and does not approve.
12
12
 
13
13
  ## Runtime
14
14
 
15
- `createPermissionAutoAcceptRuntime` loads and serializes policy writes, subscribes to the global OpenCode event hub, caches session lineage, retries transient replies, and reconciles pending permissions after startup, reconnect, and policy enablement. Enabling Auto-Accept for a session immediately accepts matching pending requests and keeps handling future requests without requiring a connected UI.
15
+ `createPermissionAutoAcceptRuntime` loads and serializes policy writes, subscribes to the global OpenCode event hub, caches session lineage, retries transient replies, and reconciles pending permissions after startup, reconnect, and policy enablement. Pending requests are listed at `GET /permission/request` and approved with `POST /session/:sessionID/permission/:requestID/reply` body `{ decision: "always" }`. OpenCode 2.0.12 rejects the legacy `{ reply }` body and the unscoped `/permission/:id/reply` path.
16
16
 
17
- Unknown lineage and failed policy loads fail closed. A failed pending-permission fetch is distinct from an empty successful response and never clears policy state.
17
+ A failed settings load fails closed and does not approve. A failed pending-permission fetch is distinct from an empty successful response and never clears policy state.
18
18
 
19
19
  ## Routes
20
20
 
@@ -27,9 +27,9 @@ These are normal authenticated OpenChamber runtime routes. They must not be adde
27
27
 
28
28
  `packages/ui/src/stores/permissionStore.ts` is a projection of server policy and does not persist an independent policy. The server is the sole responder and the UI renders pending requests until the authoritative `permission.replied` event arrives.
29
29
 
30
- `ChatInput` derives control visibility from the persisted, directory-scoped `useConfigStore` agent snapshot. A selected agent with a final global `allow` or `deny` rule has no remaining prompt path and hides the control. Unknown snapshots, missing selected agents, and rules that can ask keep the control visible. This path issues no additional request.
30
+ `ChatInput` derives control visibility from the persisted, directory-scoped `useConfigStore` agent snapshot via `shouldShowPermissionAutoAcceptControl`. It reads the V2 wire `permissions` field when present (OpenCode `AgentInfo.permissions`), otherwise the legacy singular `permission` document. Rule normalization reuses `toPermissionRuleset` so both V2 `{ action, resource, effect }` and V1 `{ permission, pattern, action }` / tool-map shapes are recognized. A selected agent with a final global `allow` or `deny` rule has no remaining prompt path and hides the control. Unknown snapshots, missing selected agents, unparseable rules, and rules that can still ask keep the control visible. This path issues no additional request and does not change server auto-accept policy.
31
31
 
32
- VS Code retains its foreground-only implementation because it does not run the web server runtime.
32
+ VS Code retains its foreground-only implementation because it does not run the web server runtime (`isVSCodeRuntime()` always shows the control).
33
33
 
34
34
  ## Tests
35
35
 
@@ -123,6 +123,7 @@ export function createPermissionAutoAcceptRuntime({
123
123
 
124
124
  const isSessionAutoAccepting = async (sessionId, directory) => {
125
125
  await load();
126
+ if (!sessionId) return false;
126
127
  const seen = new Set();
127
128
  let current = sessionId;
128
129
  let currentDirectory = directory;
@@ -133,22 +134,23 @@ export function createPermissionAutoAcceptRuntime({
133
134
  try {
134
135
  info = await getSession(current, currentDirectory);
135
136
  } catch {
136
- return false;
137
+ // Lineage lookup failed. No explicit deny was found, so keep the default allow.
138
+ return true;
137
139
  }
138
140
  current = info?.parentID ?? null;
139
141
  currentDirectory = info?.directory ?? currentDirectory;
140
142
  }
141
- return false;
143
+ return true;
142
144
  };
143
145
 
144
146
  const replyOnce = async (permission, directory) => {
145
147
  if (!permission?.id || !permission?.sessionID) return false;
146
148
  await load();
147
149
  if (!(await isSessionAutoAccepting(permission.sessionID, directory))) return false;
148
- await request(`/permission/${encodeURIComponent(permission.id)}/reply`, {
150
+ await request(`/session/${encodeURIComponent(permission.sessionID)}/permission/${encodeURIComponent(permission.id)}/reply`, {
149
151
  directory,
150
152
  method: 'POST',
151
- body: { reply: 'once' },
153
+ body: { decision: 'always' },
152
154
  });
153
155
  return true;
154
156
  };
@@ -187,7 +189,7 @@ export function createPermissionAutoAcceptRuntime({
187
189
  for (const directory of scopes) {
188
190
  let payload;
189
191
  try {
190
- payload = await request('/permission', { directory });
192
+ payload = await request('/permission/request', { directory });
191
193
  } catch {
192
194
  continue;
193
195
  }
@@ -209,12 +211,17 @@ export function createPermissionAutoAcceptRuntime({
209
211
  const raw = event?.payload;
210
212
  const payload = raw?.payload && typeof raw.payload === 'object' ? raw.payload : raw;
211
213
  const directory = typeof event?.directory === 'string' && event.directory !== 'global' ? event.directory : undefined;
214
+ const body = payload?.properties && typeof payload.properties === 'object'
215
+ ? payload.properties
216
+ : payload?.data && typeof payload.data === 'object'
217
+ ? payload.data
218
+ : payload;
212
219
  if (payload?.type === 'session.created' || payload?.type === 'session.updated') {
213
- rememberSession(payload.properties?.info, directory);
220
+ rememberSession(body?.info ?? payload.properties?.info, directory);
214
221
  return;
215
222
  }
216
223
  if (payload?.type === 'permission.asked') {
217
- void processPermission(payload.properties, directory);
224
+ void processPermission(body, directory);
218
225
  }
219
226
  };
220
227
 
@@ -31,6 +31,18 @@ const flush = async () => {
31
31
  };
32
32
 
33
33
  describe('permission auto-accept runtime', () => {
34
+ it('approves a session with no explicit policy', async () => {
35
+ const fetchImpl = vi.fn(async (url, init = {}) => {
36
+ const path = new URL(url).pathname;
37
+ if (path === '/session/root') return Response.json({ id: 'root' });
38
+ if (init.method === 'POST') return Response.json({});
39
+ return new Response('', { status: 404 });
40
+ });
41
+ const { runtime } = createRuntime({ fetchImpl });
42
+ await expect(runtime.processPermission({ id: 'perm', sessionID: 'root' }, '/project')).resolves.toBe(true);
43
+ expect(fetchImpl.mock.calls.some(([url, init]) => new URL(url).pathname === '/session/root/permission/perm/reply' && JSON.parse(init.body).decision === 'always')).toBe(true);
44
+ });
45
+
34
46
  it('persists explicit session policies across runtime restarts', async () => {
35
47
  const first = createRuntime();
36
48
  await first.runtime.setSessionPolicy('root', true);
@@ -55,7 +67,7 @@ describe('permission auto-accept runtime', () => {
55
67
  it('fetches missing subagent lineage before replying', async () => {
56
68
  const fetchImpl = vi.fn(async (url, init = {}) => {
57
69
  const path = new URL(url).pathname;
58
- if (path === '/permission') return new Response('[]');
70
+ if (path === '/permission/request') return new Response('[]');
59
71
  if (path === '/session/child') return Response.json({ id: 'child', parentID: 'root', directory: '/project' });
60
72
  if (init.method === 'POST') return Response.json({});
61
73
  return new Response('', { status: 404 });
@@ -65,15 +77,15 @@ describe('permission auto-accept runtime', () => {
65
77
  fetchImpl,
66
78
  });
67
79
  await expect(runtime.processPermission({ id: 'perm', sessionID: 'child' }, '/project')).resolves.toBe(true);
68
- expect(fetchImpl.mock.calls.some(([url, init]) => new URL(url).pathname === '/permission/perm/reply' && init.method === 'POST')).toBe(true);
80
+ expect(fetchImpl.mock.calls.some(([url, init]) => new URL(url).pathname === '/session/child/permission/perm/reply' && init.method === 'POST' && JSON.parse(init.body).decision === 'always')).toBe(true);
69
81
  });
70
82
 
71
83
  it('retries a transient reply failure and deduplicates concurrent events', async () => {
72
84
  let replyAttempts = 0;
73
85
  const fetchImpl = vi.fn(async (url, init = {}) => {
74
86
  const path = new URL(url).pathname;
75
- if (path === '/permission') return new Response('[]');
76
- if (path === '/permission/perm/reply' && init.method === 'POST') {
87
+ if (path === '/permission/request') return new Response('[]');
88
+ if (path === '/session/root/permission/perm/reply' && init.method === 'POST') {
77
89
  replyAttempts += 1;
78
90
  return replyAttempts === 1 ? new Response('', { status: 503 }) : Response.json({});
79
91
  }
@@ -94,8 +106,8 @@ describe('permission auto-accept runtime', () => {
94
106
  it('reconciles pending permissions after reconnect', async () => {
95
107
  const fetchImpl = vi.fn(async (url, init = {}) => {
96
108
  const path = new URL(url).pathname;
97
- if (path === '/permission') return Response.json([{ id: 'pending', sessionID: 'root' }]);
98
- if (path === '/permission/pending/reply' && init.method === 'POST') return Response.json({});
109
+ if (path === '/permission/request') return Response.json([{ id: 'pending', sessionID: 'root' }]);
110
+ if (path === '/session/root/permission/pending/reply' && init.method === 'POST') return Response.json({});
99
111
  return Response.json({ id: 'root' });
100
112
  });
101
113
  const { connect } = createRuntime({
@@ -104,14 +116,14 @@ describe('permission auto-accept runtime', () => {
104
116
  });
105
117
  connect();
106
118
  await flush();
107
- expect(fetchImpl.mock.calls.some(([url]) => new URL(url).pathname === '/permission/pending/reply')).toBe(true);
119
+ expect(fetchImpl.mock.calls.some(([url]) => new URL(url).pathname === '/session/root/permission/pending/reply')).toBe(true);
108
120
  });
109
121
 
110
122
  it('accepts existing pending permissions when a session policy is enabled', async () => {
111
123
  const fetchImpl = vi.fn(async (url, init = {}) => {
112
124
  const parsed = new URL(url);
113
125
  const path = parsed.pathname;
114
- if (path === '/permission') {
126
+ if (path === '/permission/request') {
115
127
  return parsed.searchParams.get('directory') === '/project'
116
128
  ? Response.json([
117
129
  { id: 'root-pending', sessionID: 'root' },
@@ -119,19 +131,22 @@ describe('permission auto-accept runtime', () => {
119
131
  ])
120
132
  : Response.json([]);
121
133
  }
122
- if (path === '/permission/root-pending/reply' && init.method === 'POST') return Response.json({});
134
+ if (init.method === 'POST' && path.endsWith('/reply')) return Response.json({});
123
135
  if (path === '/session/other') return Response.json({ id: 'other' });
124
136
  return new Response('', { status: 404 });
125
137
  });
126
- const { runtime } = createRuntime({ fetchImpl });
138
+ const { runtime } = createRuntime({
139
+ stored: { permissionAutoAccept: { sessions: { other: false } } },
140
+ fetchImpl,
141
+ });
127
142
 
128
143
  await runtime.setSessionPolicy('root', true, '/project');
129
144
 
130
145
  const replyPaths = fetchImpl.mock.calls
131
146
  .filter(([, init]) => init?.method === 'POST')
132
147
  .map(([url]) => new URL(url).pathname);
133
- expect(replyPaths).toEqual(['/permission/root-pending/reply']);
148
+ expect(replyPaths).toEqual(['/session/root/permission/root-pending/reply']);
134
149
  expect(fetchImpl.mock.calls.some(([url]) => new URL(url).searchParams.get('directory') === '/project')).toBe(true);
135
- expect(await runtime.load()).toEqual({ sessions: { root: true } });
150
+ expect(await runtime.load()).toEqual({ sessions: { other: false, root: true } });
136
151
  });
137
152
  });
@@ -147,7 +147,11 @@ const startFakeOpenCode = async (): Promise<{
147
147
  next();
148
148
  });
149
149
 
150
- app.get('/global/event', (req, res) => {
150
+ // Production proxy forwards official v2 paths with `/api` restored
151
+ // (`toUpstreamOpenCodeApiPath`). Host mounts both `/api/global/event` and
152
+ // `/api/event`; fake upstream must match those upstream targets — not the
153
+ // pre-v2 bare `/global/event` or `/event`.
154
+ const writeSseFixture = (res: express.Response) => {
151
155
  res.writeHead(200, {
152
156
  'content-type': 'text/event-stream; charset=utf-8',
153
157
  'cache-control': 'no-cache',
@@ -243,9 +247,16 @@ const startFakeOpenCode = async (): Promise<{
243
247
  );
244
248
 
245
249
  res.end();
250
+ };
251
+
252
+ app.get('/api/global/event', (_req, res) => {
253
+ writeSseFixture(res);
254
+ });
255
+ app.get('/api/event', (_req, res) => {
256
+ writeSseFixture(res);
246
257
  });
247
258
 
248
- app.get('/session/:sessionID/message', (req, res) => {
259
+ app.get('/api/session/:sessionID/message', (_req, res) => {
249
260
  res.json([
250
261
  {
251
262
  info: {
@@ -513,11 +524,15 @@ describe('relay reasoning projection e2e (Host filter over private relay)', () =
513
524
  for (const url of upstream.observation.urls) {
514
525
  expect(url).not.toContain('includeReasoning');
515
526
  }
516
- // Sanity: upstream still saw the event + message routes.
517
- expect(upstream.observation.urls.some((u) => u.includes('/global/event'))).toBe(true);
518
- expect(upstream.observation.urls.some((u) => u.includes('/session/ses_1/message'))).toBe(
519
- true,
520
- );
527
+ // Sanity: upstream still saw official v2 event + message routes.
528
+ expect(
529
+ upstream.observation.urls.some(
530
+ (u) => u.includes('/api/global/event') || u.includes('/api/event'),
531
+ ),
532
+ ).toBe(true);
533
+ expect(
534
+ upstream.observation.urls.some((u) => u.includes('/api/session/ses_1/message')),
535
+ ).toBe(true);
521
536
 
522
537
  // Unauthenticated tunnel request still hits real auth gate (not open).
523
538
  const denied = await client.fetch('/api/global/event?includeReasoning=false', {
@@ -69,6 +69,14 @@ Server-owned scheduled task runtime and routes for OpenChamber-only automation.
69
69
  - `runNow(projectId, taskId)`
70
70
  - `observeSessionEvent(event)`
71
71
 
72
+ Isolation dependencies (required for sidebar hide):
73
+
74
+ - `persistSessionMetadata(sessionID, patch)` — Host store write. Missing
75
+ persist refuses the run **before** `session.create`. Persist failure after
76
+ create fails the run before prompt.
77
+ - `onSystemSessionPersisted({ sessionID, directory, metadata })` — drop the
78
+ session from the sidebar index immediately after Host commit.
79
+
72
80
  Dependencies include optional `runHistoryStore` with:
73
81
 
74
82
  - `startRun(record)`
@@ -8,6 +8,7 @@ import {
8
8
  isSessionGoalSupported,
9
9
  sessionGoalUnavailableMessage,
10
10
  } from '../session-goal/capability.js';
11
+ import { buildScheduledTaskMetadata } from '../session-metadata/system-session.js';
11
12
  import { SCHEDULED_SLOT_CLAIMED_CODE } from './run-history-store.js';
12
13
 
13
14
  const DEFAULT_GLOBAL_CONCURRENCY = 4;
@@ -481,12 +482,26 @@ const snapshotSessionOutcome = async ({
481
482
 
482
483
  const parseSessionEventPhase = (event) => {
483
484
  const payload = event?.payload?.payload ?? event?.payload;
484
- const properties = payload?.properties;
485
+ // Accept bridge `{ properties }` and native v2 `{ data }` envelopes.
486
+ const properties = (payload?.properties && typeof payload.properties === 'object')
487
+ ? payload.properties
488
+ : ((payload?.data && typeof payload.data === 'object') ? payload.data : null);
485
489
  const sessionID = typeof properties?.sessionID === 'string' ? properties.sessionID : '';
486
490
  if (!sessionID) {
487
491
  return { phase: '', sessionID: '' };
488
492
  }
489
- if (payload?.type === 'session.idle') {
493
+ // Ticket 07: shutdown interrupt is not an idle terminal — keep run open.
494
+ if (payload?.type === 'session.execution.interrupted' && properties?.reason === 'shutdown') {
495
+ return { phase: 'busy', sessionID };
496
+ }
497
+ if (payload?.type === 'session.idle' || payload?.type === 'session.execution.succeeded') {
498
+ return { phase: 'idle', sessionID };
499
+ }
500
+ if (payload?.type === 'session.execution.failed') {
501
+ return { phase: 'error', sessionID };
502
+ }
503
+ if (payload?.type === 'session.execution.interrupted') {
504
+ // user / superseded / inactivity — claim released; treat as idle for settlement.
490
505
  return { phase: 'idle', sessionID };
491
506
  }
492
507
  if (payload?.type === 'session.status') {
@@ -518,6 +533,14 @@ export const createScheduledTasksRuntime = (deps) => {
518
533
  maxRunDurationMs = DEFAULT_MAX_RUN_MS,
519
534
  /** Host store write: (sessionId, directory, goal) => Promise */
520
535
  persistSessionGoal = null,
536
+ /** Host store write: (sessionId, patch) => Promise — required for sidebar isolation */
537
+ persistSessionMetadata = null,
538
+ /**
539
+ * After Host isolation metadata is committed. Used to drop the session
540
+ * from the sidebar index immediately (do not wait for the next list sync).
541
+ * @type {null | ((event: { sessionID: string, directory?: string, metadata: object }) => void)}
542
+ */
543
+ onSystemSessionPersisted = null,
521
544
  /** Host store read: (sessionId) => Promise<metadata|null> */
522
545
  readSessionMetadata = null,
523
546
  /**
@@ -801,9 +824,10 @@ export const createScheduledTasksRuntime = (deps) => {
801
824
  + '\n</system-reminder>';
802
825
  };
803
826
 
804
- const buildSessionCreateInput = (task, projectPath, title) => ({
827
+ const buildSessionCreateInput = (task, projectPath, title, metadata) => ({
805
828
  title,
806
829
  location: { directory: projectPath },
830
+ metadata,
807
831
  ...(task.execution.agent ? { agent: task.execution.agent } : {}),
808
832
  model: {
809
833
  id: task.execution.modelID,
@@ -812,6 +836,18 @@ export const createScheduledTasksRuntime = (deps) => {
812
836
  },
813
837
  });
814
838
 
839
+ const persistScheduledTaskIsolation = async (sessionID, directory, metadata) => {
840
+ if (typeof persistSessionMetadata !== 'function') {
841
+ throw new Error('scheduled-task session metadata persist is not configured');
842
+ }
843
+ await persistSessionMetadata(sessionID, metadata);
844
+ try {
845
+ onSystemSessionPersisted?.({ sessionID, directory, metadata });
846
+ } catch {
847
+ // Index/live hide is compensatory; Host persist is the authority.
848
+ }
849
+ };
850
+
815
851
  const buildPromptText = (task, projectPath) => {
816
852
  const promptText = expandSnippets(task.execution.prompt, projectPath);
817
853
  if (!task.execution.goalEnabled) {
@@ -915,6 +951,7 @@ export const createScheduledTasksRuntime = (deps) => {
915
951
  note: '',
916
952
  statusReason: '',
917
953
  lastAccountedMessageID: '',
954
+ executionGeneration: 0,
918
955
  createdAt: now,
919
956
  updatedAt: now,
920
957
  };
@@ -1107,6 +1144,10 @@ export const createScheduledTasksRuntime = (deps) => {
1107
1144
  }
1108
1145
  }
1109
1146
 
1147
+ if (typeof persistSessionMetadata !== 'function') {
1148
+ throw new Error('scheduled-task session metadata persist is not configured');
1149
+ }
1150
+
1110
1151
  if (typeof waitForOpenCodeReady === 'function') {
1111
1152
  await waitForOpenCodeReady(10_000, 250);
1112
1153
  signal?.throwIfAborted?.();
@@ -1141,14 +1182,21 @@ export const createScheduledTasksRuntime = (deps) => {
1141
1182
  };
1142
1183
 
1143
1184
  try {
1185
+ const isolationMetadata = buildScheduledTaskMetadata({
1186
+ projectID,
1187
+ taskID: task.id,
1188
+ runID,
1189
+ name: task.name,
1190
+ });
1144
1191
  const session = await client.session.create(
1145
- buildSessionCreateInput(task, projectPath, title),
1192
+ buildSessionCreateInput(task, projectPath, title, isolationMetadata),
1146
1193
  requestOptions,
1147
1194
  );
1148
1195
  sessionID = session?.id;
1149
1196
  if (!sessionID) {
1150
1197
  throw new Error('failed to create session');
1151
1198
  }
1199
+ await persistScheduledTaskIsolation(sessionID, projectPath, isolationMetadata);
1152
1200
 
1153
1201
  if (signal) {
1154
1202
  if (signal.aborted) {
@@ -45,6 +45,7 @@ const createRuntime = (updateScheduledTaskState, overrides = {}) => createSchedu
45
45
  waitForOpenCodeReady: vi.fn(async () => {
46
46
  throw new Error('OpenCode unavailable');
47
47
  }),
48
+ persistSessionMetadata: vi.fn(async () => ({})),
48
49
  logger: { info: vi.fn(), warn: vi.fn() },
49
50
  ...overrides,
50
51
  });
@@ -443,12 +444,16 @@ describe('scheduled-tasks run history and session lifecycle', () => {
443
444
  it('creates session with Scheduled title and location, attaches history, then prompts via session.prompt', async () => {
444
445
  const history = createHistoryStore();
445
446
  const client = createSuccessfulClient();
447
+ const persistSessionMetadata = vi.fn(async () => ({}));
448
+ const onSystemSessionPersisted = vi.fn();
446
449
 
447
450
  const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
448
451
  task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
449
452
  }));
450
453
  const runtime = createRuntime(updateScheduledTaskState, {
451
454
  runHistoryStore: history,
455
+ persistSessionMetadata,
456
+ onSystemSessionPersisted,
452
457
  waitForOpenCodeReady: vi.fn(async () => {}),
453
458
  });
454
459
  await runtime.syncProject('project-1');
@@ -474,9 +479,37 @@ describe('scheduled-tasks run history and session lifecycle', () => {
474
479
  location: { directory: '/tmp/project-1' },
475
480
  title: expect.stringMatching(/^\[Scheduled\] Task /),
476
481
  model: { id: 'gpt-4.1', providerID: 'openai' },
482
+ metadata: {
483
+ openchamber: {
484
+ scheduledTask: expect.objectContaining({
485
+ projectID: 'project-1',
486
+ taskID: 'task-1',
487
+ runID: started.id,
488
+ name: 'Task',
489
+ }),
490
+ },
491
+ },
477
492
  }), expect.objectContaining({ signal: expect.any(AbortSignal) }));
478
- expect(client.create.mock.calls[0][0].metadata).toBeUndefined();
479
493
  expect(client.create.mock.calls[0][0].directory).toBeUndefined();
494
+ expect(persistSessionMetadata).toHaveBeenCalledWith('ses_1', {
495
+ openchamber: {
496
+ scheduledTask: expect.objectContaining({
497
+ projectID: 'project-1',
498
+ taskID: 'task-1',
499
+ runID: started.id,
500
+ name: 'Task',
501
+ }),
502
+ },
503
+ });
504
+ expect(onSystemSessionPersisted).toHaveBeenCalledWith({
505
+ sessionID: 'ses_1',
506
+ directory: '/tmp/project-1',
507
+ metadata: expect.objectContaining({
508
+ openchamber: expect.objectContaining({
509
+ scheduledTask: expect.objectContaining({ taskID: 'task-1' }),
510
+ }),
511
+ }),
512
+ });
480
513
  expect(history.attachSession).toHaveBeenCalledWith(started.id, 'ses_1');
481
514
  expect(client.prompt).toHaveBeenCalledWith({
482
515
  sessionID: 'ses_1',
@@ -492,6 +525,48 @@ describe('scheduled-tasks run history and session lifecycle', () => {
492
525
  expect(client.active.mock.invocationCallOrder[0]).toBeGreaterThan(client.prompt.mock.invocationCallOrder[0]);
493
526
  });
494
527
 
528
+ it('refuses before session create when Host metadata persist is not configured', async () => {
529
+ const history = createHistoryStore();
530
+ const client = createSuccessfulClient();
531
+ const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
532
+ task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
533
+ }));
534
+ const runtime = createRuntime(updateScheduledTaskState, {
535
+ runHistoryStore: history,
536
+ persistSessionMetadata: null,
537
+ waitForOpenCodeReady: vi.fn(async () => {}),
538
+ });
539
+ await runtime.syncProject('project-1');
540
+ const result = await runtime.runNow('project-1', 'task-1');
541
+
542
+ expect(result.ok).toBe(false);
543
+ expect(result.error).toMatch(/session metadata persist is not configured/);
544
+ expect(client.create).not.toHaveBeenCalled();
545
+ expect(client.prompt).not.toHaveBeenCalled();
546
+ });
547
+
548
+ it('fails the run before prompt when Host metadata persist throws', async () => {
549
+ const history = createHistoryStore();
550
+ const client = createSuccessfulClient();
551
+ const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
552
+ task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
553
+ }));
554
+ const runtime = createRuntime(updateScheduledTaskState, {
555
+ runHistoryStore: history,
556
+ persistSessionMetadata: vi.fn(async () => {
557
+ throw new Error('store unavailable');
558
+ }),
559
+ waitForOpenCodeReady: vi.fn(async () => {}),
560
+ });
561
+ await runtime.syncProject('project-1');
562
+ const result = await runtime.runNow('project-1', 'task-1');
563
+
564
+ expect(result.ok).toBe(false);
565
+ expect(result.error).toMatch(/store unavailable/);
566
+ expect(client.create).toHaveBeenCalled();
567
+ expect(client.prompt).not.toHaveBeenCalled();
568
+ });
569
+
495
570
  it('records error outcome and real duration when the assistant turn fails after admission', async () => {
496
571
  const history = createHistoryStore();
497
572
  const client = createSuccessfulClient({ settlement: 'assistant-error' });
@@ -763,6 +838,7 @@ describe('scheduled-tasks run history and session lifecycle', () => {
763
838
  buildOpenCodeUrl: vi.fn(() => 'http://127.0.0.1:4096'),
764
839
  getOpenCodeAuthHeaders: vi.fn(() => ({})),
765
840
  waitForOpenCodeReady: vi.fn(async () => {}),
841
+ persistSessionMetadata: vi.fn(async () => ({})),
766
842
  logger: { info: vi.fn(), warn: vi.fn() },
767
843
  runHistoryStore: history,
768
844
  maxRunDurationMs: 500,
@@ -948,6 +1024,7 @@ describe('scheduled-tasks run history and session lifecycle', () => {
948
1024
  buildOpenCodeUrl: vi.fn(() => 'http://127.0.0.1:4096'),
949
1025
  getOpenCodeAuthHeaders: vi.fn(() => ({})),
950
1026
  waitForOpenCodeReady: vi.fn(async () => {}),
1027
+ persistSessionMetadata: vi.fn(async () => ({})),
951
1028
  logger: { info: vi.fn(), warn: vi.fn() },
952
1029
  runHistoryStore: history,
953
1030
  persistSessionGoal,
@@ -1016,6 +1093,7 @@ describe('scheduled-tasks run history and session lifecycle', () => {
1016
1093
  buildOpenCodeUrl: vi.fn(() => 'http://127.0.0.1:4096'),
1017
1094
  getOpenCodeAuthHeaders: vi.fn(() => ({})),
1018
1095
  waitForOpenCodeReady: vi.fn(async () => {}),
1096
+ persistSessionMetadata: vi.fn(async () => ({})),
1019
1097
  logger: { info: vi.fn(), warn: vi.fn() },
1020
1098
  runHistoryStore: history,
1021
1099
  persistSessionGoal,
@@ -1581,6 +1659,7 @@ describe('scheduled-tasks cross-process slot occupancy', () => {
1581
1659
  waitForOpenCodeReady: vi.fn(async () => {
1582
1660
  throw new Error('OpenCode unavailable');
1583
1661
  }),
1662
+ persistSessionMetadata: vi.fn(async () => ({})),
1584
1663
  logger: { info: vi.fn(), warn: vi.fn() },
1585
1664
  runHistoryStore: store,
1586
1665
  notifyTaskRun,