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.
- package/CHANGELOG.md +29 -0
- package/README.md +1 -1
- package/THIRD-PARTY-LICENSES.md +46 -4
- package/launcher.mjs +9 -3
- package/lib/start-launcher.js +6 -1
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/neural-interface/i18n/en.json +218 -7
- package/neural-interface/i18n/pt-BR.json +220 -9
- package/neural-interface/lib/account-usage.js +5 -3
- package/neural-interface/lib/app-controls-api.js +161 -0
- package/neural-interface/lib/assistant-api.js +21 -7
- package/neural-interface/lib/assistant-brains/opencode.js +13 -1
- package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +44 -0
- package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +57 -0
- package/neural-interface/lib/assistant-catalog.js +17 -2
- package/neural-interface/lib/assistant-dispatch.js +3 -0
- package/neural-interface/lib/assistant-envelope.js +19 -1
- package/neural-interface/lib/assistant-runtime.js +127 -8
- package/neural-interface/lib/claude-model-catalog.js +64 -14
- package/neural-interface/lib/codex-settings-schema.js +109 -47
- package/neural-interface/lib/editor-completion-opencode.js +132 -35
- package/neural-interface/lib/exec-loop-command.js +8 -1
- package/neural-interface/lib/launch-cache.js +293 -0
- package/neural-interface/lib/login-start.js +524 -0
- package/neural-interface/lib/native-loop-providers.js +18 -3
- package/neural-interface/lib/native-loop-runtime.js +19 -0
- package/neural-interface/lib/opencode-db-schema.js +75 -0
- package/neural-interface/lib/opencode-history.js +16 -1
- package/neural-interface/lib/opencode-protocol.js +173 -0
- package/neural-interface/lib/opencode-provider-view.js +81 -0
- package/neural-interface/lib/opencode-runtime-config.js +10 -3
- package/neural-interface/lib/opencode-serve-records.js +606 -0
- package/neural-interface/lib/opencode-v2-client.js +33 -6
- package/neural-interface/lib/opencode-v2-ws-requests.js +19 -5
- package/neural-interface/lib/opencode-worker-activity.js +229 -0
- package/neural-interface/lib/opencode2-client.js +760 -0
- package/neural-interface/lib/opencode2-proxy.js +116 -0
- package/neural-interface/lib/opencode2-translate.js +1008 -0
- package/neural-interface/lib/process-identity.js +361 -0
- package/neural-interface/lib/session-cache-builder.js +26 -5
- package/neural-interface/lib/session-title-generator.js +18 -0
- package/neural-interface/lib/style-guide/api.js +4 -3
- package/neural-interface/lib/untrusted-file-headers.js +27 -0
- package/neural-interface/package-lock.json +1343 -45
- package/neural-interface/package.json +5 -1
- package/neural-interface/public/index.html +2 -2
- package/neural-interface/public/shared/agent-runtime-options.js +55 -11
- package/neural-interface/public/shared/assistant/asst-agents-dock.js +172 -17
- package/neural-interface/public/shared/assistant/asst-api.js +11 -3
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +195 -80
- package/neural-interface/public/shared/assistant/asst-composer.js +1 -1
- package/neural-interface/public/shared/assistant/asst-hosts.js +49 -2
- package/neural-interface/public/shared/assistant/asst-menu.js +77 -6
- package/neural-interface/public/shared/assistant/asst-panel.js +197 -33
- package/neural-interface/public/shared/assistant/asst-rack-styles.js +28 -10
- package/neural-interface/public/shared/assistant/asst-redact.js +186 -0
- package/neural-interface/public/shared/assistant/asst-render.js +382 -36
- package/neural-interface/public/shared/assistant/asst-run-stage.js +231 -0
- package/neural-interface/public/shared/assistant/asst-sanitize.js +193 -3
- package/neural-interface/public/shared/assistant/asst-scene-styles.js +273 -0
- package/neural-interface/public/shared/assistant/asst-scenes.js +528 -0
- package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +3 -15
- package/neural-interface/public/shared/assistant/asst-state.js +14 -0
- package/neural-interface/public/shared/assistant/asst-styles.js +43 -0
- package/neural-interface/public/shared/assistant/asst-tool-kinds.js +13 -1
- package/neural-interface/public/shared/cc-dropdown.js +480 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +13 -1
- package/neural-interface/public/shared/cdx/cdx-render.js +15 -3
- package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
- package/neural-interface/public/shared/cdx/cdx-tabs.js +149 -35
- package/neural-interface/public/shared/cp/cp-markdown.js +4 -57
- package/neural-interface/public/shared/data-cache.js +223 -0
- package/neural-interface/public/shared/desktop-bridge.js +11 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +78 -1
- package/neural-interface/public/shared/desktop-titlebar.js +214 -28
- package/neural-interface/public/shared/desktop-update-view.js +15 -5
- package/neural-interface/public/shared/file-links.js +73 -8
- package/neural-interface/public/shared/html-shell.js +74 -80
- package/neural-interface/public/shared/launch-prefetch.js +59 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-caps.js +3 -2
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +7 -2
- package/neural-interface/public/shared/ocp-v2/ocp-v2-modelpicker.js +54 -17
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +93 -2
- package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +3 -2
- package/neural-interface/public/shared/ocp-v2/ocp-v2-providers.js +11 -9
- package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +15 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +1 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send-logic.js +401 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +16 -9
- package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +60 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +48 -19
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +195 -19
- package/neural-interface/public/shared/opencode-providers.js +39 -0
- package/neural-interface/public/shared/schedules-view.js +318 -0
- package/neural-interface/public/shared/settings/settings-app-pane.js +335 -0
- package/neural-interface/public/shared/settings/settings-app.js +243 -0
- package/neural-interface/public/shared/settings/settings-data.js +88 -0
- package/neural-interface/public/shared/settings/settings-ia.js +17 -0
- package/neural-interface/public/shared/sidepanel-session-status.js +49 -0
- package/neural-interface/public/shared/styles.css +540 -371
- package/neural-interface/public/shared/ui-assistant.js +27 -5
- package/neural-interface/public/shared/ui-automation-studio.js +67 -27
- package/neural-interface/public/shared/ui-bookmarks.js +2 -1
- package/neural-interface/public/shared/ui-claude-panel.js +76 -8
- package/neural-interface/public/shared/ui-codex-panel.js +1 -0
- package/neural-interface/public/shared/ui-detail.js +6 -6
- package/neural-interface/public/shared/ui-file-links.js +1 -0
- package/neural-interface/public/shared/ui-image-gallery.js +6 -5
- package/neural-interface/public/shared/ui-invite.js +18 -2
- package/neural-interface/public/shared/ui-link.js +9 -9
- package/neural-interface/public/shared/ui-menubar.js +232 -33
- package/neural-interface/public/shared/ui-native-loop-router.js +2 -0
- package/neural-interface/public/shared/ui-opencode-panel-v2.js +1 -0
- package/neural-interface/public/shared/ui-schedules-studio.js +2099 -869
- package/neural-interface/public/shared/ui-sessions.js +15 -4
- package/neural-interface/public/shared/ui-settings.js +87 -36
- package/neural-interface/public/shared/ui-sidebar.js +4 -4
- package/neural-interface/public/shared/ui-sidepanel-session-entry.js +13 -0
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +55 -4
- package/neural-interface/public/shared/ui-sidepanel-tray.js +84 -8
- package/neural-interface/public/shared/ui-skills.js +70 -12
- package/neural-interface/public/shared/ui-sync.js +7 -0
- package/neural-interface/public/shared/ui-terminal.js +36 -20
- package/neural-interface/public/shared/ui-trash.js +8 -1
- package/neural-interface/public/shared/ui-tutorial-steps.js +7 -3
- package/neural-interface/public/shared/ui-tutorial.js +15 -5
- package/neural-interface/public/shared/ui-update.js +83 -14
- package/neural-interface/public/shared/ui-whiteboard.js +2 -1
- package/neural-interface/public/variant/2d/main.js +4 -1
- package/neural-interface/public/variant/3d/main.js +3 -0
- package/neural-interface/public/vendor/highlight/LICENSE +29 -0
- package/neural-interface/public/vendor/highlight/github-dark-dimmed-11.12.0.min.css +9 -0
- package/neural-interface/public/vendor/highlight/highlight-11.12.0.min.js +1262 -0
- package/neural-interface/public/vendor/highlight/manifest.json +5 -0
- package/neural-interface/public/vendor/html2canvas/LICENSE +22 -0
- package/neural-interface/public/vendor/html2canvas/html2canvas-1.4.1.min.js +20 -0
- package/neural-interface/public/vendor/html2canvas/manifest.json +4 -0
- package/neural-interface/public/vendor/marked/LICENSE.md +44 -0
- package/neural-interface/public/vendor/marked/manifest.json +4 -0
- package/neural-interface/public/vendor/marked/marked-14.1.4.esm.js +2502 -0
- package/neural-interface/public/vendor/mermaid/LICENSE +21 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/abnfDiagram-PAR5IXJI.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/architecture-7GRP2DOG-M7G3SWCO.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/architectureDiagram-2PNWDENW.mjs +36 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/blockDiagram-PUWGUBOA.mjs +129 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/c4Diagram-3NLNHZP5.mjs +38 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-2NMSUPJH.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-32MCUW2P.mjs +206 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-3NPNU5GP.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-3SMGFRGC.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-4CAJECRU.mjs +14 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-4OADUZ5Q.mjs +10 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5IMINLNL.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5IQCK5RY.mjs +3 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5MVFYWFU.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-AQ6EADP3.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-AZZRMDJM.mjs +15 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-CLS4B6BI.mjs +168 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-DZP67EKU.mjs +70 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-EICP7K5X.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-GDY2UAB4.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-H7VHZCWX.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-IJBDOHL6.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JFFVMJIA.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JQRUD6KW.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JZGDI5JK.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-KD6SPIPN.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-NF366YI6.mjs +231 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-NUDPKBEF.mjs +32 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-OJINMHTS.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-Q6NGPIAH.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-QJSWEUOL.mjs +132 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-R7JDMUKV.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-RCXAM2GK.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-RT4MAWR3.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-SFMINX4W.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-SLDQ463B.mjs +88 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-STOV2HOB.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-T5OCTHI4.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-TCEHX7R3.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VQC2EELJ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VRZKL3EH.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VTHDQ7BV.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VY2OSTJR.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-WUBWJARI.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-YBMQ5JGJ.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-YDMLL4PJ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-Z6V44ARX.mjs +62 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZJJ7VQJP.mjs +339 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZNLK7HRR.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZQN7UHIL.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZS6VBONO.mjs +174 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZUNWM646.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-FGAMPOII.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-v2-AI5RJEK4.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cose-bilkent-Q72C2OJ3.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cynefin-OW5HDTMX-6OCTWOGG.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cynefinDiagram-2OOWUY4M.mjs +62 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/dagre-MPVFI544.mjs +4 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-AE4NWXFW.mjs +24 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-BUJID5UG.mjs +41 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-C44CZ5NI.mjs +24 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-EYOLEXXS.mjs +30 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-P7B6PWOJ.mjs +3 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ebnfDiagram-FTPZ3PV6.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/erDiagram-H3ERR26V.mjs +99 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/eventmodeling-NTZA5JFV-WKSWQHFS.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/flowDiagram-YHGXBVSY.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ganttDiagram-RWFTB7ED.mjs +292 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/gitGraph-4MIJSDKK-JH2HKYUO.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/gitGraphDiagram-MAR6GKOP.mjs +106 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/info-A6RAGUB7-3TX3OFWW.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/infoDiagram-LV547AFH.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ishikawaDiagram-PUJQGNDS.mjs +70 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/journeyDiagram-EW5QJT4W.mjs +139 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/kanban-definition-JSQERAZB.mjs +89 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/katex-CZ4GXH2S.mjs +257 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/mindmap-definition-VBJCLQLM.mjs +96 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/packet-AYTQ26CC-MKII6P3Y.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pegDiagram-7WILOQTJ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pie-WAS4IAKB-6B4OPMJ5.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pieDiagram-EFA7CBSB.mjs +39 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/quadrantDiagram-WARVYA7P.mjs +7 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/radar-RG4KPBEZ-UPTR3XUL.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-74A4TZTK-MEFUPFMS.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-abnf-HS5TGJTU-R5YG2S5D.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-ebnf-LZEXJU2U-G5QEX2L6.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-peg-WCYAUIDC-H5I2ZRY2.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroadDiagram-5NIQVNLQ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/requirementDiagram-SXREGOKZ.mjs +84 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sankeyDiagram-VV74M2VP.mjs +40 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sequenceDiagram-52ZFIFDL.mjs +162 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sizeCapture-FGA5AGYP.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-3QAKPIVI.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-v2-HN2P4RDZ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/swimlanes-QLVP42QD.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/swimlanesDiagram-JKPDBFCN.mjs +8 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/timeline-definition-GS45XAXW.mjs +120 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/treeView-Q6P3EWNA-ZRJGANNQ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/treemap-WGGIJYW6-K64VN5AS.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/vennDiagram-QIQU2ZRJ.mjs +34 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/wardley-WFR3VGLG-D67OWCCH.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/wardleyDiagram-U6YNM66N.mjs +78 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/xychartDiagram-XJAZEDNM.mjs +7 -0
- package/neural-interface/public/vendor/mermaid/manifest.json +4 -0
- package/neural-interface/public/vendor/mermaid/mermaid-11.17.2.esm.min.mjs +14 -0
- package/neural-interface/scripts/build-vendor.mjs +84 -3
- package/neural-interface/server.js +1099 -286
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- 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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2733
|
-
|
|
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
|
-
|
|
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
|
|
290
|
-
//
|
|
291
|
-
|
|
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 =
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
354
|
+
if (_inflight.get(key) === job) _inflight.delete(key);
|
|
313
355
|
}
|
|
314
356
|
})();
|
|
315
|
-
|
|
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
|
}
|