synabun 2.0.9 → 2.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -1
  3. package/THIRD-PARTY-LICENSES.md +46 -4
  4. package/launcher.mjs +9 -3
  5. package/lib/start-launcher.js +6 -1
  6. package/mcp-server/package-lock.json +2 -2
  7. package/mcp-server/package.json +1 -1
  8. package/neural-interface/i18n/en.json +218 -7
  9. package/neural-interface/i18n/pt-BR.json +220 -9
  10. package/neural-interface/lib/account-usage.js +5 -3
  11. package/neural-interface/lib/app-controls-api.js +161 -0
  12. package/neural-interface/lib/assistant-api.js +21 -7
  13. package/neural-interface/lib/assistant-brains/opencode.js +13 -1
  14. package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +44 -0
  15. package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +57 -0
  16. package/neural-interface/lib/assistant-catalog.js +17 -2
  17. package/neural-interface/lib/assistant-dispatch.js +3 -0
  18. package/neural-interface/lib/assistant-envelope.js +19 -1
  19. package/neural-interface/lib/assistant-runtime.js +127 -8
  20. package/neural-interface/lib/claude-model-catalog.js +64 -14
  21. package/neural-interface/lib/codex-settings-schema.js +109 -47
  22. package/neural-interface/lib/editor-completion-opencode.js +132 -35
  23. package/neural-interface/lib/exec-loop-command.js +8 -1
  24. package/neural-interface/lib/launch-cache.js +293 -0
  25. package/neural-interface/lib/login-start.js +524 -0
  26. package/neural-interface/lib/native-loop-providers.js +18 -3
  27. package/neural-interface/lib/native-loop-runtime.js +19 -0
  28. package/neural-interface/lib/opencode-db-schema.js +75 -0
  29. package/neural-interface/lib/opencode-history.js +16 -1
  30. package/neural-interface/lib/opencode-protocol.js +173 -0
  31. package/neural-interface/lib/opencode-provider-view.js +81 -0
  32. package/neural-interface/lib/opencode-runtime-config.js +10 -3
  33. package/neural-interface/lib/opencode-serve-records.js +606 -0
  34. package/neural-interface/lib/opencode-v2-client.js +33 -6
  35. package/neural-interface/lib/opencode-v2-ws-requests.js +19 -5
  36. package/neural-interface/lib/opencode-worker-activity.js +229 -0
  37. package/neural-interface/lib/opencode2-client.js +760 -0
  38. package/neural-interface/lib/opencode2-proxy.js +116 -0
  39. package/neural-interface/lib/opencode2-translate.js +1008 -0
  40. package/neural-interface/lib/process-identity.js +361 -0
  41. package/neural-interface/lib/session-cache-builder.js +26 -5
  42. package/neural-interface/lib/session-title-generator.js +18 -0
  43. package/neural-interface/lib/style-guide/api.js +4 -3
  44. package/neural-interface/lib/untrusted-file-headers.js +27 -0
  45. package/neural-interface/package-lock.json +1343 -45
  46. package/neural-interface/package.json +5 -1
  47. package/neural-interface/public/index.html +2 -2
  48. package/neural-interface/public/shared/agent-runtime-options.js +55 -11
  49. package/neural-interface/public/shared/assistant/asst-agents-dock.js +172 -17
  50. package/neural-interface/public/shared/assistant/asst-api.js +11 -3
  51. package/neural-interface/public/shared/assistant/asst-brain-picker.js +195 -80
  52. package/neural-interface/public/shared/assistant/asst-composer.js +1 -1
  53. package/neural-interface/public/shared/assistant/asst-hosts.js +49 -2
  54. package/neural-interface/public/shared/assistant/asst-menu.js +77 -6
  55. package/neural-interface/public/shared/assistant/asst-panel.js +197 -33
  56. package/neural-interface/public/shared/assistant/asst-rack-styles.js +28 -10
  57. package/neural-interface/public/shared/assistant/asst-redact.js +186 -0
  58. package/neural-interface/public/shared/assistant/asst-render.js +382 -36
  59. package/neural-interface/public/shared/assistant/asst-run-stage.js +231 -0
  60. package/neural-interface/public/shared/assistant/asst-sanitize.js +193 -3
  61. package/neural-interface/public/shared/assistant/asst-scene-styles.js +273 -0
  62. package/neural-interface/public/shared/assistant/asst-scenes.js +528 -0
  63. package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +3 -15
  64. package/neural-interface/public/shared/assistant/asst-state.js +14 -0
  65. package/neural-interface/public/shared/assistant/asst-styles.js +43 -0
  66. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +13 -1
  67. package/neural-interface/public/shared/cc-dropdown.js +480 -0
  68. package/neural-interface/public/shared/cdx/cdx-panel.js +13 -1
  69. package/neural-interface/public/shared/cdx/cdx-render.js +15 -3
  70. package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
  71. package/neural-interface/public/shared/cdx/cdx-tabs.js +149 -35
  72. package/neural-interface/public/shared/cp/cp-markdown.js +4 -57
  73. package/neural-interface/public/shared/data-cache.js +223 -0
  74. package/neural-interface/public/shared/desktop-bridge.js +11 -0
  75. package/neural-interface/public/shared/desktop-titlebar-view.js +78 -1
  76. package/neural-interface/public/shared/desktop-titlebar.js +214 -28
  77. package/neural-interface/public/shared/desktop-update-view.js +15 -5
  78. package/neural-interface/public/shared/file-links.js +73 -8
  79. package/neural-interface/public/shared/html-shell.js +74 -80
  80. package/neural-interface/public/shared/launch-prefetch.js +59 -0
  81. package/neural-interface/public/shared/ocp-v2/ocp-v2-caps.js +3 -2
  82. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +7 -2
  83. package/neural-interface/public/shared/ocp-v2/ocp-v2-modelpicker.js +54 -17
  84. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +93 -2
  85. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +3 -2
  86. package/neural-interface/public/shared/ocp-v2/ocp-v2-providers.js +11 -9
  87. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +15 -4
  88. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +1 -1
  89. package/neural-interface/public/shared/ocp-v2/ocp-v2-send-logic.js +401 -0
  90. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +16 -9
  91. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +60 -0
  92. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +48 -19
  93. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +195 -19
  94. package/neural-interface/public/shared/opencode-providers.js +39 -0
  95. package/neural-interface/public/shared/schedules-view.js +318 -0
  96. package/neural-interface/public/shared/settings/settings-app-pane.js +335 -0
  97. package/neural-interface/public/shared/settings/settings-app.js +243 -0
  98. package/neural-interface/public/shared/settings/settings-data.js +88 -0
  99. package/neural-interface/public/shared/settings/settings-ia.js +17 -0
  100. package/neural-interface/public/shared/sidepanel-session-status.js +49 -0
  101. package/neural-interface/public/shared/styles.css +540 -371
  102. package/neural-interface/public/shared/ui-assistant.js +27 -5
  103. package/neural-interface/public/shared/ui-automation-studio.js +67 -27
  104. package/neural-interface/public/shared/ui-bookmarks.js +2 -1
  105. package/neural-interface/public/shared/ui-claude-panel.js +76 -8
  106. package/neural-interface/public/shared/ui-codex-panel.js +1 -0
  107. package/neural-interface/public/shared/ui-detail.js +6 -6
  108. package/neural-interface/public/shared/ui-file-links.js +1 -0
  109. package/neural-interface/public/shared/ui-image-gallery.js +6 -5
  110. package/neural-interface/public/shared/ui-invite.js +18 -2
  111. package/neural-interface/public/shared/ui-link.js +9 -9
  112. package/neural-interface/public/shared/ui-menubar.js +232 -33
  113. package/neural-interface/public/shared/ui-native-loop-router.js +2 -0
  114. package/neural-interface/public/shared/ui-opencode-panel-v2.js +1 -0
  115. package/neural-interface/public/shared/ui-schedules-studio.js +2099 -869
  116. package/neural-interface/public/shared/ui-sessions.js +15 -4
  117. package/neural-interface/public/shared/ui-settings.js +87 -36
  118. package/neural-interface/public/shared/ui-sidebar.js +4 -4
  119. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +13 -0
  120. package/neural-interface/public/shared/ui-sidepanel-sessions.js +55 -4
  121. package/neural-interface/public/shared/ui-sidepanel-tray.js +84 -8
  122. package/neural-interface/public/shared/ui-skills.js +70 -12
  123. package/neural-interface/public/shared/ui-sync.js +7 -0
  124. package/neural-interface/public/shared/ui-terminal.js +36 -20
  125. package/neural-interface/public/shared/ui-trash.js +8 -1
  126. package/neural-interface/public/shared/ui-tutorial-steps.js +7 -3
  127. package/neural-interface/public/shared/ui-tutorial.js +15 -5
  128. package/neural-interface/public/shared/ui-update.js +83 -14
  129. package/neural-interface/public/shared/ui-whiteboard.js +2 -1
  130. package/neural-interface/public/variant/2d/main.js +4 -1
  131. package/neural-interface/public/variant/3d/main.js +3 -0
  132. package/neural-interface/public/vendor/highlight/LICENSE +29 -0
  133. package/neural-interface/public/vendor/highlight/github-dark-dimmed-11.12.0.min.css +9 -0
  134. package/neural-interface/public/vendor/highlight/highlight-11.12.0.min.js +1262 -0
  135. package/neural-interface/public/vendor/highlight/manifest.json +5 -0
  136. package/neural-interface/public/vendor/html2canvas/LICENSE +22 -0
  137. package/neural-interface/public/vendor/html2canvas/html2canvas-1.4.1.min.js +20 -0
  138. package/neural-interface/public/vendor/html2canvas/manifest.json +4 -0
  139. package/neural-interface/public/vendor/marked/LICENSE.md +44 -0
  140. package/neural-interface/public/vendor/marked/manifest.json +4 -0
  141. package/neural-interface/public/vendor/marked/marked-14.1.4.esm.js +2502 -0
  142. package/neural-interface/public/vendor/mermaid/LICENSE +21 -0
  143. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/abnfDiagram-PAR5IXJI.mjs +1 -0
  144. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/architecture-7GRP2DOG-M7G3SWCO.mjs +1 -0
  145. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/architectureDiagram-2PNWDENW.mjs +36 -0
  146. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/blockDiagram-PUWGUBOA.mjs +129 -0
  147. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/c4Diagram-3NLNHZP5.mjs +38 -0
  148. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-2NMSUPJH.mjs +1 -0
  149. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-32MCUW2P.mjs +206 -0
  150. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-3NPNU5GP.mjs +1 -0
  151. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-3SMGFRGC.mjs +1 -0
  152. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-4CAJECRU.mjs +14 -0
  153. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-4OADUZ5Q.mjs +10 -0
  154. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5IMINLNL.mjs +1 -0
  155. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5IQCK5RY.mjs +3 -0
  156. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5MVFYWFU.mjs +1 -0
  157. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-AQ6EADP3.mjs +1 -0
  158. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-AZZRMDJM.mjs +15 -0
  159. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-CLS4B6BI.mjs +168 -0
  160. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-DZP67EKU.mjs +70 -0
  161. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-EICP7K5X.mjs +1 -0
  162. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-GDY2UAB4.mjs +1 -0
  163. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-H7VHZCWX.mjs +1 -0
  164. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-IJBDOHL6.mjs +1 -0
  165. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JFFVMJIA.mjs +1 -0
  166. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JQRUD6KW.mjs +1 -0
  167. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JZGDI5JK.mjs +1 -0
  168. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-KD6SPIPN.mjs +2 -0
  169. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-NF366YI6.mjs +231 -0
  170. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-NUDPKBEF.mjs +32 -0
  171. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-OJINMHTS.mjs +1 -0
  172. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-Q6NGPIAH.mjs +2 -0
  173. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-QJSWEUOL.mjs +132 -0
  174. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-R7JDMUKV.mjs +1 -0
  175. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-RCXAM2GK.mjs +1 -0
  176. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-RT4MAWR3.mjs +1 -0
  177. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-SFMINX4W.mjs +1 -0
  178. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-SLDQ463B.mjs +88 -0
  179. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-STOV2HOB.mjs +1 -0
  180. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-T5OCTHI4.mjs +1 -0
  181. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-TCEHX7R3.mjs +1 -0
  182. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VQC2EELJ.mjs +1 -0
  183. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VRZKL3EH.mjs +1 -0
  184. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VTHDQ7BV.mjs +1 -0
  185. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VY2OSTJR.mjs +2 -0
  186. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-WUBWJARI.mjs +1 -0
  187. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-YBMQ5JGJ.mjs +2 -0
  188. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-YDMLL4PJ.mjs +1 -0
  189. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-Z6V44ARX.mjs +62 -0
  190. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZJJ7VQJP.mjs +339 -0
  191. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZNLK7HRR.mjs +1 -0
  192. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZQN7UHIL.mjs +1 -0
  193. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZS6VBONO.mjs +174 -0
  194. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZUNWM646.mjs +1 -0
  195. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-FGAMPOII.mjs +1 -0
  196. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-v2-AI5RJEK4.mjs +1 -0
  197. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cose-bilkent-Q72C2OJ3.mjs +1 -0
  198. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cynefin-OW5HDTMX-6OCTWOGG.mjs +1 -0
  199. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cynefinDiagram-2OOWUY4M.mjs +62 -0
  200. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/dagre-MPVFI544.mjs +4 -0
  201. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-AE4NWXFW.mjs +24 -0
  202. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-BUJID5UG.mjs +41 -0
  203. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-C44CZ5NI.mjs +24 -0
  204. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-EYOLEXXS.mjs +30 -0
  205. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-P7B6PWOJ.mjs +3 -0
  206. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ebnfDiagram-FTPZ3PV6.mjs +1 -0
  207. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/erDiagram-H3ERR26V.mjs +99 -0
  208. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/eventmodeling-NTZA5JFV-WKSWQHFS.mjs +1 -0
  209. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/flowDiagram-YHGXBVSY.mjs +1 -0
  210. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ganttDiagram-RWFTB7ED.mjs +292 -0
  211. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/gitGraph-4MIJSDKK-JH2HKYUO.mjs +1 -0
  212. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/gitGraphDiagram-MAR6GKOP.mjs +106 -0
  213. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/info-A6RAGUB7-3TX3OFWW.mjs +1 -0
  214. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/infoDiagram-LV547AFH.mjs +2 -0
  215. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ishikawaDiagram-PUJQGNDS.mjs +70 -0
  216. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/journeyDiagram-EW5QJT4W.mjs +139 -0
  217. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/kanban-definition-JSQERAZB.mjs +89 -0
  218. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/katex-CZ4GXH2S.mjs +257 -0
  219. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/mindmap-definition-VBJCLQLM.mjs +96 -0
  220. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/packet-AYTQ26CC-MKII6P3Y.mjs +1 -0
  221. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pegDiagram-7WILOQTJ.mjs +1 -0
  222. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pie-WAS4IAKB-6B4OPMJ5.mjs +1 -0
  223. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pieDiagram-EFA7CBSB.mjs +39 -0
  224. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/quadrantDiagram-WARVYA7P.mjs +7 -0
  225. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/radar-RG4KPBEZ-UPTR3XUL.mjs +1 -0
  226. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-74A4TZTK-MEFUPFMS.mjs +1 -0
  227. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-abnf-HS5TGJTU-R5YG2S5D.mjs +1 -0
  228. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-ebnf-LZEXJU2U-G5QEX2L6.mjs +1 -0
  229. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-peg-WCYAUIDC-H5I2ZRY2.mjs +1 -0
  230. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroadDiagram-5NIQVNLQ.mjs +1 -0
  231. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/requirementDiagram-SXREGOKZ.mjs +84 -0
  232. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sankeyDiagram-VV74M2VP.mjs +40 -0
  233. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sequenceDiagram-52ZFIFDL.mjs +162 -0
  234. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sizeCapture-FGA5AGYP.mjs +1 -0
  235. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-3QAKPIVI.mjs +1 -0
  236. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-v2-HN2P4RDZ.mjs +1 -0
  237. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/swimlanes-QLVP42QD.mjs +1 -0
  238. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/swimlanesDiagram-JKPDBFCN.mjs +8 -0
  239. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/timeline-definition-GS45XAXW.mjs +120 -0
  240. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/treeView-Q6P3EWNA-ZRJGANNQ.mjs +1 -0
  241. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/treemap-WGGIJYW6-K64VN5AS.mjs +1 -0
  242. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/vennDiagram-QIQU2ZRJ.mjs +34 -0
  243. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/wardley-WFR3VGLG-D67OWCCH.mjs +1 -0
  244. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/wardleyDiagram-U6YNM66N.mjs +78 -0
  245. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/xychartDiagram-XJAZEDNM.mjs +7 -0
  246. package/neural-interface/public/vendor/mermaid/manifest.json +4 -0
  247. package/neural-interface/public/vendor/mermaid/mermaid-11.17.2.esm.min.mjs +14 -0
  248. package/neural-interface/scripts/build-vendor.mjs +84 -3
  249. package/neural-interface/server.js +1099 -286
  250. package/npm-shrinkwrap.json +2 -2
  251. package/package.json +1 -1
  252. package/skills/synabun/modules/schedule.md +1 -1
@@ -36,6 +36,12 @@ export function opencodeImageParts(images = []) {
36
36
 
37
37
  /** The route-gate plugin (opencode-route-gate.js) as a file URL the serve can load. */
38
38
  export const OPENCODE_ROUTE_GATE_PLUGIN = pathToFileURL(resolve(dirname(fileURLToPath(import.meta.url)), 'opencode-route-gate.js')).href;
39
+ /**
40
+ * The same gate for OpenCode 2 (opencode2-route-gate/: OpenCode 2 loads a plugin
41
+ * folder from the config's `plugins` list). Both entries are written: OpenCode 1
42
+ * ignores `plugins`, OpenCode 2 skips the 1.x file (a warning in its log).
43
+ */
44
+ export const OPENCODE2_ROUTE_GATE_PLUGIN = pathToFileURL(resolve(dirname(fileURLToPath(import.meta.url)), 'opencode2-route-gate')).href;
39
45
 
40
46
  /**
41
47
  * SynaBun's plan agent. OpenCode adds its own read-only reminder ("commands may
@@ -96,9 +102,15 @@ export function patchOpenCodeAssistantConfig(xdgRoot, { persona = '', role = 'as
96
102
  // read-only classifier decides on the whole line.
97
103
  config.agent.plan = { ...(config.agent.plan || {}), permission: { ...(config.agent.plan?.permission || {}), bash: 'ask' } };
98
104
  const plugins = (Array.isArray(config.plugin) ? config.plugin : []).filter((entry) => (Array.isArray(entry) ? entry[0] : entry) !== OPENCODE_ROUTE_GATE_PLUGIN);
105
+ const plugins2 = (Array.isArray(config.plugins) ? config.plugins : []).filter((entry) => (entry && typeof entry === 'object' ? entry.package : entry) !== OPENCODE2_ROUTE_GATE_PLUGIN);
99
106
  // `remote`: a WhatsApp session's serve — the plugin refuses a call it could not check (fail closed).
100
- if (routeGate?.url && routeGate?.session && routeGate?.token) plugins.push([OPENCODE_ROUTE_GATE_PLUGIN, { url: routeGate.url, session: routeGate.session, token: routeGate.token, ...(routeGate.remote === true ? { remote: true } : {}) }]);
107
+ if (routeGate?.url && routeGate?.session && routeGate?.token) {
108
+ const options = { url: routeGate.url, session: routeGate.session, token: routeGate.token, ...(routeGate.remote === true ? { remote: true } : {}) };
109
+ plugins.push([OPENCODE_ROUTE_GATE_PLUGIN, options]);
110
+ plugins2.push({ package: OPENCODE2_ROUTE_GATE_PLUGIN, options });
111
+ }
101
112
  if (plugins.length) config.plugin = plugins; else delete config.plugin;
113
+ if (plugins2.length) config.plugins = plugins2; else delete config.plugins;
102
114
  writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
103
115
  // It carries this brain's grants (the desktop grant): readable by this user only.
104
116
  try { chmodSync(configPath, 0o600); } catch { /* a filesystem without modes */ }
@@ -0,0 +1,44 @@
1
+ // SynaBun — an Assistant task run's browser policy as an OpenCode 2 plugin.
2
+ // The same policy as ../opencode-browser-policy.js (read that file for what it
3
+ // does and why), for OpenCode 2's plugin API: a directory OpenCode loads from
4
+ // the config's `plugins` entry, whose default export is `{ id, setup(context) }`
5
+ // with a hook on `tool.execute.before`; a refusal is thrown. OpenCode 2 calls
6
+ // the shell tool `shell` and the sub-agent tool `subagent`: the policy, written
7
+ // for OpenCode 1's names, is asked with `bash` and `task`.
8
+
9
+ // An absolute file URL: the policy loads from SynaBun's lib/ wherever OpenCode imports this file from.
10
+ const POLICY_URL = new URL('../../browser-tool-policy.js', import.meta.url).href;
11
+ const V1_NAMES = { shell: 'bash', subagent: 'task' };
12
+
13
+ // OpenCode's own tools that run no command and fetch no page (OpenCode 1's names).
14
+ const OWN_TOOLS = new Set([
15
+ 'read', 'list', 'glob', 'grep', 'edit', 'write', 'patch', 'multiedit', 'apply_patch', 'todowrite', 'todoread',
16
+ 'task', 'skill', 'lsp', 'codesearch', 'question', 'batch', 'invalid', 'plan_exit', 'plan_enter',
17
+ ]);
18
+
19
+ export default {
20
+ id: 'synabun.browser-policy',
21
+ async setup(context) {
22
+ const options = context?.options || {};
23
+ let policy = null;
24
+ let loadError = null;
25
+ try { policy = await import(POLICY_URL); } catch (error) { loadError = error; }
26
+ const cwd = (typeof options.cwd === 'string' && options.cwd) || context?.location?.directory || process.cwd();
27
+ if (typeof policy?.browserToolDenial !== 'function') {
28
+ const why = loadError?.message || 'browserToolDenial is missing';
29
+ console.error(`[SynaBun] browser policy could not load from ${POLICY_URL} (${why}); refusing bash, webfetch, websearch and other servers' tools for this run`);
30
+ await context.tool.hook('execute.before', async (call) => {
31
+ const raw = String(call?.tool || '');
32
+ const tool = V1_NAMES[raw] || raw;
33
+ if (/^synabun_/i.test(tool) || OWN_TOOLS.has(tool.toLowerCase())) return;
34
+ throw new Error(`SynaBun browser policy: ${tool} was refused — SynaBun's browser policy could not load (${why}), so no command, web tool or other server's tool can be checked for a browser launch. SynaBun's own tools (SynaBun_*) and file tools still work. Stop and report this.`);
35
+ });
36
+ return;
37
+ }
38
+ await context.tool.hook('execute.before', async (call) => {
39
+ const raw = String(call?.tool || '');
40
+ const reason = policy.browserToolDenial(V1_NAMES[raw] || raw, call?.input || {}, { host: 'opencode', cwd });
41
+ if (reason) throw new Error(reason);
42
+ });
43
+ },
44
+ };
@@ -0,0 +1,57 @@
1
+ // SynaBun — the Assistant's route gate as an OpenCode 2 plugin.
2
+ // The same gate as ../opencode-route-gate.js (read that file for what it does
3
+ // and why), for OpenCode 2's plugin API: a directory OpenCode loads from the
4
+ // config's `plugins` entry ({ package: <this folder as a file URL>, options }),
5
+ // whose default export is `{ id, setup(context) }`; a hook on
6
+ // `tool.execute.before` sees `{ tool, sessionID, input }` and refuses a call by
7
+ // throwing (OpenCode records the message as that tool's error). OpenCode 2
8
+ // calls two tools by other names than OpenCode 1 (`shell` for bash,
9
+ // `subagent` for task): the gate is asked with OpenCode 1's names, which the
10
+ // rest of SynaBun knows. OpenCode 1 never reads this folder (it loads the
11
+ // `plugin` entry), and OpenCode 2 never loads the 1.x file.
12
+
13
+ const MAX_INPUT_BYTES = 256 * 1024;
14
+ const UNCHECKED = 'SynaBun could not check this call, so a WhatsApp session does not run it.';
15
+ const V1_NAMES = { shell: 'bash', subagent: 'task' };
16
+
17
+ /** { input, inputComplete }: the whole arguments when small enough to send, else their command/action. */
18
+ function callArguments(args) {
19
+ if (!args || typeof args !== 'object' || Array.isArray(args)) return { input: {}, inputComplete: false };
20
+ let size = Infinity;
21
+ try { size = JSON.stringify(args).length; } catch {}
22
+ if (size <= MAX_INPUT_BYTES) return { input: args, inputComplete: true };
23
+ const subset = {};
24
+ for (const key of ['command', 'action']) {
25
+ if (typeof args[key] === 'string' && args[key].length <= 64 * 1024) subset[key] = args[key];
26
+ }
27
+ return { input: subset, inputComplete: false };
28
+ }
29
+
30
+ export default {
31
+ id: 'synabun.route-gate',
32
+ async setup(context) {
33
+ const { url, session, token, remote = false } = context?.options || {};
34
+ if (!url || !session || !token) return;
35
+ const post = (body) => fetch(url, {
36
+ method: 'POST',
37
+ headers: { 'content-type': 'application/json' },
38
+ body: JSON.stringify({ session, token, host: 'opencode', ...body }),
39
+ signal: AbortSignal.timeout(5000),
40
+ });
41
+ // Never blocks loading; a lost hello only keeps the reactive fallback on.
42
+ post({ hello: true }).catch(() => {});
43
+ await context.tool.hook('execute.before', async (call) => {
44
+ const name = String(call?.tool || '');
45
+ let decision = null;
46
+ try {
47
+ const response = await post({ tool: V1_NAMES[name] || name, providerSessionId: call?.sessionID || null, ...callArguments(call?.input) });
48
+ decision = await response.json();
49
+ } catch {
50
+ if (remote === true) throw new Error(UNCHECKED);
51
+ return;
52
+ }
53
+ if (decision && decision.allow === false) throw new Error(String(decision.reason || 'SynaBun route gate: call agent_route first.'));
54
+ if (remote === true && !(decision && decision.allow === true)) throw new Error(UNCHECKED);
55
+ });
56
+ },
57
+ };
@@ -605,6 +605,8 @@ export function compactRow(row) {
605
605
  * @param {(change:{providers:string[], generatedAt:string})=>void} [deps.onChange] a build listed other
606
606
  * models than the build before it (hiding has its own event and does not count)
607
607
  * @param {number} [deps.retryMs] how soon a build is redone while Claude Code or Codex gave no verified list
608
+ * @param {boolean} [deps.staleWhileRevalidate] an old build (past its lifetime) is given at once and built
609
+ * again in the background, one build at a time; only a first build, invalidate() and force wait for one
608
610
  */
609
611
  export function createAssistantCatalog({
610
612
  fetchJson,
@@ -617,6 +619,7 @@ export function createAssistantCatalog({
617
619
  onChange = null,
618
620
  ttlMs = 30_000,
619
621
  retryMs = 5_000,
622
+ staleWhileRevalidate = false,
620
623
  now = Date.now,
621
624
  log = () => {},
622
625
  } = {}) {
@@ -753,7 +756,17 @@ export function createAssistantCatalog({
753
756
  */
754
757
  async function full({ force = false, deep = false } = {}) {
755
758
  if (!force && !deep && cache && now() - cache.at < cache.ttl) return cache.value;
759
+ // Past its lifetime: with staleWhileRevalidate the old build is the answer while the next one is made
760
+ // (never a build that began before an invalidate(): `current` says it was made for the lists as they are).
761
+ if (staleWhileRevalidate && !force && !deep && cache?.current) {
762
+ if (!inflight) startBuild({ deep: false }).catch((error) => log('catalog:background-error', error?.message || String(error)));
763
+ return cache.value;
764
+ }
756
765
  if (inflight) return inflight;
766
+ return startBuild({ deep });
767
+ }
768
+
769
+ function startBuild({ deep = false } = {}) {
757
770
  const started = epoch;
758
771
  inflight = build({ deep }).then((built) => {
759
772
  const { changedProviders, ...value } = built;
@@ -762,7 +775,7 @@ export function createAssistantCatalog({
762
775
  const degraded = ['claude-code', 'codex'].some((p) => value.sources[p] !== 'live');
763
776
  degradedBuilds = degraded ? degradedBuilds + 1 : 0;
764
777
  const ttl = degraded ? Math.min(ttlMs, retryMs * 2 ** (degradedBuilds - 1)) : ttlMs;
765
- cache = { at: now(), value, ttl: started === epoch ? ttl : 0 };
778
+ cache = { at: now(), value, ttl: started === epoch ? ttl : 0, current: started === epoch };
766
779
  if (changedProviders.length && typeof onChange === 'function') {
767
780
  try { onChange({ providers: changedProviders, generatedAt: value.generatedAt }); } catch (error) { log('catalog:onchange-error', error?.message || String(error)); }
768
781
  }
@@ -872,6 +885,8 @@ export function createAssistantCatalog({
872
885
  }
873
886
 
874
887
  function invalidate() { cache = null; epoch += 1; }
888
+ /** The lists may have changed (a serve restarted, a CLI updated): the next read still answers with this build and makes the next one. */
889
+ function markStale() { if (cache) cache = { ...cache, at: -Infinity }; }
875
890
  /** A provider's own list changed: drop the copy and build the next one now (onChange reports the difference). */
876
891
  function refresh() {
877
892
  invalidate();
@@ -879,5 +894,5 @@ export function createAssistantCatalog({
879
894
  return pending ? pending.catch(() => null).then(() => full()) : full();
880
895
  }
881
896
 
882
- return { get, full, peek, find, hiddenId, manageRows, manageSnapshot, brainInfo, invalidate, refresh };
897
+ return { get, full, peek, find, hiddenId, manageRows, manageSnapshot, brainInfo, invalidate, markStale, refresh };
883
898
  }
@@ -1894,6 +1894,9 @@ export function createAssistantDispatcher({
1894
1894
  liveState.turnTokens = null;
1895
1895
  entry.turns = [...(entry.turns || []), turn].slice(-50);
1896
1896
  entry.lastResult = parsed;
1897
+ // The run's own descriptor (lib/native-loop-runtime.js) carries it too, for the surfaces that read that one
1898
+ // (the OpenCode panel's pill): a warm worker whose turn ended asking waits on the user, it does not rest.
1899
+ ctx.setOutcome?.(parsed.status || null);
1897
1900
  entry.lastText = text.slice(0, 20_000);
1898
1901
  // The turn has ended: its result is in. Idle time from here on is not work.
1899
1902
  pauseWork(entry, liveState);
@@ -449,7 +449,7 @@ export function opencodeControlResponse(pending, response = {}) {
449
449
  export function createOpenCodeEnvelopeTranslator({ sessionId, emit, onTurnEnd = () => {}, onUsageEvent = () => {}, brain = {}, planTurn = () => false } = {}) {
450
450
  // costUsd is the session's running total: OpenCode reports cost per assistant
451
451
  // message, so costs are summed by message id (the runtime takes deltas).
452
- const state = { sessionId, roles: new Map(), order: [], textParts: new Map(), reasoningParts: new Map(), tools: new Map(), started: false, announcedInit: false, lastText: '', costUsd: 0, costByMessage: new Map(), usage: null };
452
+ const state = { sessionId, roles: new Map(), order: [], textParts: new Map(), reasoningParts: new Map(), partKinds: new Map(), tools: new Map(), started: false, announcedInit: false, lastText: '', costUsd: 0, costByMessage: new Map(), usage: null };
453
453
  const event = (payload) => emit({ type: 'event', event: payload });
454
454
  const belongs = (ev) => {
455
455
  const sid = ev?.sessionID || ev?.sessionId || ev?.part?.sessionID || ev?.info?.sessionID || ev?.info?.id || ev?.session?.id || null;
@@ -485,12 +485,15 @@ export function createOpenCodeEnvelopeTranslator({ sessionId, emit, onTurnEnd =
485
485
  function handlePart(part = {}) {
486
486
  if (!part.messageID) return;
487
487
  if (state.roles.get(part.messageID) === 'user') return;
488
+ if ((part.type === 'text' || part.type === 'reasoning') && part.id) state.partKinds.set(part.id, { messageID: part.messageID, type: part.type });
488
489
  if (part.type === 'text' && typeof part.text === 'string') {
489
490
  let parts = state.textParts.get(part.messageID);
490
491
  if (!parts) { parts = new Map(); state.textParts.set(part.messageID, parts); }
491
492
  const key = part.id || `${parts.size}`;
492
493
  const prev = parts.get(key) || '';
493
494
  const next = part.text;
495
+ // An update made before the deltas that already arrived streams nothing again.
496
+ if (prev.length > next.length && prev.startsWith(next)) return;
494
497
  parts.set(key, next);
495
498
  const delta = next.startsWith(prev) ? next.slice(prev.length) : next;
496
499
  const shown = delta && planTurn() === true ? planTextDelta(`${part.messageID}:${key}`, delta) : delta;
@@ -500,6 +503,7 @@ export function createOpenCodeEnvelopeTranslator({ sessionId, emit, onTurnEnd =
500
503
  if (part.type === 'reasoning' && typeof part.text === 'string') {
501
504
  const key = `${part.messageID}:${part.id || 'r'}`;
502
505
  const prev = state.reasoningParts.get(key) || '';
506
+ if (prev.length > part.text.length && prev.startsWith(part.text)) return;
503
507
  state.reasoningParts.set(key, part.text);
504
508
  const delta = part.text.startsWith(prev) ? part.text.slice(prev.length) : part.text;
505
509
  if (delta) { startIfNeeded(); event(thinkingDelta(delta)); }
@@ -521,6 +525,19 @@ export function createOpenCodeEnvelopeTranslator({ sessionId, emit, onTurnEnd =
521
525
  }
522
526
  }
523
527
  }
528
+ // A part's text arrives as `message.part.delta` between the update that announces the part and the one that
529
+ // ends it (OpenCode 2 streams only this way, through lib/opencode2-translate.js): each delta extends that part,
530
+ // and streams as an update would. A delta for a part not announced yet is left to the update that ends it,
531
+ // which carries the whole text.
532
+ function handleDelta(ev = {}) {
533
+ if ((ev.field && ev.field !== 'text') || typeof ev.delta !== 'string' || !ev.delta) return;
534
+ const known = state.partKinds.get(ev.partID);
535
+ if (!known || (ev.messageID && ev.messageID !== known.messageID)) return;
536
+ const prev = known.type === 'text'
537
+ ? state.textParts.get(known.messageID)?.get(ev.partID) || ''
538
+ : state.reasoningParts.get(`${known.messageID}:${ev.partID}`) || '';
539
+ handlePart({ id: ev.partID, messageID: known.messageID, type: known.type, text: `${prev}${ev.delta}` });
540
+ }
524
541
  return {
525
542
  state,
526
543
  handle(eventType, ev = {}) {
@@ -546,6 +563,7 @@ export function createOpenCodeEnvelopeTranslator({ sessionId, emit, onTurnEnd =
546
563
  return true;
547
564
  }
548
565
  if (/^message[.:]part[.:]updated$/i.test(eventType)) { handlePart(ev.part || {}); return true; }
566
+ if (/^message[.:]part[.:]delta$/i.test(eventType)) { handleDelta(ev); return true; }
549
567
  if (/^session[.:]idle$/i.test(eventType)) {
550
568
  if (state.errored) { state.errored = false; state.started = false; return true; } // error already closed this turn
551
569
  const said = latestAssistantText();
@@ -9,7 +9,7 @@
9
9
  // obligation nudges. All server dependencies are injected.
10
10
 
11
11
  import { createHash, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
12
- import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
12
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
13
13
  import { dirname, resolve } from 'node:path';
14
14
  import { createClaudeBrain } from './assistant-brains/claude.js';
15
15
  import { createCodexBrain } from './assistant-brains/codex.js';
@@ -252,6 +252,8 @@ export function createAssistantRuntime({
252
252
  if (!dataDir) throw new Error('createAssistantRuntime requires dataDir');
253
253
  const settings = {
254
254
  idleDisposeMs: Math.max(60_000, Number(config.idleDisposeMinutes || 120) * 60_000),
255
+ // A draft (a session the panel opened, before its first prompt) with no socket attached goes after this.
256
+ draftIdleMs: Math.max(60_000, Number(config.draftIdleMinutes || 10) * 60_000),
255
257
  // A remote session on OpenCode waits this long for its gate plugin's hello before a turn (else refused).
256
258
  remoteGateHelloMs: Math.max(0, Number(config.remoteGateHelloMs ?? 5000)),
257
259
  // An OpenCode turn's end waits this long for the serve's stored messages (the usage meter's
@@ -295,11 +297,30 @@ export function createAssistantRuntime({
295
297
 
296
298
  // ── persistence ────────────────────────────────────────────────────────────
297
299
  function transcriptPath(id) { return resolve(dataDir, 'assistant', id, 'transcript.jsonl'); }
300
+ function transcriptHasEntries(id) { try { return statSync(transcriptPath(id)).size > 0; } catch { return false; } }
301
+ function transcriptHasPrompt(id) { try { return readFileSync(transcriptPath(id), 'utf8').includes('"synabun.user_prompt"'); } catch { return false; } }
302
+ /**
303
+ * A record from before `chatStartedAt`: its chat started when it ran a turn, adopted a provider
304
+ * conversation or holds a human prompt (the time is its best guess). One that never did is listed
305
+ * nowhere; with nothing in it at all (no transcript, no runs, not remote, no spend) it is dropped.
306
+ */
307
+ function settleLegacyRow(row) {
308
+ if (row.chatStartedAt) return 'started';
309
+ if (Number(row.turns) > 0 || row.providerSessionId || row.providerThreadId || transcriptHasPrompt(row.id)) {
310
+ row.chatStartedAt = row.lastActiveAt || row.updatedAt || row.createdAt || iso(now());
311
+ return 'started';
312
+ }
313
+ const holds = transcriptHasEntries(row.id) || (Array.isArray(row.dispatchRunIds) && row.dispatchRunIds.length > 0)
314
+ || !!row.remote || !!row.channel || Number(row.costUsd) > 0;
315
+ return holds ? 'unstarted' : 'empty';
316
+ }
298
317
  function loadIndex() {
299
318
  const rows = readJson(indexPath, []);
300
319
  if (!Array.isArray(rows)) return;
320
+ let dropped = 0;
301
321
  for (const row of rows) {
302
322
  if (!row?.id) continue;
323
+ if (settleLegacyRow(row) === 'empty') { dropped += 1; continue; }
303
324
  sessions.set(row.id, makeLive({ ...row, status: row.status === 'ended' ? 'ended' : 'idle', windowIds: [] }));
304
325
  // A remote session nobody registered again since the restart reads as read-only.
305
326
  if (row.remote) {
@@ -307,11 +328,14 @@ export function createAssistantRuntime({
307
328
  try { remotePolicy?.setSessionBrain?.(row.id, row.brain?.provider || null); } catch {}
308
329
  }
309
330
  }
331
+ // The index is written again without them (and with the started marks).
332
+ if (dropped) { log('assistant:index', `${dropped} session record(s) without a conversation dropped`); persist(); }
310
333
  }
311
334
  let persistTimer = null;
312
335
  function persist({ force = false } = {}) {
313
336
  const write = () => {
314
- const rows = [...sessions.values()].map((live) => publicRecord(live.record, { withLive: false, persisted: true }))
337
+ // A draft is never written: it exists on disk only once its first prompt was sent (startChat).
338
+ const rows = [...sessions.values()].filter((live) => !live.draft).map((live) => publicRecord(live.record, { withLive: false, persisted: true }))
315
339
  .sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')))
316
340
  .slice(0, settings.retention);
317
341
  try { writeJsonAtomic(indexPath, rows); } catch (error) { log('assistant:index-error', error?.message || String(error)); }
@@ -341,6 +365,9 @@ export function createAssistantRuntime({
341
365
  function makeLive(record) {
342
366
  return {
343
367
  record, brain: null, brainProvider: null, brainMeter: null, brainMeterProvider: null, turnTaskId: null, turnTasks: [], sockets: new Set(), buffer: [], running: false, pendingControls: new Map(), controlTimers: new Map(),
368
+ // A draft: opened by a panel and not chatted in yet. Kept in memory only, listed nowhere, and
369
+ // gone when it is closed or left without a socket (draftTimer); its first prompt stores it (startChat).
370
+ draft: false, draftTimer: null,
344
371
  mailbox: [], mailboxTimer: null, obligations: new Set(), rememberedThisTurn: false, disposeTimer: null, personaText: '',
345
372
  // The last human prompt and where it came from ('ui', 'whatsapp', 'api'): a route continuation
346
373
  // answers there (lastPromptOrigin; the transcript's after a restart).
@@ -909,7 +936,9 @@ export function createAssistantRuntime({
909
936
  out.testDensityExplicit = normalizeTestDensity(record.testDensity, null);
910
937
  // `rename`: this server keeps a rename on the record (userRenamed), so a panel writes it here.
911
938
  // `adminSandbox`: the global setting is served here (PUT /api/assistant/admin-sandbox).
912
- out.features = { routing: !!router, computer: computerSupported(), testDensity: true, rename: true, adminSandbox: !!adminSandbox };
939
+ // `drafts`: a session is stored and listed from its first prompt on; `remove`: DELETE stops its work and purges it.
940
+ out.features = { routing: !!router, computer: computerSupported(), testDensity: true, rename: true, adminSandbox: !!adminSandbox, drafts: true, remove: true };
941
+ out.draft = live.draft === true;
913
942
  if (adminSandbox) { try { out.adminSandbox = adminSandbox.enabled() === true; } catch { out.adminSandbox = false; } }
914
943
  out.budget = sessionMoney(record.id);
915
944
  out.remoteLevel = policy ? effectiveLevel(policy, { untrusted: live.turnUntrusted === true, now: now() }) : null;
@@ -2302,6 +2331,8 @@ export function createAssistantRuntime({
2302
2331
  live.lastPromptOrigin = origin ? String(origin) : null;
2303
2332
  // A prompt from elsewhere (WhatsApp, the API) is news to the panel: it goes live too.
2304
2333
  const promptPacket = { type: 'event', event: { type: 'synabun.user_prompt', text: prompt, images: images.length, ...(origin && origin !== 'ui' ? { origin: String(origin) } : {}) } };
2334
+ // The chat starts with its first human prompt: from here on the session is stored and listed.
2335
+ startChat(live);
2305
2336
  appendTranscript(live.record.id, { at: iso(now()), packet: promptPacket });
2306
2337
  if (origin && origin !== 'ui') sendToSockets(live, promptPacket);
2307
2338
  if (live.brainProvider !== 'claude-code' && memory?.recallForPrompt) {
@@ -2672,8 +2703,56 @@ export function createAssistantRuntime({
2672
2703
  }
2673
2704
 
2674
2705
  // ── public API ─────────────────────────────────────────────────────────────
2706
+ /**
2707
+ * Every session list (REST, the socket's list_sessions, the catalog view) reads this: only sessions
2708
+ * whose chat started. A draft, or a record nobody ever prompted, is in none of them. `activeRuns`:
2709
+ * the workers it dispatched that still run (a removal stops them, and its question says so).
2710
+ */
2675
2711
  function listSessions() {
2676
- return [...sessions.values()].map((live) => view(live)).sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')));
2712
+ const runs = new Map();
2713
+ try {
2714
+ for (const run of dispatcher?.list?.({ activeOnly: true, limit: 500 }) || []) {
2715
+ if (run?.assistantSessionId) runs.set(run.assistantSessionId, (runs.get(run.assistantSessionId) || 0) + 1);
2716
+ }
2717
+ } catch {}
2718
+ return [...sessions.values()].filter((live) => !live.draft && !!live.record.chatStartedAt)
2719
+ .map((live) => ({ ...view(live), activeRuns: runs.get(live.record.id) || 0 }))
2720
+ .sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')));
2721
+ }
2722
+ /**
2723
+ * The first human prompt (or a provider conversation adopted with `resume`) starts the chat: the
2724
+ * record is marked, and a draft becomes a stored session (written now, listed from now on).
2725
+ */
2726
+ function startChat(live) {
2727
+ if (live.record.chatStartedAt && !live.draft) return;
2728
+ live.record.chatStartedAt ||= iso(now());
2729
+ const wasDraft = live.draft;
2730
+ live.draft = false;
2731
+ if (live.draftTimer) { clearTimeout(live.draftTimer); live.draftTimer = null; }
2732
+ persist({ force: true });
2733
+ if (wasDraft) broadcast('assistant:session-created', live);
2734
+ }
2735
+ /** A draft with no socket attached is removed after settings.draftIdleMs (a page that went and never came back). */
2736
+ function armDraftExpiry(live) {
2737
+ if (live.draftTimer) { clearTimeout(live.draftTimer); live.draftTimer = null; }
2738
+ if (!live.draft || live.sockets.size || shuttingDown || sessions.get(live.record.id) !== live) return;
2739
+ live.draftTimer = setTimeout(() => {
2740
+ live.draftTimer = null;
2741
+ if (!live.draft || live.sockets.size || sessions.get(live.record.id) !== live || sessionBusy(live) || live.closing) return;
2742
+ destroySession(live.record.id).catch((error) => log('assistant:draft-expiry', error?.message || String(error)));
2743
+ }, settings.draftIdleMs);
2744
+ live.draftTimer.unref?.();
2745
+ }
2746
+ /**
2747
+ * Whether closing a session keeps its record (ended, reopenable) or removes it: a session that
2748
+ * never held a conversation (no prompt, no turn, no runs, nothing in its transcript, not remote,
2749
+ * no work under way) leaves nothing behind.
2750
+ */
2751
+ function holdsConversation(live) {
2752
+ const r = live.record;
2753
+ return !!r.chatStartedAt || Number(r.turns) > 0 || (Array.isArray(r.dispatchRunIds) && r.dispatchRunIds.length > 0)
2754
+ || !!r.providerSessionId || !!r.providerThreadId || !!r.remote || !!r.channel || Number(r.costUsd) > 0
2755
+ || sessionBusy(live) || !!workReserved(live) || transcriptHasEntries(r.id);
2677
2756
  }
2678
2757
  function getSession(id, { transcript = true, limit = 400 } = {}) {
2679
2758
  const live = sessions.get(id);
@@ -2698,7 +2777,7 @@ export function createAssistantRuntime({
2698
2777
  if (!row) throw Object.assign(new Error('Auto requires a signed-in subscription account.'), { code: 'ACCOUNT_NO_SUBSCRIPTION', status: 409 });
2699
2778
  return { ...brain, accountId: row.id };
2700
2779
  }
2701
- async function createSession({ brain = {}, label = null, cwd = null, windowId = null, fallback = false, channel = null } = {}, internal = {}) {
2780
+ async function createSession({ brain = {}, label = null, cwd = null, windowId = null, fallback = false, channel = null, draft = false } = {}, internal = {}) {
2702
2781
  const id = `assistant-${randomUUID()}`;
2703
2782
  const requested = normalizeBrain({ ...brain, cwd: brain?.cwd || cwd || settings.defaultBrain.cwd || PACKAGE_ROOT }, settings.defaultBrain, { catalog: catalog?.peek?.() || null });
2704
2783
  const enabled = await enabledBrain(requested, { fallback: fallback === true, named: !!brain?.model });
@@ -2729,8 +2808,16 @@ export function createAssistantRuntime({
2729
2808
  live.creating = false;
2730
2809
  record.brain = clampedBrain(live, record.brain);
2731
2810
  }
2732
- persist({ force: true });
2733
- broadcast('assistant:session-created', live);
2811
+ // `draft` (a panel's new session, POST /sessions): stored and announced by its first prompt
2812
+ // (startChat); until then it lives here only, and is removed when it is closed or left (armDraftExpiry).
2813
+ // A remote session is never one: its bridge sends the prompt it was made for.
2814
+ if (draft === true && !remote && !record.channel) {
2815
+ live.draft = true;
2816
+ armDraftExpiry(live);
2817
+ } else {
2818
+ persist({ force: true });
2819
+ broadcast('assistant:session-created', live);
2820
+ }
2734
2821
  return enabled.fallback ? { ...view(live), fallback: enabled.fallback } : view(live);
2735
2822
  }
2736
2823
  function applyRouteMode(live, mode) {
@@ -2830,6 +2917,12 @@ export function createAssistantRuntime({
2830
2917
  }
2831
2918
  async function closeSession(id) {
2832
2919
  const live = requireLive(id);
2920
+ // Never chatted in (a draft, a record nobody prompted): closing it leaves nothing behind.
2921
+ if (!holdsConversation(live)) {
2922
+ const gone = { ...view(live), status: 'destroyed', removed: true };
2923
+ await destroySession(id);
2924
+ return gone;
2925
+ }
2833
2926
  const closed = await endSessionWork(live);
2834
2927
  try {
2835
2928
  await disposeBrain(live, 'closed');
@@ -2847,6 +2940,9 @@ export function createAssistantRuntime({
2847
2940
  if (!live) return false;
2848
2941
  const closed = await endSessionWork(live);
2849
2942
  try {
2943
+ if (sessions.get(id) !== live) return false; // another removal got there first
2944
+ if (live.draftTimer) { clearTimeout(live.draftTimer); live.draftTimer = null; }
2945
+ if (live.disposeTimer) { clearTimeout(live.disposeTimer); live.disposeTimer = null; }
2850
2946
  await disposeBrain(live, 'closed');
2851
2947
  for (const ws of live.sockets) { try { ws.close(1000, 'session destroyed'); } catch {} }
2852
2948
  sessions.delete(id);
@@ -2863,6 +2959,24 @@ export function createAssistantRuntime({
2863
2959
  } finally { closed(); }
2864
2960
  return true;
2865
2961
  }
2962
+ /**
2963
+ * The Sessions menu's Remove (DELETE /api/assistant/sessions/:id): the session goes for good, in
2964
+ * every host and list, and its work goes with it: the workers it dispatched that still run are
2965
+ * stopped (no orphaned run is left), then the running turn and the brain (destroySession). Only
2966
+ * the record, its transcript and its usage journal are removed: memories made from it (its
2967
+ * assistant-threads record included) stay, and so do the dispatcher's records of its runs.
2968
+ */
2969
+ async function removeSession(id) {
2970
+ const live = sessions.get(String(id || ''));
2971
+ if (!live) return { removed: false, stoppedRuns: [] };
2972
+ let stoppedRuns = [];
2973
+ try {
2974
+ const result = await dispatcher?.killAll?.({ assistantSessionId: live.record.id, reason: 'session_removed' });
2975
+ stoppedRuns = Array.isArray(result?.stopped) ? result.stopped : [];
2976
+ } catch (error) { log('assistant:remove', `${live.record.id}: stopping its runs failed (${error?.message || error})`); }
2977
+ const removed = await destroySession(live.record.id);
2978
+ return { removed, stoppedRuns };
2979
+ }
2866
2980
  /**
2867
2981
  * MCP terminal pin → assistant session id. Claude brains pin the session id
2868
2982
  * itself; the OpenCode brain pins `assistant-oc-<24>` (its isolated serve).
@@ -3127,6 +3241,7 @@ export function createAssistantRuntime({
3127
3241
  ws.on('close', () => {
3128
3242
  live.sockets.delete(ws);
3129
3243
  scheduleDispose(live);
3244
+ armDraftExpiry(live);
3130
3245
  });
3131
3246
  if (held) {
3132
3247
  while (live.closing) await live.closing;
@@ -3137,6 +3252,7 @@ export function createAssistantRuntime({
3137
3252
  if (ws.readyState !== 1) return; // gone while it waited
3138
3253
  }
3139
3254
  if (live.disposeTimer) { clearTimeout(live.disposeTimer); live.disposeTimer = null; }
3255
+ if (live.draftTimer) { clearTimeout(live.draftTimer); live.draftTimer = null; }
3140
3256
  live.sockets.add(ws);
3141
3257
  live.record.status = live.record.status === 'ended' ? 'idle' : live.record.status;
3142
3258
  const engine = live.brain ? { type: 'engine', engine: live.brainProvider === 'claude-code' ? 'sdk' : live.brainProvider, brain: live.brainProvider, sdkVersion } : { type: 'engine', engine: 'assistant', brain: live.record.brain.provider, sdkVersion };
@@ -3268,6 +3384,8 @@ export function createAssistantRuntime({
3268
3384
  if (msg.providerSessionId) {
3269
3385
  const moved = msg.providerSessionId !== live.record.providerSessionId;
3270
3386
  live.record.providerSessionId = msg.providerSessionId;
3387
+ // A provider conversation adopted (/resume <id>): the session holds a chat now.
3388
+ startChat(live);
3271
3389
  // Another provider session: the meter's baseline was the old one's.
3272
3390
  if (moved) resetMeterIdentity(live, msg.providerSessionId);
3273
3391
  // …and so was the frozen user-context digest.
@@ -3728,6 +3846,7 @@ export function createAssistantRuntime({
3728
3846
  for (const live of sessions.values()) {
3729
3847
  if (live.mailboxTimer) clearTimeout(live.mailboxTimer);
3730
3848
  if (live.disposeTimer) clearTimeout(live.disposeTimer);
3849
+ if (live.draftTimer) clearTimeout(live.draftTimer);
3731
3850
  if (live.workTimer) clearTimeout(live.workTimer);
3732
3851
  await disposeBrain(live, 'shutdown').catch(() => {});
3733
3852
  live.record.status = 'idle';
@@ -3754,7 +3873,7 @@ export function createAssistantRuntime({
3754
3873
  broadcastSync = (message) => { try { originalBroadcast(message); } catch {} if (message?.type === 'assistant:permission-request' || message?.type === 'assistant:permission-resolved') onPermissionBroadcast(message); };
3755
3874
 
3756
3875
  return {
3757
- handleWebSocket, listSessions, getSession, createSession, updateSession, closeSession, destroySession,
3876
+ handleWebSocket, listSessions, getSession, createSession, updateSession, closeSession, destroySession, removeSession,
3758
3877
  resolveTerminal, accountInUse, ownsIsolatedServe, noteDispatch, subscribe, shutdown,
3759
3878
  observeBroadcast: onPermissionBroadcast,
3760
3879
  testDensityFor, routerSession, routerSend, routerEvent, routerMailbox, routerContinue, routerSinks, routerRouted, routerRouteFailed, routerComputerApproved, gateCheck, isPlanning, isPlanReadOnly,
@@ -43,7 +43,39 @@ const DISCOVERY_TIMEOUT_MS = 15000;
43
43
  const CACHE_TTL_MS = 10 * 60 * 1000;
44
44
 
45
45
  let _cache = { at: 0, key: '', models: [] };
46
- let _inflight = null;
46
+ const _inflight = new Map(); // cache key → the discovery running for it
47
+ // Where the last verified list is kept across restarts ({ read(), write(entry) },
48
+ // lib/launch-cache.js createSnapshotStore): a start answers with it at once and
49
+ // asks the CLI again in the background.
50
+ let _store = null;
51
+ let _storeRead = false;
52
+
53
+ export function setClaudeModelStore(store) {
54
+ _store = store && typeof store.read === 'function' && typeof store.write === 'function' ? store : null;
55
+ _storeRead = false;
56
+ }
57
+
58
+ // The list the last run verified, for this very binary and account: given at once
59
+ // after a restart (as an old answer, so the first read asks the CLI again).
60
+ function adoptStoredModels(key) {
61
+ if (_storeRead || !_store) return;
62
+ _storeRead = true;
63
+ if (_cache.models.length) return;
64
+ try {
65
+ const saved = _store.read();
66
+ const models = saved?.key === key && Array.isArray(saved.models)
67
+ ? saved.models.filter(m => m && typeof m === 'object' && typeof m.id === 'string' && m.id)
68
+ : [];
69
+ if (models.length) _cache = { at: 0, key, models };
70
+ } catch { /* nothing kept: the first read asks the CLI */ }
71
+ }
72
+
73
+ // What the list depends on: the binary (an update changes it, binaryIdentity) and
74
+ // the account it was read under (another account can be entitled to other models).
75
+ function cacheKeyOf(bin, account) {
76
+ const who = String(account || '');
77
+ return who ? `${binaryIdentity(bin)}#${who}` : binaryIdentity(bin);
78
+ }
47
79
 
48
80
  // Who else keeps a copy of this list (the Assistant's model catalog). Told when a
49
81
  // discovery returns a list that differs from the one before it, so a copy is
@@ -286,37 +318,55 @@ export function queryClaudeCliModels(claudeBin, { cwd, timeoutMs = DISCOVERY_TIM
286
318
  }
287
319
 
288
320
  // Cached discovery. `source` lets the UI distinguish a verified list from the
289
- // baked-in fallback. Cache is keyed on the binary's identity, so switching the
290
- // configured CLI (Settings > Terminal) or updating it re-discovers.
291
- export async function discoverClaudeModels(claudeBin, { force = false, cwd, query = queryClaudeCliModels } = {}) {
321
+ // baked-in fallback. Cache is keyed on the binary's identity (and the account
322
+ // the list was read under, `account`), so switching the configured CLI
323
+ // (Settings > Terminal), updating it or signing in to another account
324
+ // re-discovers. A list that is merely old is still given at once, and the CLI is
325
+ // asked again in the background (one discovery at a time): only a first read, a
326
+ // changed key or `force` waits for the CLI.
327
+ export async function discoverClaudeModels(claudeBin, { force = false, cwd, query = queryClaudeCliModels, account = '' } = {}) {
292
328
  const bin = String(claudeBin || '');
293
- const key = binaryIdentity(bin);
294
- const fresh = _cache.models.length
295
- && _cache.key === key
296
- && Date.now() - _cache.at < CACHE_TTL_MS;
297
- if (!force && fresh) return { source: 'cache', models: _cache.models };
298
- if (_inflight) return _inflight;
299
-
300
- _inflight = (async () => {
329
+ const key = cacheKeyOf(bin, account);
330
+ adoptStoredModels(key);
331
+ const known = _cache.models.length && _cache.key === key;
332
+ if (!force && known) {
333
+ if (Date.now() - _cache.at >= CACHE_TTL_MS) discover(bin, key, cwd, query).catch(() => {});
334
+ return { source: 'cache', models: _cache.models };
335
+ }
336
+ return discover(bin, key, cwd, query);
337
+ }
338
+
339
+ function discover(bin, key, cwd, query) {
340
+ if (_inflight.has(key)) return _inflight.get(key);
341
+ const job = (async () => {
301
342
  try {
302
343
  const raw = await query(bin, { cwd });
303
344
  const models = normalizeClaudeModels(raw);
304
345
  if (models.length) {
305
346
  const changed = claudeModelsSignature(models) !== claudeModelsSignature(_cache.models);
306
347
  _cache = { at: Date.now(), key, models };
348
+ try { _store?.write({ key, at: _cache.at, models }); } catch { /* kept in memory still */ }
307
349
  if (changed) notifyModelsChanged(models);
308
350
  return { source: 'cli', models };
309
351
  }
310
352
  return { source: 'fallback', models: [...CLAUDE_FALLBACK_MODELS] };
311
353
  } finally {
312
- _inflight = null;
354
+ if (_inflight.get(key) === job) _inflight.delete(key);
313
355
  }
314
356
  })();
315
- return _inflight;
357
+ _inflight.set(key, job);
358
+ return job;
359
+ }
360
+
361
+ /** The kept list is due again: the next read still answers with it and asks the CLI in the background. */
362
+ export function markClaudeModelsStale() {
363
+ _cache = { ..._cache, at: 0 };
316
364
  }
317
365
 
318
366
  export function clearClaudeModelCache() {
319
367
  _cache = { at: 0, key: '', models: [] };
368
+ _inflight.clear();
369
+ _storeRead = !!_store;
320
370
  _skewCache = { key: null, result: undefined };
321
371
  _skewInflight.clear();
322
372
  }