synabun 2.0.0-beta → 2.0.5
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 +65 -0
- package/NOTICE +5 -0
- package/README.md +1 -1
- package/THIRD-PARTY-LICENSES.md +51 -0
- package/benchmarks/fixtures/browser/README.md +1 -1
- package/benchmarks/fixtures/desktop/README.md +1 -1
- package/docs/whatsapp.md +2 -2
- package/hooks/claude-code/pre-compact.mjs +11 -0
- package/hooks/claude-code/prompt-submit.mjs +60 -184
- package/hooks/claude-code/session-start.mjs +73 -16
- package/hooks/claude-code/shared.mjs +20 -0
- package/hooks/claude-code/stop.mjs +5 -5
- package/launcher.mjs +40 -0
- package/lib/claude-hooks.js +61 -8
- package/lib/client-config-repair.js +4 -3
- package/lib/data-home-migration.js +4 -0
- package/lib/desktop-pwa.js +206 -0
- package/lib/external-tools.js +221 -0
- package/lib/first-launch-protection.js +5 -0
- package/lib/packaged-runtime.js +110 -0
- package/lib/start-launcher.js +1396 -0
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -3
- package/neural-interface/i18n/en.json +1540 -154
- package/neural-interface/i18n/pt-BR.json +1538 -153
- package/neural-interface/lib/assistant-api.js +62 -6
- package/neural-interface/lib/assistant-brains/claude.js +4 -0
- package/neural-interface/lib/assistant-catalog.js +179 -27
- package/neural-interface/lib/assistant-config.js +31 -2
- package/neural-interface/lib/assistant-dispatch.js +62 -13
- package/neural-interface/lib/assistant-envelope.js +9 -6
- package/neural-interface/lib/assistant-hidden-models.js +393 -66
- package/neural-interface/lib/assistant-memory.js +24 -10
- package/neural-interface/lib/assistant-persona.js +6 -2
- package/neural-interface/lib/assistant-pricing.js +21 -6
- package/neural-interface/lib/assistant-runtime.js +181 -12
- package/neural-interface/lib/assistant-task-prompt.js +13 -3
- package/neural-interface/lib/assistant-test-density.js +142 -0
- package/neural-interface/lib/assistant-user-context-budgets.js +123 -0
- package/neural-interface/lib/assistant-user-context.js +908 -0
- package/neural-interface/lib/assistant-user-knowledge.js +328 -0
- package/neural-interface/lib/assistant-user-learn.js +404 -0
- package/neural-interface/lib/assistant-user-threads.js +226 -0
- package/neural-interface/lib/assistant-user-writer.js +181 -0
- package/neural-interface/lib/augmented-path.js +23 -3
- package/neural-interface/lib/browser-background-tab.js +134 -0
- package/neural-interface/lib/browser-language.js +83 -0
- package/neural-interface/lib/browser-screenshot.js +678 -61
- package/neural-interface/lib/claude-agent-bridge.js +87 -153
- package/neural-interface/lib/claude-executable.js +241 -0
- package/neural-interface/lib/claude-model-catalog.js +82 -93
- package/neural-interface/lib/claude-temporary.js +46 -7
- package/neural-interface/lib/cloudflared/install.js +533 -0
- package/neural-interface/lib/cloudflared/manager.js +545 -0
- package/neural-interface/lib/cloudflared/pins.js +115 -0
- package/neural-interface/lib/cloudflared/pins.json +63 -0
- package/neural-interface/lib/cloudflared/tunnel.js +265 -0
- package/neural-interface/lib/editor-completion-endpoint.js +264 -0
- package/neural-interface/lib/editor-completion-opencode.js +436 -0
- package/neural-interface/lib/editor-completion.js +714 -0
- package/neural-interface/lib/editor-config.js +149 -0
- package/neural-interface/lib/editor-files-api.js +268 -0
- package/neural-interface/lib/editor-git-api.js +134 -0
- package/neural-interface/lib/file-explorer-api.js +1039 -0
- package/neural-interface/lib/file-explorer-system.js +355 -0
- package/neural-interface/lib/file-explorer-trash.ps1 +85 -0
- package/neural-interface/lib/file-explorer-watch.js +239 -0
- package/neural-interface/lib/git-diff-api.js +103 -0
- package/neural-interface/lib/git-read.js +202 -0
- package/neural-interface/lib/greeting/api.js +89 -0
- package/neural-interface/lib/greeting/budgets.js +90 -0
- package/neural-interface/lib/greeting/engine.js +461 -0
- package/neural-interface/lib/greeting/facts.js +320 -0
- package/neural-interface/lib/greeting/sources.js +252 -0
- package/neural-interface/lib/greeting/writer.js +139 -0
- package/neural-interface/lib/http-guards.js +189 -13
- package/neural-interface/lib/machine-names.js +88 -0
- package/neural-interface/lib/memory-api.js +31 -7
- package/neural-interface/lib/native-binary-runtime.js +69 -337
- package/neural-interface/lib/native-loop-providers.js +40 -49
- package/neural-interface/lib/offline-page.js +54 -0
- package/neural-interface/lib/plugin-installer.js +4 -2
- package/neural-interface/lib/project-storage.js +5 -7
- package/neural-interface/lib/request-gate.js +589 -0
- package/neural-interface/lib/session-title-generator.js +5 -0
- package/neural-interface/lib/synabun-update-plan.js +19 -0
- package/neural-interface/lib/ui-blob-api.js +108 -0
- package/neural-interface/lib/ui-blob-store.js +68 -0
- package/neural-interface/package-lock.json +701 -253
- package/neural-interface/package.json +13 -2
- package/neural-interface/public/install-app.html +34 -0
- package/neural-interface/public/manifest.json +2 -0
- package/neural-interface/public/offline.html +402 -66
- package/neural-interface/public/onboarding.html +264 -147
- package/neural-interface/public/shared/agent-runtime-options.js +22 -6
- package/neural-interface/public/shared/assistant/asst-automation.js +286 -0
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +312 -62
- package/neural-interface/public/shared/assistant/asst-composer.js +11 -1
- package/neural-interface/public/shared/assistant/asst-mascot.js +26 -64
- package/neural-interface/public/shared/assistant/asst-menu.js +12 -8
- package/neural-interface/public/shared/assistant/asst-panel.js +150 -28
- package/neural-interface/public/shared/assistant/asst-rack-styles.js +0 -3
- package/neural-interface/public/shared/assistant/asst-render.js +219 -67
- package/neural-interface/public/shared/assistant/asst-route-models.js +108 -0
- package/neural-interface/public/shared/assistant/asst-route.js +23 -329
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +495 -0
- package/neural-interface/public/shared/assistant/asst-sanitize.js +8 -1
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +31 -9
- package/neural-interface/public/shared/assistant/asst-state.js +185 -1
- package/neural-interface/public/shared/assistant/asst-styles.js +162 -6
- package/neural-interface/public/shared/blob-store.js +111 -22
- package/neural-interface/public/shared/cdx/cdx-panel.js +32 -11
- package/neural-interface/public/shared/cdx/cdx-render.js +25 -89
- package/neural-interface/public/shared/cdx/cdx-styles.js +36 -0
- package/neural-interface/public/shared/cdx/cdx-tabs.js +87 -4
- package/neural-interface/public/shared/command-runner-model.js +30 -0
- package/neural-interface/public/shared/constants.js +6 -1
- package/neural-interface/public/shared/cp/cp-rehydrate.js +2 -1
- package/neural-interface/public/shared/cp/cp-styles.js +15 -0
- package/neural-interface/public/shared/editor/editor-ai-logic.js +409 -0
- package/neural-interface/public/shared/editor/editor-ai.js +619 -0
- package/neural-interface/public/shared/editor/editor-commands.js +212 -0
- package/neural-interface/public/shared/editor/editor-compare.js +103 -0
- package/neural-interface/public/shared/editor/editor-completion-list.js +172 -0
- package/neural-interface/public/shared/editor/editor-doc.js +257 -0
- package/neural-interface/public/shared/editor/editor-git-logic.js +131 -0
- package/neural-interface/public/shared/editor/editor-git.js +95 -0
- package/neural-interface/public/shared/editor/editor-languages.js +156 -0
- package/neural-interface/public/shared/editor/editor-lint.js +229 -0
- package/neural-interface/public/shared/editor/editor-loader.js +51 -0
- package/neural-interface/public/shared/editor/editor-markdown.js +90 -0
- package/neural-interface/public/shared/editor/editor-menu.js +233 -0
- package/neural-interface/public/shared/editor/editor-minimap.js +371 -0
- package/neural-interface/public/shared/editor/editor-palette-logic.js +192 -0
- package/neural-interface/public/shared/editor/editor-palette.js +125 -0
- package/neural-interface/public/shared/editor/editor-panel-logic.js +288 -0
- package/neural-interface/public/shared/editor/editor-panel.js +2099 -0
- package/neural-interface/public/shared/editor/editor-prefs.js +224 -0
- package/neural-interface/public/shared/editor/editor-safety.js +500 -0
- package/neural-interface/public/shared/editor/editor-search.js +352 -0
- package/neural-interface/public/shared/editor/editor-statusbar.js +144 -0
- package/neural-interface/public/shared/editor/editor-tabs-logic.js +331 -0
- package/neural-interface/public/shared/editor/editor-tabs.js +233 -0
- package/neural-interface/public/shared/editor/editor-theme.js +244 -0
- package/neural-interface/public/shared/editor/editor-view.js +332 -0
- package/neural-interface/public/shared/explorer/fx-api.js +216 -0
- package/neural-interface/public/shared/explorer/fx-links.js +204 -0
- package/neural-interface/public/shared/explorer/fx-logic.js +537 -0
- package/neural-interface/public/shared/explorer/fx-main.js +865 -0
- package/neural-interface/public/shared/explorer/fx-names.js +87 -0
- package/neural-interface/public/shared/explorer/fx-ops.js +370 -0
- package/neural-interface/public/shared/explorer/fx-popups.js +490 -0
- package/neural-interface/public/shared/explorer/fx-tree.js +790 -0
- package/neural-interface/public/shared/file-links.js +339 -0
- package/neural-interface/public/shared/greeting/greeting-card.js +178 -0
- package/neural-interface/public/shared/greeting/greeting-view.js +148 -0
- package/neural-interface/public/shared/html-shell.js +169 -170
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-greeting.js +47 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +99 -17
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +5 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +40 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +3 -2
- package/neural-interface/public/shared/pwa-install-page.js +62 -0
- package/neural-interface/public/shared/pwa-install.css +12 -0
- package/neural-interface/public/shared/pwa-install.js +127 -0
- package/neural-interface/public/shared/settings/settings-code-editor-pane.js +412 -0
- package/neural-interface/public/shared/settings/settings-code-editor.js +328 -0
- package/neural-interface/public/shared/settings/settings-greeting.js +133 -0
- package/neural-interface/public/shared/settings/settings-ia.js +18 -4
- package/neural-interface/public/shared/settings/settings-rules.js +160 -0
- package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
- package/neural-interface/public/shared/start-bridge.js +802 -0
- package/neural-interface/public/shared/styleguide/sg-colors.js +5 -2
- package/neural-interface/public/shared/styleguide/sg-import-export.js +4 -3
- package/neural-interface/public/shared/styleguide/sg-kit.js +4 -1
- package/neural-interface/public/shared/styleguide/sg-panel.js +9 -1
- package/neural-interface/public/shared/styleguide/sg-typography.js +1 -0
- package/neural-interface/public/shared/styles.css +2219 -1117
- package/neural-interface/public/shared/synabun-mascot.js +20 -56
- package/neural-interface/public/shared/term-ready.js +307 -0
- package/neural-interface/public/shared/tunnel-view.js +167 -0
- package/neural-interface/public/shared/ui-claude-panel.js +160 -19
- package/neural-interface/public/shared/ui-command-runner.js +251 -468
- package/neural-interface/public/shared/ui-detail.js +15 -0
- package/neural-interface/public/shared/ui-explorer.js +22 -0
- package/neural-interface/public/shared/ui-file-explorer.js +481 -2496
- package/neural-interface/public/shared/ui-file-links.js +103 -0
- package/neural-interface/public/shared/ui-help.js +26 -0
- package/neural-interface/public/shared/ui-invite.js +126 -116
- package/neural-interface/public/shared/ui-judgments.js +1 -1
- package/neural-interface/public/shared/ui-keybinds.js +4 -0
- package/neural-interface/public/shared/ui-loading.js +367 -12
- package/neural-interface/public/shared/ui-menubar.js +21 -55
- package/neural-interface/public/shared/ui-native-loop-router-state.js +11 -0
- package/neural-interface/public/shared/ui-native-loop-router.js +15 -2
- package/neural-interface/public/shared/ui-navbar.js +45 -21
- package/neural-interface/public/shared/ui-settings.js +491 -547
- package/neural-interface/public/shared/ui-sidepanel-runtime.js +2 -1
- package/neural-interface/public/shared/ui-sync.js +16 -0
- package/neural-interface/public/shared/ui-terminal.js +153 -149
- package/neural-interface/public/shared/ui-tunnel.js +130 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +6 -2
- package/neural-interface/public/shared/ui-whiteboard.js +12 -0
- package/neural-interface/public/shared/ui-worker-end.js +245 -0
- package/neural-interface/public/shared/ui-workspaces.js +41 -27
- package/neural-interface/public/shared/worker-end.js +251 -0
- package/neural-interface/public/sw.js +3 -1
- package/neural-interface/public/variant/2d/main.js +2 -1
- package/neural-interface/public/variant/3d/main.js +2 -1
- package/neural-interface/public/vendor/codemirror/chunks/apl-CSDKT72Z.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asciiarmor-JYYPU5BM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asn1-NRWCZDA6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asterisk-5OU6D4ZQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/brainfuck-ID4N4SYC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-4RZWGCS2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-ACTJM3JR.js +11 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-S6BMAXLK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-TO2RDOHQ.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-US2GMZZQ.js +13 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-WYYO5ZAJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-XALZTDJC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-YRNUTRDV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-Z3QOKH62.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/clike-OWQBSOGL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/clojure-KV75UQWC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cmake-GTEP5Y3C.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cobol-WCZ7QEGJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/coffeescript-TGLBOT6B.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/commonlisp-WVVI432V.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/crystal-S6R6HLKQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/css-PDA2RINP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cypher-HL5QPL4C.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/d-HHJH2HAJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/diff-MN3A35BY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-25SQLBVS.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-27ZGGJJS.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-4E52OJIF.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-6FT73XDC.js +2 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-BE6YGRMW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CJSGKW2O.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CRPH2WLI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CTG7JEQN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-I3FEVELX.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-IM5BBSWZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-KERUKTIF.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-LXTMWZAM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MABASSET.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MJVBUZLL.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MNB3WZVU.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-QYWXOE3U.js +7 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-TEBOFWLZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-UNXFROM3.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-XL2SLQO6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-XO42Y3AP.js +6 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-Y2WWDWJK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-YFEZKWMU.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-ZY7SWQO6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dockerfile-3Y4R4NCH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dtd-5343GJ4P.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dylan-FI2CXSW2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ebnf-GNQ77YWA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ecl-MRPUAE2L.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/eiffel-DZROES27.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/elm-4KICRFKQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/erlang-3IHYC263.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/factor-C6D3E5UK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/fcl-VYTWUJMI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/forth-ZBO2EH5M.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/fortran-5NDCRWIK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/gas-SKRMOXFP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/gherkin-L45YXZLW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/groovy-GLPTLN44.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/haskell-E7NF7P4Y.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/haxe-K2NVDOB6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/http-Q6DRQIJB.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/idl-MZYNJVLN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/javascript-6YRYK2HR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/julia-HXEL2T5G.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/livescript-MV7CI2JZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/lua-E36NHOOD.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mathematica-3XOZZCMC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mbox-ODYGFKS7.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mirc-WB43AWF5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mllike-ZT6ET7ZH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/modelica-LLMZPFGA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mscgen-2OPNNYFH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mumps-6P3HWJH6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/nginx-TCGKQTRW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/nsis-ATIGIXTJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ntriples-5PLBI5OV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/octave-QF2S3NL6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/oz-JFIVFFH2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pascal-JG6SW76V.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/perl-4B3IWVVO.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pig-ABEBH4MI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/powershell-H32XE7TL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/properties-NN644MMA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/protobuf-YKWQK2J3.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pug-RIYXEKFW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/puppet-JZQ5WPEV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/python-EBCGGHAC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/q-5UMH2YQY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/r-7XMGG453.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/rpm-IEV7TSLR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ruby-T4SPFPNM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sas-GEDX67ET.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/scheme-FTLOWKPC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/shell-HUFRKFSJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sieve-P5QUDVYO.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/smalltalk-2SORJNQI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/solr-M22TGAR5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sparql-4M3KL752.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/spreadsheet-3K23LA4N.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sql-VHRFMUBI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/stex-QZDAQUY2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/stylus-7ZDKAKBD.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/swift-WCLCLGMR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tcl-JLMU6WOK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/textile-4CHDKZ4T.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tiddlywiki-DKMCZZQ4.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tiki-F4JIG36Q.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/toml-BHMHTIM2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/troff-YQ2KV5NT.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ttcn-ZHRCVFJY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ttcn-cfg-KJOVTY4X.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/turtle-535P5N6I.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vb-76UMQHOT.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vbscript-CGNDTK6Z.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/velocity-VSJDO5T5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/verilog-4T6ETXIP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vhdl-T7FNAXUN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/webidl-P6MJSJ6J.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/xquery-UOVILRGN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/yacas-CV2APROL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/z80-3KFNVM4M.js +1 -0
- package/neural-interface/public/vendor/codemirror/codemirror-6.43.14-75f05e67.min.js +2 -0
- package/neural-interface/public/vendor/codemirror/manifest.json +60 -0
- package/neural-interface/scripts/build-vendor.mjs +78 -2
- package/neural-interface/scripts/rebuild-pty.js +15 -12
- package/neural-interface/scripts/vendor-codemirror-entry.js +121 -0
- package/neural-interface/server.js +1092 -1212
- package/neural-interface/templates/rulesets/README.md +1 -1
- package/package.json +13 -7
- package/postinstall.js +11 -81
- package/setup.js +111 -6
- package/benchmarks/browser-live-results.json +0 -79
- package/benchmarks/browser-v2-results.json +0 -258
- package/docs/browser-v2.md +0 -109
- package/docs/hook-integrity.md +0 -89
- package/docs/judgments.md +0 -182
- package/docs/style-guide.md +0 -246
- package/neural-interface/lib/codex-capabilities.d.ts +0 -41
- package/neural-interface/public/category-logos/claude.svg +0 -1
- package/neural-interface/public/category-logos/gemini-color.svg +0 -1
- package/neural-interface/public/category-logos/openai.svg +0 -1
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude.svg +0 -1
- package/neural-interface/public/gallery.svg +0 -6
- package/neural-interface/public/leonardoai.svg +0 -1
- package/neural-interface/public/mcp.svg +0 -4
- package/neural-interface/public/opencode-logo-dark-square.svg +0 -1
- package/neural-interface/public/pin-svgrepo-com.svg +0 -25
- package/neural-interface/public/sessionicon.svg +0 -8
- package/neural-interface/public/toolbar-album.svg +0 -8
- package/neural-interface/public/toolbar-question.svg +0 -6
- package/neural-interface/public/toolbar-sessions.svg +0 -6
- package/neural-interface/scripts/codex-protocol-fixture.mjs +0 -83
- package/scripts/audit-claude-hook-usage.mjs +0 -12
- package/scripts/audit-memory-duplicates.mjs +0 -44
- package/scripts/measure-claude-hook-overhead.mjs +0 -77
- package/scripts/recover-user-config.mjs +0 -254
- package/skills/synabun/synabunicon.png +0 -0
|
@@ -0,0 +1,908 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun — Assistant user context (the engine)
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// What makes the Assistant behave like someone who knows the user, whatever
|
|
6
|
+
// model is routed: knowledge of the user from past sessions and small nudges
|
|
7
|
+
// from recent interactions, all of it in SynaBun memory. The harness does the
|
|
8
|
+
// learning and the injection itself (no provider hook, no brain tokens spent
|
|
9
|
+
// on capture), and every turn says what it did in one `synabun.automation`
|
|
10
|
+
// event the side panel draws.
|
|
11
|
+
//
|
|
12
|
+
// store user-knowledge (one memory = one statement) and assistant-threads
|
|
13
|
+
// (one memory per conversation); legacy communication-style
|
|
14
|
+
// memories are read in place as seeds. Operational state only: the
|
|
15
|
+
// frozen digest and the delivery ledgers on the session record,
|
|
16
|
+
// nudged-item keys and the working threads in
|
|
17
|
+
// assistant-user-context.json.
|
|
18
|
+
// digest ≤ 300 tokens, deterministic, appended to the persona (so every
|
|
19
|
+
// brain gets it in its system-level slot), frozen byte for byte
|
|
20
|
+
// per provider conversation.
|
|
21
|
+
// per turn ≤ 120 tokens on the first prompt, ≤ 80 later, nothing when there
|
|
22
|
+
// is nothing to say: the last thread, an environment change, an
|
|
23
|
+
// open item, one knowledge cue, a delta. Each said once.
|
|
24
|
+
// learning assistant-user-learn.js, after the turn, serialized per session.
|
|
25
|
+
//
|
|
26
|
+
// Everything heavy is injected (the store, Jev, the writer, the usage ledger),
|
|
27
|
+
// so the module is unit-testable and never opens the memory database itself.
|
|
28
|
+
// Nothing here throws into a turn: a failure is logged and the turn goes on
|
|
29
|
+
// without the layer.
|
|
30
|
+
|
|
31
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
32
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
33
|
+
import { dirname, resolve } from 'node:path';
|
|
34
|
+
import { CATEGORIES, CATEGORY_DESCRIPTIONS, DIGEST, LEARN, NUDGES, THREAD, TURN_BLOCK, estimateTokens, normalizeUserContext } from './assistant-user-context-budgets.js';
|
|
35
|
+
import { clipText, nearest, parseKnowledge, renderDigest, usableKnowledge } from './assistant-user-knowledge.js';
|
|
36
|
+
import { dropRun, isClosingTurn, lastThreadLine, newThread, noteAsk, noteRun, openItems, overlap, parseThread, threadContent, threadIsRecent } from './assistant-user-threads.js';
|
|
37
|
+
import { createUserLearner, prefilter, typedEvidence } from './assistant-user-learn.js';
|
|
38
|
+
|
|
39
|
+
export const AUTOMATION_EVENT = 'synabun.automation';
|
|
40
|
+
export const TURN_BLOCK_HEADER = 'SynaBun notes (background):';
|
|
41
|
+
const STATE_VERSION = 1;
|
|
42
|
+
|
|
43
|
+
const iso = (ms) => new Date(ms).toISOString();
|
|
44
|
+
const shortHash = (value) => createHash('sha1').update(String(value ?? '')).digest('hex').slice(0, 10);
|
|
45
|
+
const count = (value) => (Number.isFinite(Number(value)) && Number(value) > 0 ? Math.round(Number(value)) : 0);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Why Jev cannot answer the learning loop right now, or null when it can (Settings → Automation
|
|
49
|
+
* behavior says it next to "Knowledge of you"). The same order the client refuses in: the
|
|
50
|
+
* environment's kill switch, the master switch, the key, the surface, a wait the API asked for.
|
|
51
|
+
* @param {object} deps
|
|
52
|
+
* @param {object} deps.typesafe mcp-server services/typesafe.js ({ typesafeApiKey, typesafeRetryAfterMs })
|
|
53
|
+
* @param {object} deps.knobs mcp-server services/typesafe-config.js ({ typesafeMasterEnabled, surfaceConfig })
|
|
54
|
+
* @returns {'kill-switch'|'master-off'|'no-key'|'surface-off'|'waiting'|'unavailable'|null}
|
|
55
|
+
*/
|
|
56
|
+
export function learningJudgeReason({ env = process.env, typesafe = null, knobs = null, surface = 'user-learning' } = {}) {
|
|
57
|
+
try {
|
|
58
|
+
if (env?.SYNABUN_TYPESAFE === 'off') return 'kill-switch';
|
|
59
|
+
if (!typesafe || !knobs) return 'unavailable';
|
|
60
|
+
if (!knobs.typesafeMasterEnabled()) return 'master-off';
|
|
61
|
+
if (typesafe.typesafeApiKey() === null) return 'no-key';
|
|
62
|
+
if (knobs.surfaceConfig(surface).enabled === false) return 'surface-off';
|
|
63
|
+
if (typesafe.typesafeRetryAfterMs() > 0) return 'waiting';
|
|
64
|
+
return null;
|
|
65
|
+
} catch { return 'unavailable'; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The branch checked out in `cwd` (or a parent), read from .git/HEAD: no process is started. null when unknown. */
|
|
69
|
+
export function readGitBranch(cwd) {
|
|
70
|
+
try {
|
|
71
|
+
let dir = resolve(String(cwd || ''));
|
|
72
|
+
for (let depth = 0; depth < 16; depth += 1) {
|
|
73
|
+
const dotGit = resolve(dir, '.git');
|
|
74
|
+
if (existsSync(dotGit)) {
|
|
75
|
+
let headPath = resolve(dotGit, 'HEAD');
|
|
76
|
+
if (statSync(dotGit).isFile()) {
|
|
77
|
+
// A worktree or a submodule: ".git" is a file that names the real directory.
|
|
78
|
+
const match = /^gitdir:\s*(.+)$/m.exec(readFileSync(dotGit, 'utf8'));
|
|
79
|
+
if (!match) return null;
|
|
80
|
+
headPath = resolve(dir, match[1].trim(), 'HEAD');
|
|
81
|
+
}
|
|
82
|
+
const head = readFileSync(headPath, 'utf8').trim();
|
|
83
|
+
const ref = /^ref:\s*refs\/heads\/(.+)$/.exec(head);
|
|
84
|
+
if (ref) return ref[1];
|
|
85
|
+
return head ? `detached@${head.slice(0, 7)}` : null;
|
|
86
|
+
}
|
|
87
|
+
const parent = dirname(dir);
|
|
88
|
+
if (parent === dir) break;
|
|
89
|
+
dir = parent;
|
|
90
|
+
}
|
|
91
|
+
} catch { /* unreadable: unknown */ }
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── the store (SynaBun memory) ──────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
function resultText(result) {
|
|
98
|
+
if (Array.isArray(result?.content)) return result.content.map((block) => block?.text || '').join('\n');
|
|
99
|
+
return String(result?.text ?? result ?? '');
|
|
100
|
+
}
|
|
101
|
+
const UUID_IN_BRACKETS = /\[([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\]/i;
|
|
102
|
+
|
|
103
|
+
function toVector(blob) {
|
|
104
|
+
if (!blob) return null;
|
|
105
|
+
const bytes = blob instanceof Uint8Array ? blob : new Uint8Array(blob);
|
|
106
|
+
if (!bytes.byteLength || bytes.byteLength % 4) return null;
|
|
107
|
+
return new Float32Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The engine's view of SynaBun memory. Reads go to SQLite directly (indexed by category, a few
|
|
112
|
+
* hundred rows at most); writes go through the `remember` / `reflect` tool handlers, so their
|
|
113
|
+
* duplicate and secret gates stay in place.
|
|
114
|
+
* @param {object} deps
|
|
115
|
+
* @param {() => object} deps.getDb node:sqlite handle of the memory database
|
|
116
|
+
* @param {(args:object) => Promise<object>} deps.remember MCP handleRemember
|
|
117
|
+
* @param {(args:object) => Promise<object>} deps.reflect MCP handleReflect
|
|
118
|
+
* @param {(text:string) => Promise<ArrayLike<number>>} [deps.embed] the local embedding model
|
|
119
|
+
* @param {(name:string, description:string, parent:string, parentDescription:string) => void} [deps.ensureCategory]
|
|
120
|
+
* @param {() => string[]} [deps.knowledgeCategories] user-knowledge and the seed categories
|
|
121
|
+
* @param {(sessionId:string|null, project:string|null, fn:Function) => any} [deps.withSession]
|
|
122
|
+
* runs a write so its write-time judgments are logged against the assistant session
|
|
123
|
+
*/
|
|
124
|
+
export function createUserContextStore({
|
|
125
|
+
getDb, remember, reflect, embed = null, ensureCategory = null,
|
|
126
|
+
knowledgeCategories = () => [CATEGORIES.knowledge, CATEGORIES.knowledgeParent],
|
|
127
|
+
withSession = (_sessionId, _project, fn) => fn(),
|
|
128
|
+
log = () => {},
|
|
129
|
+
} = {}) {
|
|
130
|
+
let ensured = false;
|
|
131
|
+
let cache = null; // { at, rows }
|
|
132
|
+
const CACHE_MS = 5000;
|
|
133
|
+
|
|
134
|
+
const categories = () => {
|
|
135
|
+
let list = [];
|
|
136
|
+
try { list = knowledgeCategories() || []; } catch { list = []; }
|
|
137
|
+
const out = [...new Set([CATEGORIES.knowledge, ...list.map(String).filter(Boolean)])];
|
|
138
|
+
return out.filter((name) => name !== CATEGORIES.threads);
|
|
139
|
+
};
|
|
140
|
+
const marks = (n) => Array.from({ length: n }, () => '?').join(',');
|
|
141
|
+
|
|
142
|
+
/** The two categories, created on first use (lazily: nothing is created until something is stored). */
|
|
143
|
+
function ensureCategories() {
|
|
144
|
+
if (ensured) return true;
|
|
145
|
+
if (typeof ensureCategory !== 'function') { ensured = true; return true; }
|
|
146
|
+
try {
|
|
147
|
+
ensureCategory(CATEGORIES.knowledge, CATEGORY_DESCRIPTIONS.knowledge, CATEGORIES.knowledgeParent, CATEGORY_DESCRIPTIONS.knowledgeParent);
|
|
148
|
+
ensureCategory(CATEGORIES.threads, CATEGORY_DESCRIPTIONS.threads, CATEGORIES.threadsParent, CATEGORY_DESCRIPTIONS.threadsParent);
|
|
149
|
+
ensured = true;
|
|
150
|
+
} catch (error) { log('user-context:category-error', error?.message || String(error)); }
|
|
151
|
+
return ensured;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Every live, not superseded memory of the knowledge categories, parsed, newest first. Cached for 5 s. */
|
|
155
|
+
function listKnowledge({ fresh = false } = {}) {
|
|
156
|
+
if (!fresh && cache && Date.now() - cache.at < CACHE_MS) return cache.rows;
|
|
157
|
+
const names = categories();
|
|
158
|
+
let rows = [];
|
|
159
|
+
try {
|
|
160
|
+
const db = getDb();
|
|
161
|
+
try {
|
|
162
|
+
rows = db.prepare(`SELECT m.id, m.content, m.category, m.subcategory, m.project, m.importance, m.created_at, m.updated_at, m.related_memory_ids,
|
|
163
|
+
md.kind, md.source_ref, (SELECT MAX(r.revision) FROM memory_revisions r WHERE r.memory_id = m.id) AS revision
|
|
164
|
+
FROM memories m LEFT JOIN memory_metadata md ON md.memory_id = m.id
|
|
165
|
+
WHERE m.trashed_at IS NULL AND m.category IN (${marks(names.length)})
|
|
166
|
+
AND NOT EXISTS (SELECT 1 FROM memory_relations s WHERE s.kind = 'supersedes' AND s.active = 1 AND s.to_id = m.id)
|
|
167
|
+
ORDER BY m.updated_at DESC LIMIT ?`).all(...names, DIGEST.readLimit);
|
|
168
|
+
} catch {
|
|
169
|
+
// A store without the v2 tables: the memories alone.
|
|
170
|
+
rows = db.prepare(`SELECT id, content, category, subcategory, project, importance, created_at, updated_at, related_memory_ids
|
|
171
|
+
FROM memories WHERE trashed_at IS NULL AND category IN (${marks(names.length)}) ORDER BY updated_at DESC LIMIT ?`).all(...names, DIGEST.readLimit);
|
|
172
|
+
}
|
|
173
|
+
} catch (error) { log('user-context:read-error', error?.message || String(error)); rows = []; }
|
|
174
|
+
const parsed = rows.map((row) => parseKnowledge(row)).filter((item) => item.id && item.statement);
|
|
175
|
+
cache = { at: Date.now(), rows: parsed };
|
|
176
|
+
return parsed;
|
|
177
|
+
}
|
|
178
|
+
function invalidate() { cache = null; }
|
|
179
|
+
|
|
180
|
+
/** id → the id of the memory that superseded it, for the ids given (active relations only). */
|
|
181
|
+
function supersededBy(ids = []) {
|
|
182
|
+
const list = [...new Set(ids.map(String).filter(Boolean))].slice(0, 200);
|
|
183
|
+
const out = new Map();
|
|
184
|
+
if (!list.length) return out;
|
|
185
|
+
try {
|
|
186
|
+
for (const row of getDb().prepare(`SELECT from_id, to_id FROM memory_relations WHERE kind = 'supersedes' AND active = 1 AND to_id IN (${marks(list.length)})`).all(...list)) out.set(String(row.to_id), String(row.from_id));
|
|
187
|
+
} catch { /* no relations table: nothing was superseded */ }
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The known statements nearest to `text` (a local embedding search), usable for `project`, best first. */
|
|
192
|
+
async function known(text, { project = 'global', limit = LEARN.knownStatements } = {}) {
|
|
193
|
+
if (typeof embed !== 'function') return [];
|
|
194
|
+
const all = usableKnowledge(listKnowledge(), { project });
|
|
195
|
+
if (!all.length) return [];
|
|
196
|
+
const names = categories();
|
|
197
|
+
let vectors = [];
|
|
198
|
+
try {
|
|
199
|
+
vectors = getDb().prepare(`SELECT id, vector FROM memories WHERE trashed_at IS NULL AND category IN (${marks(names.length)}) ORDER BY updated_at DESC LIMIT ?`).all(...names, DIGEST.readLimit);
|
|
200
|
+
} catch (error) { log('user-context:read-error', error?.message || String(error)); return []; }
|
|
201
|
+
let query = null;
|
|
202
|
+
try { query = await embed(String(text ?? '').slice(0, LEARN.messageClip)); } catch (error) { log('user-context:embed-error', error?.message || String(error)); return []; }
|
|
203
|
+
const wanted = new Map(all.map((item) => [item.id, item]));
|
|
204
|
+
const rows = vectors.filter((row) => wanted.has(String(row.id))).map((row) => ({ id: String(row.id), vector: toVector(row.vector) })).filter((row) => row.vector);
|
|
205
|
+
return nearest(Array.from(query || []), rows, limit).map((hit) => ({ ...wanted.get(hit.id), similarity: hit.similarity }));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** A new statement. → { id } | { duplicateOf } | { error } */
|
|
209
|
+
async function create({ content, project = 'global', facet = 'expectations', kind = 'preference', sessionId = null, links = [], hash = null } = {}) {
|
|
210
|
+
if (!ensureCategories()) return { error: 'no-category' };
|
|
211
|
+
try {
|
|
212
|
+
const stored = await withSession(sessionId, project, () => remember({
|
|
213
|
+
content, category: CATEGORIES.knowledge, project: project || 'global', subcategory: facet, kind,
|
|
214
|
+
tags: ['user-knowledge', facet, 'assistant-learned'], importance: 6, source: 'auto-saved',
|
|
215
|
+
source_ref: sessionId ? `assistant-user:${sessionId}` : 'assistant-user',
|
|
216
|
+
...(hash ? { idempotency_key: `user-knowledge:${hash}` } : {}),
|
|
217
|
+
}));
|
|
218
|
+
const text = resultText(stored);
|
|
219
|
+
const id = UUID_IN_BRACKETS.exec(text)?.[1] || null;
|
|
220
|
+
invalidate();
|
|
221
|
+
if (stored?.isError || !id) return { error: clipText(text || 'remember failed', 160) };
|
|
222
|
+
if (/^Not stored/i.test(text)) return { duplicateOf: id };
|
|
223
|
+
if (links.length) {
|
|
224
|
+
try { await withSession(sessionId, project, () => reflect({ memory_id: id, related_memory_ids: links.map(String) })); }
|
|
225
|
+
catch (error) { log('user-context:link-error', error?.message || String(error)); }
|
|
226
|
+
invalidate();
|
|
227
|
+
}
|
|
228
|
+
return { id };
|
|
229
|
+
} catch (error) { return { error: clipText(error?.message || String(error), 160) }; }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Rewrite a statement in place. → { ok } | { ok:false, error } */
|
|
233
|
+
async function update(id, { content, expectedRevision = null, sessionId = null, project = null } = {}) {
|
|
234
|
+
try {
|
|
235
|
+
const args = { memory_id: String(id), content };
|
|
236
|
+
if (Number(expectedRevision) > 0) args.expected_revision = Number(expectedRevision);
|
|
237
|
+
const answer = await withSession(sessionId, project, () => reflect(args));
|
|
238
|
+
const text = resultText(answer);
|
|
239
|
+
invalidate();
|
|
240
|
+
return answer?.isError || !/^Updated \[/.test(text) ? { ok: false, error: clipText(text || 'reflect failed', 160) } : { ok: true };
|
|
241
|
+
} catch (error) { return { ok: false, error: clipText(error?.message || String(error), 160) }; }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** `newId` supersedes `oldId` (a reversible relation; the old memory is not edited). */
|
|
245
|
+
async function supersede(newId, oldId, { sessionId = null } = {}) {
|
|
246
|
+
try {
|
|
247
|
+
const answer = await withSession(sessionId, null, () => reflect({ memory_id: String(newId), supersedes: String(oldId) }));
|
|
248
|
+
const text = resultText(answer);
|
|
249
|
+
invalidate();
|
|
250
|
+
return answer?.isError || !/^Updated \[/.test(text) ? { ok: false, error: clipText(text || 'reflect failed', 160) } : { ok: true };
|
|
251
|
+
} catch (error) { return { ok: false, error: clipText(error?.message || String(error), 160) }; }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function threadMemoryFor(sessionId) {
|
|
255
|
+
try {
|
|
256
|
+
const db = getDb();
|
|
257
|
+
const live = db.prepare('SELECT 1 FROM memories WHERE id = ? AND trashed_at IS NULL AND category = ?');
|
|
258
|
+
for (const row of db.prepare('SELECT memory_id FROM memory_metadata WHERE source_ref = ?').all(`assistant-thread:${sessionId}`)) {
|
|
259
|
+
if (live.get(row.memory_id, CATEGORIES.threads)) return String(row.memory_id);
|
|
260
|
+
}
|
|
261
|
+
} catch { /* no metadata table */ }
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Create the conversation's memory, or rewrite it in place. → { id } | { error } */
|
|
266
|
+
async function writeThread({ sessionId, memoryId = null, content, project = 'global' } = {}) {
|
|
267
|
+
if (!ensureCategories()) return { error: 'no-category' };
|
|
268
|
+
try {
|
|
269
|
+
let id = memoryId || threadMemoryFor(sessionId);
|
|
270
|
+
if (id) {
|
|
271
|
+
const answer = await withSession(sessionId, project, () => reflect({ memory_id: id, content }));
|
|
272
|
+
const text = resultText(answer);
|
|
273
|
+
if (!answer?.isError && /^Updated \[/.test(text)) return { id };
|
|
274
|
+
// Gone (trashed or deleted by the owner): a fresh one below.
|
|
275
|
+
if (!/not found/i.test(text)) return { error: clipText(text || 'reflect failed', 160) };
|
|
276
|
+
id = null;
|
|
277
|
+
}
|
|
278
|
+
const stored = await withSession(sessionId, project, () => remember({
|
|
279
|
+
content, category: CATEGORIES.threads, project: project || 'global', kind: 'session', subcategory: 'thread',
|
|
280
|
+
tags: ['assistant-thread'], importance: 5, source: 'auto-saved', source_ref: `assistant-thread:${sessionId}`,
|
|
281
|
+
}));
|
|
282
|
+
const text = resultText(stored);
|
|
283
|
+
const created = UUID_IN_BRACKETS.exec(text)?.[1] || null;
|
|
284
|
+
if (stored?.isError || !created || /^Not stored/i.test(text)) return { error: clipText(text || 'remember failed', 160) };
|
|
285
|
+
return { id: created };
|
|
286
|
+
} catch (error) { return { error: clipText(error?.message || String(error), 160) }; }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** The newest thread memories: [{ id, content, project, updated_at, revision }]. */
|
|
290
|
+
function lastThreads({ limit = 5 } = {}) {
|
|
291
|
+
try {
|
|
292
|
+
const db = getDb();
|
|
293
|
+
try {
|
|
294
|
+
return db.prepare(`SELECT m.id, m.content, m.project, m.updated_at, (SELECT MAX(r.revision) FROM memory_revisions r WHERE r.memory_id = m.id) AS revision
|
|
295
|
+
FROM memories m WHERE m.trashed_at IS NULL AND m.category = ? ORDER BY m.updated_at DESC LIMIT ?`).all(CATEGORIES.threads, limit);
|
|
296
|
+
} catch {
|
|
297
|
+
return db.prepare('SELECT id, content, project, updated_at FROM memories WHERE trashed_at IS NULL AND category = ? ORDER BY updated_at DESC LIMIT ?').all(CATEGORIES.threads, limit);
|
|
298
|
+
}
|
|
299
|
+
} catch { return []; }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return { ensureCategories, knowledgeCategories: categories, listKnowledge, invalidate, supersededBy, known, create, update, supersede, writeThread, lastThreads };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── the engine ──────────────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
function freshDelivered() {
|
|
308
|
+
return { opened: false, lastThread: false, cues: [], deltas: [], nudges: [] };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** The session record's user-context state, made whole (older records, hand edits). */
|
|
312
|
+
export function userContextState(record) {
|
|
313
|
+
const uc = record.userContext && typeof record.userContext === 'object' && !Array.isArray(record.userContext) ? record.userContext : (record.userContext = {});
|
|
314
|
+
if (!uc.digest || typeof uc.digest !== 'object' || typeof uc.digest.text !== 'string') uc.digest = null;
|
|
315
|
+
if (!uc.delivered || typeof uc.delivered !== 'object') uc.delivered = freshDelivered();
|
|
316
|
+
for (const key of ['cues', 'deltas', 'nudges']) if (!Array.isArray(uc.delivered[key])) uc.delivered[key] = [];
|
|
317
|
+
if (!uc.env || typeof uc.env !== 'object') uc.env = { branch: null, bootId: null, lastTurnAt: null };
|
|
318
|
+
if (!uc.learn || typeof uc.learn !== 'object') uc.learn = { counts: { gate: 0, writer: 0, writes: 0 }, hashes: [] };
|
|
319
|
+
if (!uc.learn.counts || typeof uc.learn.counts !== 'object') uc.learn.counts = { gate: 0, writer: 0, writes: 0 };
|
|
320
|
+
if (!Array.isArray(uc.learn.hashes)) uc.learn.hashes = [];
|
|
321
|
+
if (!Array.isArray(uc.learnedHere)) uc.learnedHere = [];
|
|
322
|
+
if (!uc.totals || typeof uc.totals !== 'object') uc.totals = { turns: 0, injectedTokens: 0, learned: 0, jevRequests: 0, jevInputTokens: 0, writerCalls: 0, costUsd: 0 };
|
|
323
|
+
return uc;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* @param {object} deps
|
|
328
|
+
* @param {() => object} deps.settings assistant-config.json `userContext` (read live)
|
|
329
|
+
* @param {object} deps.store createUserContextStore()
|
|
330
|
+
* @param {{ ask: Function, available?: Function, reason?: Function } | null} [deps.judge] Jev, surface `user-learning`
|
|
331
|
+
* (`reason()` names why it cannot answer: learningJudgeReason)
|
|
332
|
+
* @param {{ write: Function, available?: Function } | null} [deps.writer]
|
|
333
|
+
* @param {object | null} [deps.usage] the usage ledger (the writer's tokens are settled there)
|
|
334
|
+
* @param {(rows:object[], identity:{ session:string, generation:number }) => void} [deps.markInjected]
|
|
335
|
+
* the injection ledger Related Memories uses (memory-api.js filterInjected)
|
|
336
|
+
* @param {(cwd:string) => string|null} [deps.readBranch]
|
|
337
|
+
* @param {string} [deps.bootId] this server process: a different one means SynaBun restarted
|
|
338
|
+
* @param {string} [deps.statePath] assistant-user-context.json
|
|
339
|
+
*/
|
|
340
|
+
export function createAssistantUserContext({
|
|
341
|
+
settings = () => ({}), store = null, judge = null, writer = null, usage = null, markInjected = null,
|
|
342
|
+
readBranch = readGitBranch, bootId = randomUUID(), statePath = null, rates = () => null,
|
|
343
|
+
now = Date.now, log = () => {}, timers = { setTimeout, clearTimeout },
|
|
344
|
+
} = {}) {
|
|
345
|
+
const learner = createUserLearner({ judge, writer, store, rates, now, log });
|
|
346
|
+
const chains = new Map(); // sessionId → the learning under way (one at a time per session)
|
|
347
|
+
const threadTimers = new Map(); // sessionId → the debounced thread write
|
|
348
|
+
const cardGetters = new Map(); // sessionId → () => the cards left unanswered right now
|
|
349
|
+
const installs = new Set(); // sessionIds whose brain was just built with the digest (the next trace shows its text)
|
|
350
|
+
const lastThreads = new Map(); // sessionId → the open items of the thread offered at session open
|
|
351
|
+
// How the statement writer's last one-shot went, since this server started (Settings says it): null = not tried yet.
|
|
352
|
+
const health = { writer: null };
|
|
353
|
+
let state = loadState();
|
|
354
|
+
let stateTimer = null;
|
|
355
|
+
let stopped = false;
|
|
356
|
+
|
|
357
|
+
function layers() { return normalizeUserContext((() => { try { return settings(); } catch { return null; } })()); }
|
|
358
|
+
|
|
359
|
+
// ── operational state file ──
|
|
360
|
+
function loadState() {
|
|
361
|
+
const empty = { version: STATE_VERSION, nudged: {}, threads: {} };
|
|
362
|
+
if (!statePath) return empty;
|
|
363
|
+
try {
|
|
364
|
+
const raw = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
365
|
+
if (!raw || typeof raw !== 'object') return empty;
|
|
366
|
+
return { version: STATE_VERSION, nudged: raw.nudged && typeof raw.nudged === 'object' ? raw.nudged : {}, threads: raw.threads && typeof raw.threads === 'object' ? raw.threads : {} };
|
|
367
|
+
} catch { return empty; }
|
|
368
|
+
}
|
|
369
|
+
function saveStateNow() {
|
|
370
|
+
if (stateTimer) { timers.clearTimeout(stateTimer); stateTimer = null; }
|
|
371
|
+
if (!statePath) return;
|
|
372
|
+
const nudged = Object.entries(state.nudged).sort((a, b) => Number(b[1]) - Number(a[1])).slice(0, NUDGES.nudgedKept);
|
|
373
|
+
state.nudged = Object.fromEntries(nudged);
|
|
374
|
+
const threads = Object.values(state.threads).sort((a, b) => String(b.lastAt || '').localeCompare(String(a.lastAt || ''))).slice(0, NUDGES.threadsKept);
|
|
375
|
+
state.threads = Object.fromEntries(threads.map((thread) => [thread.sessionId, thread]));
|
|
376
|
+
try {
|
|
377
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
378
|
+
const tmp = `${statePath}.${process.pid}.tmp`;
|
|
379
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
380
|
+
renameSync(tmp, statePath);
|
|
381
|
+
} catch (error) { log('user-context:state-error', error?.message || String(error)); }
|
|
382
|
+
}
|
|
383
|
+
function saveState() {
|
|
384
|
+
if (stateTimer || stopped) return;
|
|
385
|
+
stateTimer = timers.setTimeout(() => { stateTimer = null; saveStateNow(); }, 500);
|
|
386
|
+
stateTimer?.unref?.();
|
|
387
|
+
}
|
|
388
|
+
const nudgeKey = (sessionId, text) => `open:${sessionId}:${shortHash(String(text ?? '').toLowerCase())}`;
|
|
389
|
+
const wasNudged = (key) => Object.prototype.hasOwnProperty.call(state.nudged, key);
|
|
390
|
+
function markNudged(uc, key) {
|
|
391
|
+
state.nudged[key] = now();
|
|
392
|
+
uc.delivered.nudges = [...uc.delivered.nudges.filter((k) => k !== key), key].slice(-100);
|
|
393
|
+
saveState();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ── digest ──
|
|
397
|
+
function buildDigest(record) {
|
|
398
|
+
const rows = store ? store.listKnowledge({ fresh: true }) : [];
|
|
399
|
+
const digest = renderDigest(rows, { project: record.project || 'global', now: now() });
|
|
400
|
+
return { text: digest.text, tokens: digest.tokens, rows: digest.rows.map((row) => ({ id: row.id, revision: row.revision, statement: row.statement, facet: row.facet, seed: row.seed })), omitted: digest.omitted, candidates: digest.candidates, at: iso(now()), project: record.project || 'global' };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* What a brain is built for: a change means an idle brain restarts (the runtime's brainNeedsRestart).
|
|
405
|
+
* Every brain carries the digest in its persona; only the Claude brain is built for the nudges
|
|
406
|
+
* layer too (its CLI's environment tells pre-compact.mjs who keeps the thread record).
|
|
407
|
+
*/
|
|
408
|
+
function brainKey(record = null) {
|
|
409
|
+
const on = layers();
|
|
410
|
+
const claude = !record || record.brain?.provider === 'claude-code';
|
|
411
|
+
return `k${on.knowledge ? 1 : 0}${claude ? `n${on.nudges ? 1 : 0}` : ''}`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* The digest to append to the persona of a brain about to start (synchronous: a SQLite read at
|
|
416
|
+
* most). Built once per provider conversation and frozen on the session record.
|
|
417
|
+
* → { text, key, nudges, digest }
|
|
418
|
+
*/
|
|
419
|
+
function personaSuffix(record) {
|
|
420
|
+
const on = layers();
|
|
421
|
+
const key = brainKey(record);
|
|
422
|
+
if (!on.knowledge || !store) return { text: '', key, nudges: on.nudges, digest: null };
|
|
423
|
+
try {
|
|
424
|
+
const uc = userContextState(record);
|
|
425
|
+
if (!uc.digest) uc.digest = buildDigest(record);
|
|
426
|
+
if (uc.digest.text) installs.add(record.id);
|
|
427
|
+
return { text: uc.digest.text, key, nudges: on.nudges, digest: uc.digest };
|
|
428
|
+
} catch (error) {
|
|
429
|
+
log('user-context:digest-error', error?.message || String(error));
|
|
430
|
+
return { text: '', key, nudges: on.nudges, digest: null };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** The conversation was replaced (provider, account or project switch; a resume into another provider session). */
|
|
435
|
+
function conversationReplaced(record) {
|
|
436
|
+
const uc = userContextState(record);
|
|
437
|
+
uc.digest = null;
|
|
438
|
+
uc.delivered = freshDelivered();
|
|
439
|
+
uc.learnedHere = [];
|
|
440
|
+
lastThreads.delete(record.id);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** The brain's context was compacted or cleared: what was said once may be said again, where it still matters. */
|
|
444
|
+
function onCompaction(record) {
|
|
445
|
+
const uc = userContextState(record);
|
|
446
|
+
uc.delivered.cues = [];
|
|
447
|
+
uc.learnedHere = uc.learnedHere.map((item) => ({ ...item, compacted: true }));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** The categories recall keeps out of Related Memories (they reach the brain as the digest or a cue), or null. */
|
|
451
|
+
function divertCategories() {
|
|
452
|
+
if (!layers().knowledge || !store) return null;
|
|
453
|
+
try { return store.knowledgeCategories(); } catch { return null; }
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ── threads ──
|
|
457
|
+
function threadOf(record) {
|
|
458
|
+
let thread = state.threads[record.id];
|
|
459
|
+
if (!thread) { thread = newThread({ sessionId: record.id, title: record.title, project: record.project || 'global', at: null }); state.threads[record.id] = thread; }
|
|
460
|
+
return thread;
|
|
461
|
+
}
|
|
462
|
+
function cardsOf(sessionId) {
|
|
463
|
+
try { return cardGetters.get(sessionId)?.() || []; } catch { return []; }
|
|
464
|
+
}
|
|
465
|
+
async function writeThread(sessionId) {
|
|
466
|
+
const timer = threadTimers.get(sessionId);
|
|
467
|
+
if (timer) { timers.clearTimeout(timer); threadTimers.delete(sessionId); }
|
|
468
|
+
const thread = state.threads[sessionId];
|
|
469
|
+
if (!thread || !store || !(Number(thread.prompts) > 0) || !layers().nudges) return null;
|
|
470
|
+
const content = threadContent(thread, { cards: cardsOf(sessionId) });
|
|
471
|
+
const hash = shortHash(content);
|
|
472
|
+
if (hash === thread.hash && thread.memoryId) { thread.dirty = false; return thread.memoryId; }
|
|
473
|
+
const written = await store.writeThread({ sessionId, memoryId: thread.memoryId || null, content, project: thread.project || 'global' });
|
|
474
|
+
if (written?.id) { thread.memoryId = written.id; thread.hash = hash; thread.writtenAt = iso(now()); thread.dirty = false; saveState(); return written.id; }
|
|
475
|
+
log('user-context:thread-error', written?.error || 'thread write failed');
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
function scheduleThread(sessionId) {
|
|
479
|
+
if (stopped) return;
|
|
480
|
+
const existing = threadTimers.get(sessionId);
|
|
481
|
+
if (existing) timers.clearTimeout(existing);
|
|
482
|
+
const timer = timers.setTimeout(() => { threadTimers.delete(sessionId); writeThread(sessionId).catch((error) => log('user-context:thread-error', error?.message || String(error))); }, THREAD.writeDebounceMs);
|
|
483
|
+
timer?.unref?.();
|
|
484
|
+
threadTimers.set(sessionId, timer);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** The most recent other conversation, from the working threads and the store. */
|
|
488
|
+
function findLastThread(record) {
|
|
489
|
+
let best = null;
|
|
490
|
+
const consider = (candidate) => {
|
|
491
|
+
if (!candidate?.title || !candidate.sessionId || candidate.sessionId === record.id) return;
|
|
492
|
+
const ended = Date.parse(candidate.endedAt ?? '');
|
|
493
|
+
if (!Number.isFinite(ended)) return;
|
|
494
|
+
if (!best || ended > Date.parse(best.endedAt)) best = candidate;
|
|
495
|
+
};
|
|
496
|
+
for (const thread of Object.values(state.threads)) {
|
|
497
|
+
if (!(Number(thread.prompts) > 0)) continue;
|
|
498
|
+
consider({ title: thread.title, project: thread.project, endedAt: thread.lastAt, sessionId: thread.sessionId, open: openItems(thread, { cards: cardsOf(thread.sessionId) }).map((item) => item.text) });
|
|
499
|
+
}
|
|
500
|
+
if (store) {
|
|
501
|
+
for (const row of store.lastThreads({ limit: 5 })) {
|
|
502
|
+
const parsed = parseThread(row.content, { updatedAt: row.updated_at });
|
|
503
|
+
// A working thread is newer than its own memory.
|
|
504
|
+
if (parsed && !state.threads[parsed.sessionId]) consider({ ...parsed, project: parsed.project || row.project || 'global', memoryId: row.id, revision: row.revision });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return best;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ── a turn ──
|
|
511
|
+
/**
|
|
512
|
+
* A human turn about to reach the brain. → the turn's handle (never null).
|
|
513
|
+
* input: { prompt, typed, origin, untrusted, generation, previousReply, cwd, taskId,
|
|
514
|
+
* emit(event), openCards() }
|
|
515
|
+
*/
|
|
516
|
+
function beginTurn(record, input = {}) {
|
|
517
|
+
const on = layers();
|
|
518
|
+
const uc = userContextState(record);
|
|
519
|
+
const at = now();
|
|
520
|
+
const handle = {
|
|
521
|
+
traceId: `auto-${randomUUID().slice(0, 12)}`, sessionId: record.id, at, layers: on,
|
|
522
|
+
prompt: String(input.prompt ?? ''), origin: input.origin || null, generation: Number(input.generation) || 1,
|
|
523
|
+
taskId: input.taskId || null, previousReply: String(input.previousReply ?? ''),
|
|
524
|
+
first: !uc.delivered.opened, env: [], capture: null,
|
|
525
|
+
// The greeting this conversation opened with (lib/greeting/), to be said once: { id, text, ids, source, model, costUsd } | null
|
|
526
|
+
greeting: input.greeting && typeof input.greeting.text === 'string' && input.greeting.text.trim() ? input.greeting : null,
|
|
527
|
+
injected: [], withheld: [], recall: null, block: '', blockTokens: 0, built: false, phase: 'pending', outcome: null,
|
|
528
|
+
learning: null, emit: on.trace && typeof input.emit === 'function' ? input.emit : null, persist: typeof input.persist === 'function' ? input.persist : null,
|
|
529
|
+
};
|
|
530
|
+
try {
|
|
531
|
+
uc.delivered.opened = true;
|
|
532
|
+
uc.totals.turns = count(uc.totals.turns) + 1;
|
|
533
|
+
if (typeof input.openCards === 'function') cardGetters.set(record.id, input.openCards);
|
|
534
|
+
if (on.nudges) {
|
|
535
|
+
const typed = typedEvidence({ prompt: handle.prompt, typed: input.typed });
|
|
536
|
+
noteAsk(threadOf(record), { text: typed || handle.prompt, at: iso(at), title: record.title, project: record.project || 'global' });
|
|
537
|
+
saveState();
|
|
538
|
+
// What changed around the conversation since its last turn.
|
|
539
|
+
const branch = (() => { try { return readBranch(input.cwd || record.brain?.cwd || '') || null; } catch { return null; } })();
|
|
540
|
+
const before = uc.env;
|
|
541
|
+
if (before.lastTurnAt) {
|
|
542
|
+
if (before.branch && branch && before.branch !== branch) handle.env.push({ key: 'branch', text: `Git branch changed since the last turn: ${before.branch} → ${branch}.` });
|
|
543
|
+
if (before.bootId && before.bootId !== bootId) handle.env.push({ key: 'restart', text: 'SynaBun restarted since the last turn: workers and browser tabs from before are gone.' });
|
|
544
|
+
const gapHours = (at - Date.parse(before.lastTurnAt)) / 3_600_000;
|
|
545
|
+
if (Number.isFinite(gapHours) && gapHours >= NUDGES.gapHours) handle.env.push({ key: 'gap', text: `${gapHours >= 36 ? `${Math.round(gapHours / 24)} days` : `${Math.round(gapHours)} h`} passed since the last message here.` });
|
|
546
|
+
}
|
|
547
|
+
uc.env = { branch: branch || before.branch || null, bootId, lastTurnAt: iso(at) };
|
|
548
|
+
}
|
|
549
|
+
handle.capture = on.knowledge
|
|
550
|
+
? prefilter({ origin: input.origin, untrusted: input.untrusted === true, system: false, prompt: handle.prompt, typed: input.typed }, uc.learn)
|
|
551
|
+
: { pass: false, reason: 'layer-off', cues: [], chars: 0 };
|
|
552
|
+
// The digest's memories are in the context for good: Related Memories never repeats them.
|
|
553
|
+
if (on.knowledge && uc.digest?.rows?.length && typeof markInjected === 'function') {
|
|
554
|
+
try { markInjected(uc.digest.rows, { session: record.id, generation: handle.generation }); } catch { /* the ledger is an optimisation */ }
|
|
555
|
+
}
|
|
556
|
+
} catch (error) { log('user-context:turn-error', error?.message || String(error)); }
|
|
557
|
+
return handle;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* The per-turn block, built once the turn's recall result is known. → { text, tokens }
|
|
562
|
+
* `recalled`: assistant-memory.js recallForPrompt's answer ({ block, results, skipped, decision,
|
|
563
|
+
* diverted:[{ id, revision, category, content, relevance, ranked, … }] }), or null.
|
|
564
|
+
*/
|
|
565
|
+
function turnBlock(record, handle, recalled = null) {
|
|
566
|
+
if (!handle || handle.built) return { text: handle?.block || '', tokens: handle?.blockTokens || 0 };
|
|
567
|
+
handle.built = true;
|
|
568
|
+
const on = handle.layers;
|
|
569
|
+
const uc = userContextState(record);
|
|
570
|
+
const items = []; // { tier, text, ids, key?, commit() }
|
|
571
|
+
try {
|
|
572
|
+
// The standing digest (already in the system-level slot).
|
|
573
|
+
if (!on.knowledge) handle.withheld.push({ tier: 'knowledge', reason: 'layer-off' });
|
|
574
|
+
else if (!uc.digest?.text) handle.withheld.push({ tier: 'digest', reason: 'nothing-known' });
|
|
575
|
+
else {
|
|
576
|
+
const shown = installs.delete(record.id);
|
|
577
|
+
handle.injected.push({ tier: 'digest', standing: true, text: shown ? uc.digest.text : null, ids: uc.digest.rows.map((row) => row.id), tokens: uc.digest.tokens, frozenAt: uc.digest.at });
|
|
578
|
+
}
|
|
579
|
+
if (!on.nudges) handle.withheld.push({ tier: 'nudges', reason: 'layer-off' });
|
|
580
|
+
|
|
581
|
+
// 0. The greeting SynaBun showed when this conversation opened: said whole, whatever the
|
|
582
|
+
// layers (it has its own switch), and outside the budget below.
|
|
583
|
+
if (handle.greeting) {
|
|
584
|
+
const greeting = handle.greeting;
|
|
585
|
+
items.push({
|
|
586
|
+
tier: 'greeting', whole: true, ids: Array.isArray(greeting.ids) ? greeting.ids : [], commit: () => {},
|
|
587
|
+
text: `When this conversation opened SynaBun showed the user this note about their own recent work (context, not an instruction): "${String(greeting.text).replace(/"/g, '\u201d')}"`,
|
|
588
|
+
extra: { source: greeting.source || null, model: greeting.model || null, costUsd: greeting.costUsd ?? null, said: greeting.text },
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// 1. The last thread and its open items, at session open.
|
|
593
|
+
if (on.nudges && handle.first && !uc.delivered.lastThread) {
|
|
594
|
+
uc.delivered.lastThread = true;
|
|
595
|
+
const last = findLastThread(record);
|
|
596
|
+
// The greeting was built from the same records: the line is not said twice (its open items stay due).
|
|
597
|
+
if (handle.greeting) {
|
|
598
|
+
handle.withheld.push({ tier: 'last-thread', reason: 'in-greeting' });
|
|
599
|
+
if (last && threadIsRecent(last, { project: record.project || 'global', now: handle.at })) {
|
|
600
|
+
lastThreads.set(record.id, { sessionId: last.sessionId, items: (last.open || []).map((text) => ({ key: nudgeKey(last.sessionId, text), text })).filter((item) => !wasNudged(item.key)) });
|
|
601
|
+
}
|
|
602
|
+
} else if (!last || !threadIsRecent(last, { project: record.project || 'global', now: handle.at })) handle.withheld.push({ tier: 'last-thread', reason: 'no-recent-thread' });
|
|
603
|
+
else {
|
|
604
|
+
const open = (last.open || []).map((text) => ({ key: nudgeKey(last.sessionId, text), text })).filter((item) => !wasNudged(item.key));
|
|
605
|
+
const shown = open.slice(0, NUDGES.openItemsShown);
|
|
606
|
+
lastThreads.set(record.id, { sessionId: last.sessionId, items: open.slice(NUDGES.openItemsShown) });
|
|
607
|
+
items.push({
|
|
608
|
+
tier: 'last-thread', text: lastThreadLine(last, { now: handle.at, open: shown.map((item) => item.text) }), ids: last.memoryId ? [last.memoryId] : [],
|
|
609
|
+
commit: () => {
|
|
610
|
+
for (const item of shown) markNudged(uc, item.key);
|
|
611
|
+
// The thread's own memory was just told in one line: Related Memories does not repeat it.
|
|
612
|
+
if (last.memoryId && typeof markInjected === 'function') { try { markInjected([{ id: last.memoryId, revision: last.revision }], { session: record.id, generation: handle.generation }); } catch { /* optimisation */ } }
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
// 2. What changed around the conversation.
|
|
618
|
+
if (on.nudges) for (const change of handle.env) items.push({ tier: 'environment', text: change.text, ids: [], commit: () => {} });
|
|
619
|
+
// 3. An open item, when the prompt touches it or closes the conversation.
|
|
620
|
+
if (on.nudges) {
|
|
621
|
+
const own = openItems(threadOf(record), { cards: cardsOf(record.id) }).map((item) => ({ key: nudgeKey(record.id, item.text), text: item.text }));
|
|
622
|
+
const carried = lastThreads.get(record.id)?.items || [];
|
|
623
|
+
const closing = isClosingTurn(handle.prompt);
|
|
624
|
+
const due = [...own, ...carried].filter((item) => !wasNudged(item.key) && (closing || overlap(handle.prompt, item.text) >= NUDGES.overlapWords)).slice(0, NUDGES.openItemsShown);
|
|
625
|
+
if (due.length) items.push({ tier: 'open-item', text: `Still open: ${due.map((item) => item.text).join('; ')}.`, ids: [], commit: () => { for (const item of due) markNudged(uc, item.key); } });
|
|
626
|
+
}
|
|
627
|
+
// 4. One knowledge cue: a statement Jev ranked relevant, kept out of Related Memories.
|
|
628
|
+
if (on.knowledge) {
|
|
629
|
+
const diverted = Array.isArray(recalled?.diverted) ? recalled.diverted : [];
|
|
630
|
+
const inDigest = new Set((uc.digest?.rows || []).map((row) => row.id));
|
|
631
|
+
const replaced = new Set();
|
|
632
|
+
if (diverted.length && store) for (const item of store.listKnowledge()) if (item.structured) for (const id of item.links || []) replaced.add(id);
|
|
633
|
+
let picked = null;
|
|
634
|
+
for (const row of diverted) {
|
|
635
|
+
if (!row?.id) continue;
|
|
636
|
+
const reason = !row.ranked ? 'not-ranked' : inDigest.has(row.id) ? 'in-digest' : uc.delivered.cues.includes(row.id) ? 'already-told' : replaced.has(row.id) ? 'replaced' : null;
|
|
637
|
+
const parsed = parseKnowledge(row);
|
|
638
|
+
if (reason || !parsed.statement) { handle.withheld.push({ tier: 'cue', reason: reason || 'unusable', ids: [row.id] }); continue; }
|
|
639
|
+
if (picked) { handle.withheld.push({ tier: 'cue', reason: 'one-per-turn', ids: [row.id] }); continue; }
|
|
640
|
+
picked = { row, statement: parsed.statement };
|
|
641
|
+
}
|
|
642
|
+
if (picked) {
|
|
643
|
+
items.push({
|
|
644
|
+
tier: 'cue', text: `About the user, relevant here: ${picked.statement}`, ids: [picked.row.id],
|
|
645
|
+
commit: () => {
|
|
646
|
+
uc.delivered.cues = [...uc.delivered.cues, picked.row.id].slice(-200);
|
|
647
|
+
if (typeof markInjected === 'function') { try { markInjected([{ id: picked.row.id, revision: picked.row.revision }], { session: record.id, generation: handle.generation }); } catch { /* optimisation */ } }
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// 5. Delta: knowledge learned since the digest was frozen, or a fact that replaces one of its lines.
|
|
653
|
+
if (on.knowledge && uc.digest && store) {
|
|
654
|
+
const frozenAt = Date.parse(uc.digest.at);
|
|
655
|
+
const digestRows = new Map(uc.digest.rows.map((row) => [row.id, row]));
|
|
656
|
+
const here = new Map(uc.learnedHere.map((item) => [item.id, item]));
|
|
657
|
+
const fresh = usableKnowledge(store.listKnowledge(), { project: record.project || 'global' }).filter((item) => {
|
|
658
|
+
const updated = Date.parse(item.updatedAt ?? '');
|
|
659
|
+
if (!Number.isFinite(updated) || updated <= frozenAt) return false;
|
|
660
|
+
if (uc.delivered.deltas.includes(item.id)) return false;
|
|
661
|
+
const inDigest = digestRows.get(item.id);
|
|
662
|
+
if (inDigest && inDigest.statement === item.statement) return false;
|
|
663
|
+
// Learned in this conversation: the brain heard the user say it, until a compaction.
|
|
664
|
+
const mine = here.get(item.id);
|
|
665
|
+
return !mine || mine.compacted === true;
|
|
666
|
+
}).slice(0, TURN_BLOCK.deltasPerTurn);
|
|
667
|
+
const replacedBy = fresh.length ? store.supersededBy([...digestRows.keys()]) : new Map();
|
|
668
|
+
const oldOf = new Map([...replacedBy].map(([oldId, newId]) => [newId, digestRows.get(oldId)]));
|
|
669
|
+
for (const item of fresh) {
|
|
670
|
+
const old = oldOf.get(item.id) || (digestRows.has(item.id) ? digestRows.get(item.id) : null);
|
|
671
|
+
const text = old?.statement && old.statement !== item.statement
|
|
672
|
+
? `Update about the user (replaces "${clipText(old.statement, 60)}"): ${item.statement}`
|
|
673
|
+
: `Newly learned about the user: ${item.statement}`;
|
|
674
|
+
items.push({ tier: 'delta', text, ids: [item.id], commit: () => { uc.delivered.deltas = [...uc.delivered.deltas, item.id].slice(-200); } });
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Budget: items in order while they fit; what does not fit is withheld (and stays due).
|
|
679
|
+
let budget = handle.first ? TURN_BLOCK.firstTokens : TURN_BLOCK.laterTokens;
|
|
680
|
+
let text = '';
|
|
681
|
+
for (const item of items) {
|
|
682
|
+
// `whole` (the greeting): never clipped, and it does not take the other items' room.
|
|
683
|
+
const line = `- ${item.whole ? item.text : clipText(item.text, TURN_BLOCK.itemMaxChars)}`;
|
|
684
|
+
const next = text ? `${text}\n${line}` : `${TURN_BLOCK_HEADER}\n${line}`;
|
|
685
|
+
if (item.whole) budget += estimateTokens(line);
|
|
686
|
+
else if (estimateTokens(next) > budget) { handle.withheld.push({ tier: item.tier, reason: 'budget', text: item.text, ids: item.ids }); continue; }
|
|
687
|
+
text = next;
|
|
688
|
+
try { item.commit(); } catch { /* a ledger write must not cost the item */ }
|
|
689
|
+
handle.injected.push({ tier: item.tier, text: item.extra?.said || line.slice(2), ids: item.ids, tokens: estimateTokens(line), ...(item.extra ? { source: item.extra.source, model: item.extra.model, costUsd: item.extra.costUsd } : {}) });
|
|
690
|
+
}
|
|
691
|
+
handle.block = text;
|
|
692
|
+
handle.blockTokens = text ? estimateTokens(text) : 0;
|
|
693
|
+
uc.totals.injectedTokens = count(uc.totals.injectedTokens) + handle.blockTokens;
|
|
694
|
+
|
|
695
|
+
// Related Memories, for the whole picture of what this turn carried.
|
|
696
|
+
if (recalled) {
|
|
697
|
+
const ids = Array.isArray(recalled.results) ? recalled.results.map((row) => row.id).filter(Boolean) : [];
|
|
698
|
+
handle.recall = { skipped: recalled.skipped || null, decision: recalled.decision || null, tokens: recalled.block ? estimateTokens(recalled.block) : 0, ids, alreadyPresent: recalled.alreadyPresent === true };
|
|
699
|
+
if (recalled.block) handle.injected.push({ tier: 'recall', text: null, ids, tokens: handle.recall.tokens, decision: handle.recall.decision, alreadyPresent: handle.recall.alreadyPresent });
|
|
700
|
+
else handle.withheld.push({ tier: 'recall', reason: recalled.skipped || (recalled.decision === 'skip' ? 'judged' : 'nothing-retrieved') });
|
|
701
|
+
}
|
|
702
|
+
} catch (error) {
|
|
703
|
+
log('user-context:block-error', error?.message || String(error));
|
|
704
|
+
handle.block = '';
|
|
705
|
+
handle.blockTokens = 0;
|
|
706
|
+
}
|
|
707
|
+
handle.phase = 'sent';
|
|
708
|
+
emit(record, handle);
|
|
709
|
+
handle.persist?.();
|
|
710
|
+
return { text: handle.block, tokens: handle.blockTokens };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function learningView(handle) {
|
|
714
|
+
const capture = handle.capture || { pass: false, reason: 'layer-off', cues: [], chars: 0 };
|
|
715
|
+
const view = { state: 'skipped', prefilter: { pass: capture.pass === true, reason: capture.reason || null, cues: capture.cues || [], chars: capture.chars || 0 } };
|
|
716
|
+
if (!capture.pass) return view;
|
|
717
|
+
if (!handle.learning) return { ...view, state: handle.phase === 'sent' ? 'waiting' : 'running' };
|
|
718
|
+
return { ...view, state: 'done', gate: handle.learning.gate, writer: handle.learning.writer, action: handle.learning.action, cost: handle.learning.cost };
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/** The turn's full snapshot (one `synabun.automation` event per phase, the same traceId). */
|
|
722
|
+
function snapshot(record, handle) {
|
|
723
|
+
const uc = userContextState(record);
|
|
724
|
+
const sum = (tier) => handle.injected.filter((item) => (tier ? item.tier === tier : true)).reduce((total, item) => total + count(item.tokens), 0);
|
|
725
|
+
return {
|
|
726
|
+
type: AUTOMATION_EVENT, traceId: handle.traceId, phase: handle.phase, at: iso(now()), outcome: handle.outcome,
|
|
727
|
+
first: handle.first, layers: { knowledge: handle.layers.knowledge, nudges: handle.layers.nudges },
|
|
728
|
+
injected: handle.injected.map((item) => ({ ...item })),
|
|
729
|
+
withheld: handle.withheld.map((item) => ({ ...item })),
|
|
730
|
+
learning: learningView(handle),
|
|
731
|
+
tokens: { digest: sum('digest'), turn: handle.blockTokens, recall: sum('recall'), budget: handle.first ? TURN_BLOCK.firstTokens : TURN_BLOCK.laterTokens, digestBudget: DIGEST.maxTokens },
|
|
732
|
+
totals: {
|
|
733
|
+
turns: count(uc.totals.turns), injectedTokens: count(uc.totals.injectedTokens), learned: count(uc.totals.learned),
|
|
734
|
+
gate: count(uc.learn.counts.gate), gateCap: LEARN.gateCap, writer: count(uc.learn.counts.writer), writerCap: LEARN.writerCap,
|
|
735
|
+
writes: count(uc.learn.counts.writes), writeCap: LEARN.writeCap, jevRequests: count(uc.totals.jevRequests), costUsd: Number(Number(uc.totals.costUsd || 0).toFixed(6)),
|
|
736
|
+
},
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function emit(record, handle) {
|
|
740
|
+
if (!handle.emit || (!handle.layers.knowledge && !handle.layers.nudges && !handle.greeting)) return;
|
|
741
|
+
try { handle.emit(snapshot(record, handle)); } catch (error) { log('user-context:trace-error', error?.message || String(error)); }
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async function runLearning(record, handle) {
|
|
745
|
+
const uc = userContextState(record);
|
|
746
|
+
const capture = handle.capture;
|
|
747
|
+
let result = null;
|
|
748
|
+
try {
|
|
749
|
+
result = await learner.learn({
|
|
750
|
+
sessionId: record.id, project: record.project || 'global', taskId: handle.taskId,
|
|
751
|
+
text: capture.text, sentences: capture.sentences, cues: capture.cues, hash: capture.hash, previousReply: handle.previousReply,
|
|
752
|
+
}, uc.learn);
|
|
753
|
+
} catch (error) {
|
|
754
|
+
log('user-context:learn-error', error?.message || String(error));
|
|
755
|
+
result = { gate: null, writer: null, action: { type: 'none', reason: 'error' }, cost: { jevRequests: 0, jevInputTokens: 0, jevOutputTokens: 0, jevUsd: 0, writerTokens: null, writerUsd: 0, totalUsd: 0 } };
|
|
756
|
+
}
|
|
757
|
+
try {
|
|
758
|
+
uc.totals.jevRequests = count(uc.totals.jevRequests) + count(result.cost?.jevRequests);
|
|
759
|
+
uc.totals.jevInputTokens = count(uc.totals.jevInputTokens) + count(result.cost?.jevInputTokens);
|
|
760
|
+
uc.totals.costUsd = Number(((Number(uc.totals.costUsd) || 0) + (Number(result.cost?.totalUsd) || 0)).toFixed(8));
|
|
761
|
+
if (result.writer?.used) {
|
|
762
|
+
uc.totals.writerCalls = count(uc.totals.writerCalls) + 1;
|
|
763
|
+
// No draft: the one-shot itself failed (no Claude login, a timeout). A draft Jev turned down is the writer working.
|
|
764
|
+
const failed = !result.writer.draft;
|
|
765
|
+
health.writer = { ok: !failed, reason: failed ? String(result.writer.reason || 'writer-failed').slice(0, 160) : null, at: iso(now()) };
|
|
766
|
+
}
|
|
767
|
+
// The writer's tokens are a row of their own in the usage ledger (part 'aux': nothing pending is cleared).
|
|
768
|
+
const tokens = result.writer?.tokens;
|
|
769
|
+
if (usage?.settle && tokens && (count(tokens.input) + count(tokens.output) + count(tokens.cacheRead) + count(tokens.cacheWrite)) > 0) {
|
|
770
|
+
try {
|
|
771
|
+
usage.settle({
|
|
772
|
+
sessionId: record.id, taskId: handle.taskId || undefined, scope: 'brain', provider: 'claude-code', model: result.writer.model || null, part: 'aux',
|
|
773
|
+
tokens, costUsd: Number(result.writer.costUsd) > 0 ? Number(result.writer.costUsd) : undefined, costBasis: Number(result.writer.costUsd) > 0 ? 'reported' : null,
|
|
774
|
+
reason: 'statement-writer', source: 'user-context',
|
|
775
|
+
});
|
|
776
|
+
} catch (error) { log('user-context:usage-error', error?.message || String(error)); }
|
|
777
|
+
}
|
|
778
|
+
if (result.action?.type && result.action.type !== 'none' && result.action.memoryId) {
|
|
779
|
+
uc.totals.learned = count(uc.totals.learned) + 1;
|
|
780
|
+
uc.learnedHere = [...uc.learnedHere.filter((item) => item.id !== result.action.memoryId), { id: result.action.memoryId, at: iso(now()), compacted: false }].slice(-50);
|
|
781
|
+
}
|
|
782
|
+
} catch (error) { log('user-context:learn-error', error?.message || String(error)); }
|
|
783
|
+
handle.learning = result;
|
|
784
|
+
handle.phase = 'learned';
|
|
785
|
+
emit(record, handle);
|
|
786
|
+
handle.persist?.();
|
|
787
|
+
return result;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* The turn ended (`done`, `aborted` or `error`: the same for every brain). The trace moves under
|
|
792
|
+
* the reply; learning runs now, off the reply path, one turn at a time per session.
|
|
793
|
+
* → the learning promise (resolved at once when there is nothing to learn from).
|
|
794
|
+
*/
|
|
795
|
+
function endTurn(record, handle, { outcome = 'done' } = {}) {
|
|
796
|
+
if (!handle || handle.ended) return Promise.resolve(null);
|
|
797
|
+
handle.ended = true;
|
|
798
|
+
handle.outcome = outcome;
|
|
799
|
+
// A Claude turn whose hook never ran (the CLI queued it away): its block was never built.
|
|
800
|
+
if (!handle.built) { handle.built = true; handle.withheld.push({ tier: 'turn', reason: 'not-delivered' }); }
|
|
801
|
+
handle.phase = 'turn_end';
|
|
802
|
+
emit(record, handle);
|
|
803
|
+
try {
|
|
804
|
+
if (handle.layers.nudges) {
|
|
805
|
+
const thread = threadOf(record);
|
|
806
|
+
thread.lastAt = iso(now());
|
|
807
|
+
if (record.title) thread.title = record.title;
|
|
808
|
+
thread.dirty = true;
|
|
809
|
+
saveState();
|
|
810
|
+
scheduleThread(record.id);
|
|
811
|
+
}
|
|
812
|
+
} catch (error) { log('user-context:thread-error', error?.message || String(error)); }
|
|
813
|
+
handle.persist?.();
|
|
814
|
+
if (!handle.capture?.pass || !handle.layers.knowledge) return Promise.resolve(null);
|
|
815
|
+
const previous = chains.get(record.id) || Promise.resolve();
|
|
816
|
+
const next = previous.catch(() => {}).then(() => runLearning(record, handle));
|
|
817
|
+
chains.set(record.id, next);
|
|
818
|
+
next.finally(() => { if (chains.get(record.id) === next) chains.delete(record.id); }).catch(() => {});
|
|
819
|
+
return next;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/** The turn never reached the brain: nothing is traced or learned. */
|
|
823
|
+
function cancelTurn(record, handle) {
|
|
824
|
+
if (!handle) return;
|
|
825
|
+
handle.ended = true;
|
|
826
|
+
handle.built = true;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/** A dispatch event of the conversation (assistant-dispatch.js): its runs and their outcomes join the thread. */
|
|
830
|
+
function onDispatch(record, payload = {}) {
|
|
831
|
+
if (!layers().nudges) return;
|
|
832
|
+
try {
|
|
833
|
+
const thread = threadOf(record);
|
|
834
|
+
if (payload.type === 'assistant:run-removed') { for (const runId of payload.runIds || []) dropRun(thread, runId); }
|
|
835
|
+
else if (payload.run?.runId) noteRun(thread, payload.run, payload.reason || '', iso(now()));
|
|
836
|
+
if (thread.dirty) { saveState(); if (Number(thread.prompts) > 0) scheduleThread(record.id); }
|
|
837
|
+
} catch (error) { log('user-context:thread-error', error?.message || String(error)); }
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** The conversation closed: its thread memory is written now. */
|
|
841
|
+
function sessionClosed(sessionId) {
|
|
842
|
+
return writeThread(String(sessionId)).catch((error) => { log('user-context:thread-error', error?.message || String(error)); return null; });
|
|
843
|
+
}
|
|
844
|
+
/** The conversation was deleted: its working state goes (the thread memory stays: it is the owner's memory). */
|
|
845
|
+
function sessionDestroyed(sessionId) {
|
|
846
|
+
const id = String(sessionId);
|
|
847
|
+
const timer = threadTimers.get(id);
|
|
848
|
+
if (timer) { timers.clearTimeout(timer); threadTimers.delete(id); }
|
|
849
|
+
delete state.threads[id];
|
|
850
|
+
cardGetters.delete(id);
|
|
851
|
+
lastThreads.delete(id);
|
|
852
|
+
installs.delete(id);
|
|
853
|
+
saveState();
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/** What a new conversation in `project` would be told (Settings → Automation behavior). */
|
|
857
|
+
function preview({ project = 'global' } = {}) {
|
|
858
|
+
const rows = store ? store.listKnowledge({ fresh: true }) : [];
|
|
859
|
+
const digest = renderDigest(rows, { project, now: now() });
|
|
860
|
+
return {
|
|
861
|
+
project, text: digest.text, tokens: digest.tokens, maxTokens: DIGEST.maxTokens, perFacet: DIGEST.perFacet,
|
|
862
|
+
rows: digest.rows, omitted: digest.omitted, candidates: digest.candidates,
|
|
863
|
+
total: rows.length, structured: rows.filter((row) => row.structured).length,
|
|
864
|
+
categories: store ? store.knowledgeCategories() : [CATEGORIES.knowledge],
|
|
865
|
+
turnBudget: { first: TURN_BLOCK.firstTokens, later: TURN_BLOCK.laterTokens },
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* What the layers can do on this server right now (Settings shows what is not there).
|
|
871
|
+
* `jevReason`: why learning cannot run (learningJudgeReason, 'not-configured' without a judge), null while it can.
|
|
872
|
+
* `writerLast`: { ok, reason, at } of the statement writer's last one-shot since the server started, or null.
|
|
873
|
+
*/
|
|
874
|
+
function capabilities() {
|
|
875
|
+
const jev = learner.available();
|
|
876
|
+
let jevReason = null;
|
|
877
|
+
if (!jev) {
|
|
878
|
+
try { jevReason = (judge && typeof judge.reason === 'function' ? judge.reason() : null) || (judge ? 'unavailable' : 'not-configured'); }
|
|
879
|
+
catch { jevReason = 'unavailable'; }
|
|
880
|
+
}
|
|
881
|
+
return {
|
|
882
|
+
jev, jevReason,
|
|
883
|
+
writer: !!writer && (typeof writer.available !== 'function' || !!writer.available()),
|
|
884
|
+
writerLast: health.writer ? { ...health.writer } : null,
|
|
885
|
+
store: !!store,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/** Learning under way for a session (tests and shutdown wait on it). */
|
|
890
|
+
function idle(sessionId = null) {
|
|
891
|
+
const pending = sessionId ? [chains.get(String(sessionId))].filter(Boolean) : [...chains.values()];
|
|
892
|
+
return Promise.allSettled(pending);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function shutdown() {
|
|
896
|
+
stopped = true;
|
|
897
|
+
for (const timer of threadTimers.values()) timers.clearTimeout(timer);
|
|
898
|
+
threadTimers.clear();
|
|
899
|
+
saveStateNow();
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
return {
|
|
903
|
+
layers, brainKey, personaSuffix, conversationReplaced, onCompaction, divertCategories,
|
|
904
|
+
beginTurn, turnBlock, endTurn, cancelTurn, onDispatch, sessionClosed, sessionDestroyed,
|
|
905
|
+
preview, capabilities, idle, shutdown, flushThread: writeThread,
|
|
906
|
+
_internals: { state: () => state, saveStateNow, snapshot, learner },
|
|
907
|
+
};
|
|
908
|
+
}
|