synabun 2.0.0 → 2.0.7
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 +97 -0
- package/NOTICE +5 -0
- package/README.md +2 -1
- package/THIRD-PARTY-LICENSES.md +59 -0
- package/benchmarks/fixtures/browser/README.md +1 -1
- package/benchmarks/fixtures/desktop/README.md +1 -1
- package/docs/whatsapp.md +153 -18
- 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 +26 -3
- package/hooks/claude-code/stop.mjs +5 -5
- package/lib/child-path.js +303 -0
- package/lib/claude-hooks.js +2 -1
- package/lib/data-home-migration.js +2 -0
- package/lib/desktop-app.js +265 -0
- package/lib/external-tools.js +7 -3
- package/lib/log-redact.js +102 -0
- package/lib/packaged-runtime.js +138 -11
- package/lib/start-launcher.js +88 -1
- package/mcp-server/dist/http.d.ts +3 -1
- package/mcp-server/dist/http.d.ts.map +1 -1
- package/mcp-server/dist/http.js +15 -4
- package/mcp-server/dist/http.js.map +1 -1
- package/mcp-server/dist/index.d.ts.map +1 -1
- package/mcp-server/dist/index.js +9 -6
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/budget-judgments.js +153 -0
- package/mcp-server/dist/services/budget-judgments.js.map +1 -0
- package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.js +41 -4
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +11 -0
- package/mcp-server/dist/services/identity.d.ts.map +1 -1
- package/mcp-server/dist/services/identity.js +24 -0
- package/mcp-server/dist/services/identity.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +13 -0
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js +34 -2
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
- package/mcp-server/dist/services/typesafe-config.js +4 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +10 -7
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +34 -13
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
- package/mcp-server/dist/tui.js +2 -1
- package/mcp-server/dist/tui.js.map +1 -1
- package/mcp-server/dist/version.d.ts +5 -0
- package/mcp-server/dist/version.d.ts.map +1 -0
- package/mcp-server/dist/version.js +21 -0
- package/mcp-server/dist/version.js.map +1 -0
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -3
- package/mcp-server/run.mjs +15 -9
- package/mcp-server/src/http.ts +18 -3
- package/mcp-server/src/index.ts +9 -6
- package/mcp-server/src/services/budget-judgments.ts +183 -0
- package/mcp-server/src/services/codex-config-heal.ts +35 -4
- package/mcp-server/src/services/identity.ts +27 -0
- package/mcp-server/src/services/neural-interface.ts +34 -2
- package/mcp-server/src/services/typesafe-config.ts +4 -1
- package/mcp-server/src/tools/agents.ts +33 -14
- package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
- package/mcp-server/src/tui.ts +2 -1
- package/mcp-server/src/version.ts +22 -0
- package/neural-interface/i18n/en.json +2126 -209
- package/neural-interface/i18n/pt-BR.json +2123 -207
- package/neural-interface/lib/account-usage.js +163 -0
- package/neural-interface/lib/assistant-api.js +316 -57
- package/neural-interface/lib/assistant-brains/claude.js +19 -2
- package/neural-interface/lib/assistant-brains/codex.js +3 -0
- package/neural-interface/lib/assistant-brains/opencode.js +17 -4
- package/neural-interface/lib/assistant-budget-planner.js +390 -0
- package/neural-interface/lib/assistant-budget-split.js +408 -0
- package/neural-interface/lib/assistant-budget.js +94 -95
- package/neural-interface/lib/assistant-catalog.js +179 -27
- package/neural-interface/lib/assistant-config.js +91 -14
- package/neural-interface/lib/assistant-dispatch.js +494 -138
- package/neural-interface/lib/assistant-hidden-models.js +393 -66
- package/neural-interface/lib/assistant-jev.js +26 -3
- package/neural-interface/lib/assistant-memory.js +24 -10
- package/neural-interface/lib/assistant-persona.js +73 -12
- package/neural-interface/lib/assistant-plan-permissions.js +8 -1
- package/neural-interface/lib/assistant-pricing.js +21 -6
- package/neural-interface/lib/assistant-route-accounts.js +14 -0
- package/neural-interface/lib/assistant-route-gate.js +19 -3
- package/neural-interface/lib/assistant-router.js +373 -42
- package/neural-interface/lib/assistant-runtime.js +488 -59
- 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 +183 -0
- package/neural-interface/lib/augmented-path.js +317 -53
- package/neural-interface/lib/browser-background-tab.js +134 -0
- package/neural-interface/lib/browser-screenshot.js +678 -61
- package/neural-interface/lib/claude-accounts.js +342 -31
- package/neural-interface/lib/claude-agent-bridge.js +123 -9
- package/neural-interface/lib/claude-executable.js +6 -1
- package/neural-interface/lib/claude-model-catalog.js +33 -5
- package/neural-interface/lib/claude-panel-auth.js +221 -0
- package/neural-interface/lib/claude-profile-api-key.js +62 -0
- package/neural-interface/lib/claude-temporary.js +46 -7
- package/neural-interface/lib/cli-install-api.js +137 -0
- package/neural-interface/lib/cli-install.js +797 -0
- package/neural-interface/lib/cli-path.js +130 -0
- package/neural-interface/lib/cli-resolver.js +744 -0
- package/neural-interface/lib/cli-update-planner.js +143 -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/codex-account-auth.js +178 -0
- package/neural-interface/lib/codex-accounts.js +489 -0
- package/neural-interface/lib/codex-capabilities.js +2 -1
- package/neural-interface/lib/codex-compat-rpc.js +8 -2
- package/neural-interface/lib/codex-mcp-approvals.js +5 -1
- package/neural-interface/lib/codex-panel-auth.js +62 -0
- package/neural-interface/lib/codex-session-settings.js +1 -1
- package/neural-interface/lib/codex-settings-schema.js +120 -0
- package/neural-interface/lib/desktop/config.js +3 -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-bash/bzip2.js +339 -0
- package/neural-interface/lib/git-bash/claude-settings.js +73 -0
- package/neural-interface/lib/git-bash/extract-worker.js +34 -0
- package/neural-interface/lib/git-bash/install.js +529 -0
- package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
- package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
- package/neural-interface/lib/git-bash/manager.js +268 -0
- package/neural-interface/lib/git-bash/pins.js +206 -0
- package/neural-interface/lib/git-bash/pins.json +34 -0
- package/neural-interface/lib/git-bash/resolver.js +194 -0
- package/neural-interface/lib/git-diff-api.js +103 -0
- package/neural-interface/lib/git-read.js +234 -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/install-details.js +145 -0
- package/neural-interface/lib/machine-names.js +88 -0
- package/neural-interface/lib/mcp-installer.js +14 -2
- package/neural-interface/lib/memory-api.js +31 -7
- package/neural-interface/lib/native-loop-providers.js +26 -8
- package/neural-interface/lib/native-loop-runtime.js +9 -4
- package/neural-interface/lib/os-path.js +319 -0
- package/neural-interface/lib/plugin-installer.js +4 -2
- package/neural-interface/lib/project-storage.js +5 -7
- package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
- package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
- package/neural-interface/lib/pty-host/core.js +2 -1
- package/neural-interface/lib/pty-host/manager.js +27 -2
- package/neural-interface/lib/remote-policy.js +20 -11
- package/neural-interface/lib/request-gate.js +589 -0
- package/neural-interface/lib/session-title-generator.js +4 -3
- package/neural-interface/lib/system-about.js +340 -0
- package/neural-interface/lib/system-activity.js +66 -0
- package/neural-interface/lib/terminal-env.js +11 -3
- package/neural-interface/lib/terminal-path.js +55 -0
- package/neural-interface/lib/ui-blob-api.js +108 -0
- package/neural-interface/lib/ui-blob-store.js +68 -0
- package/neural-interface/lib/virtual-socket.js +12 -1
- package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
- package/neural-interface/lib/whatsapp/bridge.js +78 -34
- package/neural-interface/lib/whatsapp/commands.js +13 -14
- package/neural-interface/lib/whatsapp/config.js +134 -20
- package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
- package/neural-interface/lib/whatsapp/host-core.js +168 -35
- package/neural-interface/lib/whatsapp/inbound.js +5 -3
- package/neural-interface/lib/whatsapp/local-file.js +524 -0
- package/neural-interface/lib/whatsapp/manager.js +92 -32
- package/neural-interface/lib/whatsapp/paths.js +11 -1
- package/neural-interface/lib/whatsapp/protocol.js +65 -5
- package/neural-interface/lib/whatsapp/redact.js +63 -0
- package/neural-interface/lib/whatsapp/service.js +343 -91
- package/neural-interface/lib/whatsapp/transcription.js +463 -0
- package/neural-interface/package-lock.json +701 -2
- package/neural-interface/package.json +13 -2
- package/neural-interface/public/index.html +2 -0
- package/neural-interface/public/index2d.html +2 -0
- package/neural-interface/public/install-app.html +9 -6
- package/neural-interface/public/offline.html +19 -3
- package/neural-interface/public/onboarding.html +107 -2
- package/neural-interface/public/shared/account-i18n.js +7 -0
- package/neural-interface/public/shared/account-menu.js +539 -0
- package/neural-interface/public/shared/account-usage.js +19 -0
- package/neural-interface/public/shared/account-view.js +335 -0
- package/neural-interface/public/shared/agent-runtime-options.js +50 -14
- package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
- package/neural-interface/public/shared/assistant/asst-api.js +32 -2
- package/neural-interface/public/shared/assistant/asst-automation.js +286 -0
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +430 -81
- package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
- package/neural-interface/public/shared/assistant/asst-composer.js +11 -1
- package/neural-interface/public/shared/assistant/asst-mascot.js +140 -61
- package/neural-interface/public/shared/assistant/asst-menu.js +27 -11
- package/neural-interface/public/shared/assistant/asst-panel.js +198 -42
- package/neural-interface/public/shared/assistant/asst-render.js +197 -23
- package/neural-interface/public/shared/assistant/asst-route-models.js +108 -0
- package/neural-interface/public/shared/assistant/asst-route.js +95 -343
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +534 -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-slash.js +1 -1
- package/neural-interface/public/shared/assistant/asst-state.js +186 -2
- package/neural-interface/public/shared/assistant/asst-styles.js +247 -11
- package/neural-interface/public/shared/blob-store.js +111 -22
- package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
- package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
- package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +80 -118
- package/neural-interface/public/shared/cdx/cdx-render.js +25 -89
- package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +164 -107
- package/neural-interface/public/shared/cdx/cdx-tabs.js +348 -668
- package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
- package/neural-interface/public/shared/claude-account-view.js +3 -0
- package/neural-interface/public/shared/cli-install-view.js +392 -0
- package/neural-interface/public/shared/cli-status.js +282 -22
- package/neural-interface/public/shared/constants.js +6 -1
- package/neural-interface/public/shared/cp/cp-events.js +2 -1
- package/neural-interface/public/shared/cp/cp-rehydrate.js +2 -1
- package/neural-interface/public/shared/cp/cp-styles.js +61 -0
- package/neural-interface/public/shared/desktop-bridge.js +39 -0
- package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
- package/neural-interface/public/shared/desktop-titlebar.js +153 -0
- package/neural-interface/public/shared/desktop-update-view.js +462 -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/git-bash-view.js +145 -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 +161 -165
- package/neural-interface/public/shared/i18n.js +19 -1
- package/neural-interface/public/shared/install-details-view.js +86 -0
- 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 +131 -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 +22 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +158 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +3 -2
- package/neural-interface/public/shared/pwa-install-page.js +32 -2
- package/neural-interface/public/shared/pwa-install.css +3 -3
- package/neural-interface/public/shared/pwa-install.js +100 -1
- package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
- package/neural-interface/public/shared/settings/settings-about.js +254 -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 +33 -4
- package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
- package/neural-interface/public/shared/showcase-privacy.js +489 -0
- package/neural-interface/public/shared/start-bridge.js +238 -19
- package/neural-interface/public/shared/storage.js +30 -3
- package/neural-interface/public/shared/styles.css +1710 -770
- package/neural-interface/public/shared/synabun-mascot.js +68 -58
- package/neural-interface/public/shared/tunnel-view.js +167 -0
- package/neural-interface/public/shared/ui-claude-panel.js +369 -229
- package/neural-interface/public/shared/ui-cli-banner.js +355 -0
- 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-git-bash.js +124 -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 +10 -1
- package/neural-interface/public/shared/ui-keybinds.js +4 -0
- package/neural-interface/public/shared/ui-loading.js +187 -19
- package/neural-interface/public/shared/ui-menubar.js +25 -33
- 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 +48 -22
- package/neural-interface/public/shared/ui-provider-auth.js +104 -0
- package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
- package/neural-interface/public/shared/ui-sessions.js +9 -3
- package/neural-interface/public/shared/ui-settings.js +509 -490
- package/neural-interface/public/shared/ui-sidepanel-runtime.js +2 -1
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
- package/neural-interface/public/shared/ui-sync.js +22 -0
- package/neural-interface/public/shared/ui-terminal.js +40 -5
- package/neural-interface/public/shared/ui-tool-updates.js +5 -0
- 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-update.js +594 -5
- package/neural-interface/public/shared/ui-whatsapp.js +21 -6
- 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/whatsapp/wa-view.js +51 -8
- package/neural-interface/public/shared/worker-end.js +253 -0
- package/neural-interface/public/sw.js +2 -2
- package/neural-interface/public/variant/2d/main.js +10 -1
- package/neural-interface/public/variant/3d/main.js +10 -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/vendor-codemirror-entry.js +121 -0
- package/neural-interface/server.js +2085 -1629
- package/neural-interface/templates/rulesets/README.md +1 -1
- package/package.json +5 -8
- package/setup.js +53 -15
- package/benchmarks/browser-live-results.json +0 -79
- package/benchmarks/browser-v2-results.json +0 -258
- 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/skills/synabun/synabunicon.png +0 -0
|
@@ -8,21 +8,24 @@
|
|
|
8
8
|
// rehydration, per-turn auto-recall, dispatch mailbox delivery, and the memory
|
|
9
9
|
// obligation nudges. All server dependencies are injected.
|
|
10
10
|
|
|
11
|
-
import { randomUUID } from 'node:crypto';
|
|
11
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
12
12
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, 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';
|
|
16
16
|
import { createOpenCodeBrain } from './assistant-brains/opencode.js';
|
|
17
17
|
import { buildAssistantPersona, formatMailbox, reconcileRouteItems } from './assistant-persona.js';
|
|
18
|
-
import {
|
|
18
|
+
import { normalizeTestDensity, testDensityStamp } from './assistant-test-density.js';
|
|
19
|
+
import { codexUsageCostUsd, floorCents } from './assistant-budget.js';
|
|
20
|
+
import { MIN_SPEND_USD } from './assistant-budget-split.js';
|
|
19
21
|
import { CLAUDE_ESTIMATE_SOURCE, createClaudeMeter, createCodexMeter, createOpenCodeMeter, totalTokens, withTotal, zeroTokens } from './assistant-usage.js';
|
|
20
22
|
import { claudeTokensCostUsd } from './assistant-pricing.js';
|
|
21
23
|
import { normalizeRouteMode } from './assistant-config.js';
|
|
22
24
|
import { TASK_CLASS_META } from './assistant-router.js';
|
|
25
|
+
import { chooseUsageAccount } from './account-usage.js';
|
|
23
26
|
import { defaultIsHidden, enabledStandIn, findModel, providerAllHidden, selectSheetModels } from './assistant-catalog.js';
|
|
24
27
|
import { normalizeEffort } from './effort-levels.js';
|
|
25
|
-
import { claudeRouteAvailability, createRouteGate, mcpStatusRouteAvailability, ROUTE_GATE_LOOP_MESSAGE } from './assistant-route-gate.js';
|
|
28
|
+
import { claudeRouteAvailability, createRouteGate, isReplyTool, mcpStatusRouteAvailability, ROUTE_GATE_LOOP_MESSAGE } from './assistant-route-gate.js';
|
|
26
29
|
import { claudeReadOnlyPermission, planDenyMessage, planToolDecision, readOnlyDenyMessage, readOnlyToolDecision } from './assistant-plan-permissions.js';
|
|
27
30
|
import { codexRouteGateBootstrap } from './assistant-route-gate-codex.js';
|
|
28
31
|
import { proposedPlanText } from './assistant-envelope.js';
|
|
@@ -36,17 +39,49 @@ export const DEFAULT_SESSION_TITLE = 'SynaBun';
|
|
|
36
39
|
export function isDefaultSessionTitle(title) { return !title || /^(?:SynaBun|Assistant(?: \d+)?)$/.test(String(title).trim()); }
|
|
37
40
|
const MAILBOX_REASONS = new Set(['turn_completed', 'needs_input', 'permission_request', 'failed', 'interrupted', 'stalled', 'budget_warning', 'budget_exceeded', 'budget_unpriced', 'queue_started', 'user_intervened', 'stopped']);
|
|
38
41
|
// A stopped run reaches the brain when the user stopped it or it ran out of money.
|
|
39
|
-
const MAILBOX_STOP_REASONS = new Set(['user', 'budget_cap', 'session_budget_cap', 'budget_unpriced']);
|
|
42
|
+
const MAILBOX_STOP_REASONS = new Set(['user', 'budget_cap', 'session_budget_cap', 'budget_unpriced', 'budget_share']);
|
|
43
|
+
/** The `[SynaBun Budget]` line: a change of the session budget reaches the brain as one line before its next turn. */
|
|
44
|
+
export const BUDGET_HEADER = '[SynaBun Budget]';
|
|
45
|
+
/**
|
|
46
|
+
* What the brain is told when the budget changed after its prompt was built (setting: { unlimited,
|
|
47
|
+
* amountUsd }; money: the session's budget view, for what is spent and left). The key changes only
|
|
48
|
+
* with the setting, so spending never repeats the line.
|
|
49
|
+
*/
|
|
50
|
+
export function budgetStamp(setting, money = null) {
|
|
51
|
+
if (!setting) return { key: null, text: '' };
|
|
52
|
+
const usdText = (value) => `$${(Number(value) || 0).toFixed(2)}`;
|
|
53
|
+
if (setting.unlimited === true) {
|
|
54
|
+
return { key: 'unlimited', text: `${BUDGET_HEADER} The user switched this session to No limit: there is no budget now, no provider shares and no stops for spend. Do not pass budget_usd; tasks route by the saved routes as before. Spend is still metered (agent_usage).` };
|
|
55
|
+
}
|
|
56
|
+
const amount = Number(setting.amountUsd) || 0;
|
|
57
|
+
const spent = money && Number.isFinite(Number(money.totalUsd)) ? Number(money.totalUsd) : null;
|
|
58
|
+
const status = spent === null ? '' : ` ${usdText(spent)} spent, ${usdText(Math.max(0, amount - spent))} left.`;
|
|
59
|
+
return {
|
|
60
|
+
key: `limited:${amount.toFixed(2)}`,
|
|
61
|
+
text: `${BUDGET_HEADER} The user set this session's budget to ${usdText(amount)} (your own turns plus every run, at list price; one warning at 80%).${status} SynaBun divides what is left across the providers: each provider's runs draw from its share and your turns from your provider's share (agent_usage shows the split). When a share is used up, agent_route moves a task to a provider with money left, so saved routes yield to the budget. budget_usd is optional: a ceiling for one run inside its share.`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
40
64
|
const usd = (value) => `$${(Number(value) || 0).toFixed(2)}`;
|
|
41
65
|
const TRANSCRIPT_EVENT_TYPES = new Set(['assistant', 'user', 'result', 'system']);
|
|
42
66
|
// Below the autonomous level a remote session's brain asks before any tool the
|
|
43
67
|
// read-only classifier would refuse. These stay unasked: questions and plans (they reach
|
|
44
68
|
// the user anyway), in-session bookkeeping, subagent launches (their own calls
|
|
45
|
-
// are asked), memory writes,
|
|
46
|
-
|
|
69
|
+
// are asked), memory writes, agent_dispatch (its route card is the approval), and
|
|
70
|
+
// whatsapp_send_file: delivering a file the user asked for is a reply, like the final text
|
|
71
|
+
// (any host's name for it: isReplyTool).
|
|
72
|
+
export const REMOTE_ASK_EXEMPT = new Set([
|
|
47
73
|
'AskUserQuestion', 'ExitPlanMode', 'EnterPlanMode', 'TodoWrite', 'TodoRead', 'ToolSearch', 'Task', 'Agent',
|
|
48
74
|
'mcp__SynaBun__remember', 'mcp__SynaBun__reflect', 'mcp__SynaBun__category', 'mcp__SynaBun__agent_dispatch',
|
|
75
|
+
'mcp__SynaBun__whatsapp_send_file',
|
|
49
76
|
]);
|
|
77
|
+
/**
|
|
78
|
+
* The credential of one brain launch of a WhatsApp conversation (whatsapp_send_file): it reaches
|
|
79
|
+
* that brain's SynaBun MCP server only (Claude: an MCP header; Codex and OpenCode: the MCP child's
|
|
80
|
+
* environment), which sends it on POST /api/assistant/whatsapp/send-file and nowhere else. Random,
|
|
81
|
+
* in memory only (never on the record, in a log, the persona or a transcript), gone with the brain.
|
|
82
|
+
*/
|
|
83
|
+
export const BRAIN_CREDENTIAL_RE = /^sbb_[A-Za-z0-9_-]{43}$/;
|
|
84
|
+
export function mintBrainCredential() { return `sbb_${randomBytes(32).toString('base64url')}`; }
|
|
50
85
|
// An OpenCode brain of a remote session reaches the web through these on its own
|
|
51
86
|
// (no SynaBun browser, no approval): the gate plugin refuses them.
|
|
52
87
|
const REMOTE_OPENCODE_WEB_TOOLS = new Set(['webfetch', 'websearch', 'codesearch']);
|
|
@@ -155,6 +190,9 @@ export function mergeHooks(a, b) {
|
|
|
155
190
|
export function createAssistantRuntime({
|
|
156
191
|
dispatcher = null,
|
|
157
192
|
claudeAccounts = null,
|
|
193
|
+
// lib/codex-accounts.js: a Codex brain whose account cannot run is refused before it starts.
|
|
194
|
+
codexAccounts = null,
|
|
195
|
+
accountUsage = null,
|
|
158
196
|
ClaudeSession = null,
|
|
159
197
|
sdkVersion = null,
|
|
160
198
|
handleCodexSkinWebSocket = null,
|
|
@@ -183,8 +221,11 @@ export function createAssistantRuntime({
|
|
|
183
221
|
codexBin = () => 'codex',
|
|
184
222
|
codexGateBootstrap = codexRouteGateBootstrap,
|
|
185
223
|
// Codex list prices (assistant-budget.js createModelPricing). When given, a
|
|
186
|
-
// brain whose spend cannot be priced is refused (
|
|
224
|
+
// brain whose spend cannot be priced is refused under a budget (it fails closed).
|
|
187
225
|
pricing = null,
|
|
226
|
+
// The session budget (assistant-budget-planner.js): the setting, the provider shares, the
|
|
227
|
+
// checkpoints. Defaults to the dispatcher's own planner.
|
|
228
|
+
budget: budgetPlanner = null,
|
|
188
229
|
usage = null,
|
|
189
230
|
// The meters of assistant-usage.js, by provider (tests swap one in).
|
|
190
231
|
meterFactories = null,
|
|
@@ -192,6 +233,11 @@ export function createAssistantRuntime({
|
|
|
192
233
|
// Clarification (assistant-clarify.js): human prompts, the brain's own
|
|
193
234
|
// question cards and the clarify cards' answers reach it from here.
|
|
194
235
|
clarifier = null,
|
|
236
|
+
// The user-context engine (assistant-user-context.js): what the Assistant knows about the
|
|
237
|
+
// user, nudges from recent conversations, the learning loop and the per-turn trace.
|
|
238
|
+
userContext = null,
|
|
239
|
+
// lib/greeting/ engine (or null): the note about the greeting a conversation opened with, and when the person was last here.
|
|
240
|
+
greeting = null,
|
|
195
241
|
// Remote sessions (lib/remote-policy.js): the level registry shared with the
|
|
196
242
|
// dispatcher and the WhatsApp bridge, and the registered project paths a
|
|
197
243
|
// remote session's workers must run in.
|
|
@@ -214,7 +260,6 @@ export function createAssistantRuntime({
|
|
|
214
260
|
retention: 200,
|
|
215
261
|
defaultBrain: config.defaultBrain || {},
|
|
216
262
|
personaExtra: config.persona?.extra || '',
|
|
217
|
-
maxBudgetUsd: Number(config.brains?.claude?.maxBudgetUsd) || 10,
|
|
218
263
|
// A brain's approval or question card waits this long, then it is declined
|
|
219
264
|
// (0 = wait forever). A pending card holds the turn and the mailbox.
|
|
220
265
|
approvalTimeoutMs: Math.max(0, Number(config.approvalTimeoutMinutes ?? 10) * 60_000) || 0,
|
|
@@ -295,7 +340,9 @@ export function createAssistantRuntime({
|
|
|
295
340
|
return {
|
|
296
341
|
record, brain: null, brainProvider: null, brainMeter: null, brainMeterProvider: null, turnTaskId: null, turnTasks: [], sockets: new Set(), buffer: [], running: false, pendingControls: new Map(), controlTimers: new Map(),
|
|
297
342
|
mailbox: [], mailboxTimer: null, obligations: new Set(), rememberedThisTurn: false, disposeTimer: null, personaText: '',
|
|
298
|
-
|
|
343
|
+
// The last human prompt and where it came from ('ui', 'whatsapp', 'api'): a route continuation
|
|
344
|
+
// answers there (lastPromptOrigin; the transcript's after a restart).
|
|
345
|
+
lastPrompt: '', lastPromptOrigin: null, mailboxTurn: false, nudgedRuns: new Set(),
|
|
299
346
|
// The provider's usage limit as last reported (limitNotice), or null: one state, never a list.
|
|
300
347
|
limit: null,
|
|
301
348
|
// Human prompts so far: one request cycle each (assistant-clarify.js briefs).
|
|
@@ -303,6 +350,8 @@ export function createAssistantRuntime({
|
|
|
303
350
|
// Claude brains: the turns sent, keyed by their final text, for the in-process recall hook.
|
|
304
351
|
turnMeta: new Map(),
|
|
305
352
|
desktopGrant: null,
|
|
353
|
+
// A WhatsApp conversation's brain credential (mintBrainCredential): this launch only, never persisted.
|
|
354
|
+
brainCredential: null,
|
|
306
355
|
// Route gate (assistant-route-gate.js): one per session while routing is wired.
|
|
307
356
|
// gatePluginAt: when the OpenCode serve loaded the gate plugin (until then, reactive).
|
|
308
357
|
// gateNudge: { reason, gen } while a reactive refusal waits for its turn to end.
|
|
@@ -340,6 +389,10 @@ export function createAssistantRuntime({
|
|
|
340
389
|
// Closing (endSessionWork): the close under way (a promise), and the generation it started.
|
|
341
390
|
gen: 0, work: new Map(), workActive: null, workTimer: null, workPumpQueued: false, routeGens: new Map(),
|
|
342
391
|
closing: null, closedGen: 0,
|
|
392
|
+
// User context (assistant-user-context.js): what the live brain was built for (a layer
|
|
393
|
+
// switched since restarts an idle brain), the human turns sent and not ended yet (oldest
|
|
394
|
+
// first), and the last reply (context for what the next message teaches).
|
|
395
|
+
brainUserContext: null, ucTurns: [], lastReply: '',
|
|
343
396
|
};
|
|
344
397
|
}
|
|
345
398
|
// ── remote sessions (lib/remote-policy.js) ─────────────────────────────────
|
|
@@ -727,6 +780,12 @@ export function createAssistantRuntime({
|
|
|
727
780
|
*/
|
|
728
781
|
function brainNeedsRestart(live) {
|
|
729
782
|
if (!live?.brain) return false;
|
|
783
|
+
// Its persona was built with (or without) a user-context layer that was switched since.
|
|
784
|
+
if (userContext && live.brainUserContext !== null) {
|
|
785
|
+
let key = null;
|
|
786
|
+
try { key = userContext.brainKey(live.record); } catch { key = null; }
|
|
787
|
+
if (key !== null && key !== live.brainUserContext) return true;
|
|
788
|
+
}
|
|
730
789
|
const policy = remotePolicyFor(live);
|
|
731
790
|
// A Codex / OpenCode brain built before the session became remote is not read-only yet.
|
|
732
791
|
if (live.brainProvider !== 'claude-code') return !!policy && live.brainRemoteReadOnly !== true;
|
|
@@ -747,6 +806,8 @@ export function createAssistantRuntime({
|
|
|
747
806
|
const effectiveMode = policy ? clampRouteMode(policy, mode || defaultRouteMode(), { now: now() }) : (mode || defaultRouteMode());
|
|
748
807
|
return { mode, effectiveMode, defaultMode: defaultRouteMode(), askBelow };
|
|
749
808
|
}
|
|
809
|
+
/** The session's Test density: its own value, else the default level (assistant-test-density.js). */
|
|
810
|
+
function testDensityOf(record) { return normalizeTestDensity(record?.testDensity); }
|
|
750
811
|
function computerSupported() { try { return !!desktop?.isSupported?.(); } catch { return false; } }
|
|
751
812
|
/**
|
|
752
813
|
* Effective computer toggle: explicit per-session value, else the service default (ON after setup).
|
|
@@ -825,6 +886,8 @@ export function createAssistantRuntime({
|
|
|
825
886
|
function publicRecord(record, { withLive = true, live = null, persisted = false } = {}) {
|
|
826
887
|
const out = { ...record };
|
|
827
888
|
if (!persisted) delete out.usageState;
|
|
889
|
+
// The user-context state (frozen digest, delivery ledgers) is the server's own.
|
|
890
|
+
if (!persisted) delete out.userContext;
|
|
828
891
|
if (withLive && live) {
|
|
829
892
|
out.running = live.running;
|
|
830
893
|
out.attached = live.sockets.size;
|
|
@@ -840,7 +903,10 @@ export function createAssistantRuntime({
|
|
|
840
903
|
// A remote session's computer use is not a per-conversation choice: no explicit value, and why it is what it is.
|
|
841
904
|
out.computerUseExplicit = policy ? null : (record.computerUse === true || record.computerUse === false ? record.computerUse : null);
|
|
842
905
|
out.computerRemote = computer ? { state: computer.state, reason: computer.reason, level: computer.level, approved: live.computerTurn?.approved === true } : null;
|
|
843
|
-
out.
|
|
906
|
+
out.testDensity = testDensityOf(record);
|
|
907
|
+
out.testDensityExplicit = normalizeTestDensity(record.testDensity, null);
|
|
908
|
+
// `rename`: this server keeps a rename on the record (userRenamed), so a panel writes it here.
|
|
909
|
+
out.features = { routing: !!router, computer: computerSupported(), testDensity: true, rename: true };
|
|
844
910
|
out.budget = sessionMoney(record.id);
|
|
845
911
|
out.remoteLevel = policy ? effectiveLevel(policy, { untrusted: live.turnUntrusted === true, now: now() }) : null;
|
|
846
912
|
}
|
|
@@ -1119,7 +1185,7 @@ export function createAssistantRuntime({
|
|
|
1119
1185
|
fidelity: row.fidelity || settled?.fidelity, reason: row.reason ?? settled?.reason ?? null, source: row.source || settled?.source,
|
|
1120
1186
|
});
|
|
1121
1187
|
if (estimate && costUsd > 0) {
|
|
1122
|
-
live
|
|
1188
|
+
addBrainCost(live, costUsd, 'claude-code');
|
|
1123
1189
|
try { dispatcher?.checkSession?.(id); } catch {}
|
|
1124
1190
|
sendCost(live);
|
|
1125
1191
|
}
|
|
@@ -1177,16 +1243,18 @@ export function createAssistantRuntime({
|
|
|
1177
1243
|
// session restarted (resume, brain switch), so it counts in full.
|
|
1178
1244
|
const last = Number(live.record.lastReportedCost) || 0;
|
|
1179
1245
|
const delta = cost >= last ? cost - last : cost;
|
|
1180
|
-
live.
|
|
1246
|
+
addBrainCost(live, delta, live.brainProvider || live.record.brain?.provider || null);
|
|
1181
1247
|
live.record.lastReportedCost = cost;
|
|
1182
1248
|
live.record.costBasis = event.cost_basis || 'reported';
|
|
1183
|
-
// The brain's spend counts toward the session
|
|
1249
|
+
// The brain's spend counts toward the session budget: warn, or stop the session's runs.
|
|
1184
1250
|
try { dispatcher?.checkSession?.(id); } catch {}
|
|
1185
1251
|
sendCost(live);
|
|
1186
1252
|
}
|
|
1187
1253
|
// The Claude CLI's own cap stopped the brain: its process cannot run another turn.
|
|
1188
1254
|
if (event.subtype === 'error_max_budget_usd' && live.brainProvider === 'claude-code') live.brainCapHit = true;
|
|
1189
1255
|
if (live.planTurn) live.planText = planTurnText(event);
|
|
1256
|
+
// The reply the user's next message answers (context for the learning loop; a subagent's result is not it).
|
|
1257
|
+
if (typeof event.result === 'string' && event.result.trim() && !event.parent_tool_use_id) live.lastReply = clip(event.result, 600);
|
|
1190
1258
|
touch(live);
|
|
1191
1259
|
}
|
|
1192
1260
|
if (TRANSCRIPT_EVENT_TYPES.has(event.type) && !(event.type === 'system' && event.subtype !== 'init')) appendTranscript(id, { at: iso(now()), packet });
|
|
@@ -1292,8 +1360,32 @@ export function createAssistantRuntime({
|
|
|
1292
1360
|
}
|
|
1293
1361
|
}
|
|
1294
1362
|
sendToSockets(live, packet);
|
|
1363
|
+
// After the packet: the turn's trace moves under the reply the panel has just closed.
|
|
1364
|
+
if ((packet.type === 'done' || packet.type === 'aborted' || packet.type === 'error') && !live.running) finishUserContextTurn(live, packet.type);
|
|
1295
1365
|
if (plan) offerPlanCard(live, plan, planGen);
|
|
1296
1366
|
}
|
|
1367
|
+
/** The oldest human turn still open ended: its trace moves under the reply and learning starts, off the reply path. */
|
|
1368
|
+
function finishUserContextTurn(live, outcome) {
|
|
1369
|
+
const handle = live.ucTurns.shift();
|
|
1370
|
+
if (!handle || !userContext) return;
|
|
1371
|
+
try { userContext.endTurn(live.record, handle, { outcome }); } catch (error) { log('assistant:user-context', error?.message || String(error)); }
|
|
1372
|
+
}
|
|
1373
|
+
/** One turn's user-context trace: to the panel and into the journal, a full snapshot each time (the panel upserts by traceId). */
|
|
1374
|
+
function emitAutomation(live, event) {
|
|
1375
|
+
if (!event || sessions.get(live.record.id) !== live) return;
|
|
1376
|
+
const packet = { type: 'event', event };
|
|
1377
|
+
sendToSockets(live, packet);
|
|
1378
|
+
appendTranscript(live.record.id, { at: iso(now()), packet });
|
|
1379
|
+
}
|
|
1380
|
+
/** The cards of a session left unanswered right now: the runtime's plan card, route cards, clarify cards. */
|
|
1381
|
+
function openCards(live) {
|
|
1382
|
+
const out = [];
|
|
1383
|
+
const title = (card) => clip(card?.request?.title || card?.request?.summary || card?.request?.route?.summary || card?.request?.questions?.[0]?.question || '', 80);
|
|
1384
|
+
if (live.planCard?.requestId) out.push({ id: String(live.planCard.requestId), kind: 'plan', title: clip(String(live.planCard.plan || '').split('\n')[0], 80) });
|
|
1385
|
+
try { for (const card of router?.pendingCards?.(live.record.id) || []) if (card?.request_id) out.push({ id: String(card.request_id), kind: 'route', title: title(card) }); } catch {}
|
|
1386
|
+
try { for (const card of clarifier?.pendingCards?.(live.record.id) || []) if (card?.request_id) out.push({ id: String(card.request_id), kind: 'question', title: title(card) }); } catch {}
|
|
1387
|
+
return out;
|
|
1388
|
+
}
|
|
1297
1389
|
/**
|
|
1298
1390
|
* Codex and OpenCode end a plan turn with the plan instead of asking for
|
|
1299
1391
|
* approval the way Claude's ExitPlanMode does: the runtime asks, with the same
|
|
@@ -1631,13 +1723,54 @@ export function createAssistantRuntime({
|
|
|
1631
1723
|
}
|
|
1632
1724
|
/** The brain's own spend in a session (the dispatcher adds it to the session total). */
|
|
1633
1725
|
function brainSpend(sessionId) { return Number(sessions.get(sessionId)?.record.costUsd) || 0; }
|
|
1634
|
-
/**
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1726
|
+
/**
|
|
1727
|
+
* Book the brain's spend: its session total and the provider it ran on (the budget counts the
|
|
1728
|
+
* brain's turns in that provider's share).
|
|
1729
|
+
*/
|
|
1730
|
+
function addBrainCost(live, usdAmount, provider) {
|
|
1731
|
+
const added = Number(usdAmount);
|
|
1732
|
+
if (!Number.isFinite(added) || added <= 0) return;
|
|
1733
|
+
live.record.costUsd = Number(((Number(live.record.costUsd) || 0) + added).toFixed(4));
|
|
1734
|
+
const key = BRAIN_PROVIDERS.has(provider) ? provider : (live.record.brain?.provider || 'claude-code');
|
|
1735
|
+
const rows = live.record.costByProvider && typeof live.record.costByProvider === 'object' ? live.record.costByProvider : {};
|
|
1736
|
+
rows[key] = Number(((Number(rows[key]) || 0) + added).toFixed(8));
|
|
1737
|
+
live.record.costByProvider = rows;
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* The brain's spend by the provider it ran on. A session recorded before this was kept books what
|
|
1741
|
+
* no provider holds on the brain's provider.
|
|
1742
|
+
*/
|
|
1743
|
+
function brainSpendByProvider(sessionId) {
|
|
1744
|
+
const record = sessions.get(sessionId)?.record;
|
|
1745
|
+
if (!record) return {};
|
|
1746
|
+
const rows = record.costByProvider && typeof record.costByProvider === 'object' ? { ...record.costByProvider } : {};
|
|
1747
|
+
const known = Object.values(rows).reduce((sum, value) => sum + (Number(value) || 0), 0);
|
|
1748
|
+
const rest = (Number(record.costUsd) || 0) - known;
|
|
1749
|
+
if (rest > 0.00005) {
|
|
1750
|
+
const key = BRAIN_PROVIDERS.has(record.brain?.provider) ? record.brain.provider : 'claude-code';
|
|
1751
|
+
rows[key] = (Number(rows[key]) || 0) + rest;
|
|
1752
|
+
}
|
|
1753
|
+
return rows;
|
|
1754
|
+
}
|
|
1755
|
+
/** The session budget: the injected planner, else the dispatcher's. */
|
|
1756
|
+
function planner() { return budgetPlanner || dispatcher?.budget || null; }
|
|
1757
|
+
/** The budget setting now ({ unlimited, amountUsd, version }), or null when nothing keeps one. */
|
|
1758
|
+
function budgetSettingNow() {
|
|
1759
|
+
try { const value = planner()?.setting?.() || configStore?.budget?.() || null; return value ? { unlimited: value.unlimited === true, amountUsd: Number(value.amountUsd) || 0, version: Number(value.version) || 0 } : null; } catch { return null; }
|
|
1760
|
+
}
|
|
1761
|
+
/** The "[SynaBun Budget]" line when the setting changed since the brain last saw it (then marked seen), else ''. */
|
|
1762
|
+
function budgetLine(live) {
|
|
1763
|
+
try {
|
|
1764
|
+
const stamp = budgetStamp(budgetSettingNow(), sessionMoney(live.record.id));
|
|
1765
|
+
if (!stamp.key) return '';
|
|
1766
|
+
if (live.budgetStampKey === undefined) { live.budgetStampKey = stamp.key; return ''; }
|
|
1767
|
+
if (live.budgetStampKey === stamp.key) return '';
|
|
1768
|
+
live.budgetStampKey = stamp.key;
|
|
1769
|
+
return stamp.text;
|
|
1770
|
+
} catch { return ''; }
|
|
1638
1771
|
}
|
|
1639
1772
|
function sessionMoney(sessionId) {
|
|
1640
|
-
try { return dispatcher?.sessionBudget?.(sessionId) || null; } catch { return null; }
|
|
1773
|
+
try { return dispatcher?.sessionBudget?.(sessionId) || planner()?.view?.(sessionId) || null; } catch { return null; }
|
|
1641
1774
|
}
|
|
1642
1775
|
/**
|
|
1643
1776
|
* How the brain's spend is known: 'reported' (Claude; OpenCode with a list
|
|
@@ -1662,34 +1795,91 @@ export function createAssistantRuntime({
|
|
|
1662
1795
|
function sendCost(live) {
|
|
1663
1796
|
const budget = sessionMoney(live.record.id);
|
|
1664
1797
|
const basis = brainCostBasis(live);
|
|
1665
|
-
sendToSockets(live, { type: 'assistant:cost', sessionUsd: live.record.costUsd, dispatchUsd: dispatchSpend(live.record.id), budget,
|
|
1798
|
+
sendToSockets(live, { type: 'assistant:cost', sessionUsd: live.record.costUsd, dispatchUsd: dispatchSpend(live.record.id), budget, brainMetered: basis !== null, brainCostBasis: basis });
|
|
1666
1799
|
}
|
|
1667
1800
|
/**
|
|
1668
|
-
*
|
|
1669
|
-
*
|
|
1670
|
-
|
|
1801
|
+
* The session's budget as it is now, live: `assistant:budget` to the session's sockets and the sync
|
|
1802
|
+
* channel (every window), after a split or a change of the budget. `reason`: what made it.
|
|
1803
|
+
*/
|
|
1804
|
+
function sendBudget(sessionId, budget = null, { reason = null } = {}) {
|
|
1805
|
+
const live = sessions.get(String(sessionId || ''));
|
|
1806
|
+
if (!live) return;
|
|
1807
|
+
const view = budget || sessionMoney(live.record.id);
|
|
1808
|
+
if (!view) return;
|
|
1809
|
+
const packet = { type: 'assistant:budget', sessionId: live.record.id, reason: reason || null, budget: view };
|
|
1810
|
+
// A view, not an event (like usage): the open sockets only, never buffered (attach sends a fresh one).
|
|
1811
|
+
const raw = JSON.stringify(packet);
|
|
1812
|
+
for (const ws of [...live.sockets]) { if (ws.readyState === 1) { try { ws.send(raw); } catch {} } }
|
|
1813
|
+
try { broadcastSync({ ...packet, assistantSessionId: live.record.id }); } catch {}
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Why the brain may not start a turn, checked before every turn (a turn already running finishes):
|
|
1817
|
+
* an unpriced brain model, the session's budget spent, or no money left in the brain's provider
|
|
1818
|
+
* share (runTurn divides the session's unspent budget again first). Nothing under No limit.
|
|
1671
1819
|
*/
|
|
1672
1820
|
function brainBudgetBlock(live) {
|
|
1673
|
-
|
|
1821
|
+
const money = sessionMoney(live.record.id);
|
|
1822
|
+
if (!money || money.unlimited) return null;
|
|
1823
|
+
// Fail closed: a brain whose spend has no price could spend past the budget.
|
|
1674
1824
|
if (pricing && brainCostBasis(live) === null) {
|
|
1675
1825
|
const model = live.record.brain?.model || brainCapabilities(live.record)?.id || 'its default model';
|
|
1676
|
-
return { code: 'BRAIN_UNPRICED', message: `No price is known for the ${live.record.brain?.provider || ''} brain model ${model}, so the budget
|
|
1826
|
+
return { code: 'BRAIN_UNPRICED', message: `No price is known for the ${live.record.brain?.provider || ''} brain model ${model}, so the session budget cannot hold its spend. Pick a priced or free model for the brain, or switch the budget to No limit.` };
|
|
1677
1827
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1828
|
+
if (money.exceeded) {
|
|
1829
|
+
return { code: 'SESSION_BUDGET_EXCEEDED', message: `This session spent ${usd(money.totalUsd)} of its ${usd(money.amountUsd)} budget (brain ${usd(money.brainUsd)} + agents ${usd(money.dispatchUsd)}). Raise the budget or switch to No limit in ⋯ → Budget… to continue.` };
|
|
1830
|
+
}
|
|
1831
|
+
const provider = live.record.brain?.provider || null;
|
|
1832
|
+
const covered = planner()?.covers?.(live.record.id, { provider, forBrain: true });
|
|
1833
|
+
if (covered === false) {
|
|
1834
|
+
return { code: 'BRAIN_SHARE_EXHAUSTED', share: true, message: `The ${provider} share of this session's budget is used up, so the brain cannot take another turn (${usd(money.leftUsd)} of the ${usd(money.amountUsd)} budget left in other providers' shares). Raise the budget or switch to No limit in ⋯ → Budget… to continue.` };
|
|
1681
1835
|
}
|
|
1682
|
-
const cap = brainCapUsd();
|
|
1683
|
-
const spent = Number(live.record.costUsd) || 0;
|
|
1684
|
-
if (spent >= cap) return { code: 'BRAIN_BUDGET_EXCEEDED', message: `The brain spent ${usd(spent)} in this session, at its cap of ${usd(cap)}. Raise the brain cap in ⋯ → Budget… to continue.` };
|
|
1685
1836
|
return null;
|
|
1686
1837
|
}
|
|
1687
|
-
/**
|
|
1838
|
+
/**
|
|
1839
|
+
* The planner divided a session's budget again (budget planner `emit`): the panels hear it, and a
|
|
1840
|
+
* mailbox the brain's empty share held goes now.
|
|
1841
|
+
*/
|
|
1842
|
+
function onBudgetSplit(sessionId, budget = null, { reason = null } = {}) {
|
|
1843
|
+
sendBudget(sessionId, budget, { reason });
|
|
1844
|
+
const live = sessions.get(String(sessionId || ''));
|
|
1845
|
+
if (live && live.mailbox.length && !brainBudgetBlock(live)) { live.budgetHoldNotified = false; live.budgetShareTriedAt = 0; scheduleMailbox(live, 0); }
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Before a brain turn: when the brain's provider share is used up but the session has money left,
|
|
1849
|
+
* the session's unspent budget is divided again (its reserve comes back with the new split).
|
|
1850
|
+
*/
|
|
1851
|
+
async function coverBrainTurn(live) {
|
|
1852
|
+
const budget = planner();
|
|
1853
|
+
if (!budget?.ensureCovered || !budget.limited?.()) return true;
|
|
1854
|
+
try { return await budget.ensureCovered(live.record.id, { provider: live.record.brain?.provider || null, forBrain: true, reason: 'brain_turn' }); }
|
|
1855
|
+
catch { return false; }
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* The Claude brain CLI's own stop (--max-budget-usd): what its provider's share has left for it when
|
|
1859
|
+
* it starts, never more than the session has left. null under No limit (no CLI cap at all).
|
|
1860
|
+
*/
|
|
1861
|
+
function brainCliCap(live) {
|
|
1862
|
+
const money = sessionMoney(live.record.id);
|
|
1863
|
+
if (!money || money.unlimited) return null;
|
|
1864
|
+
let left = Number(money.leftUsd) || 0;
|
|
1865
|
+
try {
|
|
1866
|
+
const cov = planner()?.coverage?.(live.record.id);
|
|
1867
|
+
if (cov && Number.isFinite(Number(cov.brainLeftUsd))) left = Math.min(left, Number(cov.brainLeftUsd));
|
|
1868
|
+
} catch { /* the session's own left */ }
|
|
1869
|
+
return Math.max(MIN_SPEND_USD, floorCents(left) / 100);
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* The budget changed (Assistant → Budget): the planner divides each open session's budget again,
|
|
1873
|
+
* held mailbox items may go now, and every open panel refreshes its chip and split.
|
|
1874
|
+
*/
|
|
1688
1875
|
function onBudgetChanged() {
|
|
1876
|
+
try { planner()?.onBudgetChanged?.(); } catch {}
|
|
1689
1877
|
for (const live of sessions.values()) {
|
|
1690
1878
|
if (live.record.status === 'ended') continue;
|
|
1879
|
+
live.budgetShareTriedAt = 0;
|
|
1691
1880
|
try { dispatcher?.checkSession?.(live.record.id); } catch {}
|
|
1692
1881
|
sendCost(live);
|
|
1882
|
+
sendBudget(live.record.id, null, { reason: 'budget_changed' });
|
|
1693
1883
|
if (live.mailbox.length && !brainBudgetBlock(live)) { live.budgetHoldNotified = false; scheduleMailbox(live, 0); }
|
|
1694
1884
|
}
|
|
1695
1885
|
}
|
|
@@ -1703,6 +1893,12 @@ export function createAssistantRuntime({
|
|
|
1703
1893
|
try { if (catalogValue?.models) sheet = selectSheetModels(catalogValue, { brain, preferences: routingCfg.preferences || {}, max: 8 }); } catch {}
|
|
1704
1894
|
let setupState = null;
|
|
1705
1895
|
try { setupState = desktop?.setupState?.() || null; } catch {}
|
|
1896
|
+
// This prompt states the level: a "[SynaBun Tests]" line follows only a later change.
|
|
1897
|
+
live.testDensityStamp = testDensityStamp(testDensityOf(live.record));
|
|
1898
|
+
// …and the budget: a "[SynaBun Budget]" line follows only a later change of the setting.
|
|
1899
|
+
const budgetSetting = budgetSettingNow();
|
|
1900
|
+
const money = sessionMoney(live.record.id);
|
|
1901
|
+
live.budgetStampKey = budgetStamp(budgetSetting, money).key;
|
|
1706
1902
|
return buildAssistantPersona({
|
|
1707
1903
|
assistantSessionId: live.record.id,
|
|
1708
1904
|
brain,
|
|
@@ -1721,6 +1917,9 @@ export function createAssistantRuntime({
|
|
|
1721
1917
|
// A remote session: what remoteComputerUse says for it (a turn with untrusted content is asked either way).
|
|
1722
1918
|
computer: { available: computerSupported(), enabled: computerEffective(live.record), setupState, remote: computerDecision(live, { untrusted: false }) },
|
|
1723
1919
|
taskClasses: TASK_CLASS_META,
|
|
1920
|
+
testDensity: testDensityOf(live.record),
|
|
1921
|
+
// The session budget as the brain starts: the setting and, with a budget, the split so far.
|
|
1922
|
+
budget: budgetSetting ? { ...budgetSetting, view: money } : null,
|
|
1724
1923
|
clarify: !!clarifier,
|
|
1725
1924
|
channel: live.record.channel || live.record.remote?.channel || null,
|
|
1726
1925
|
});
|
|
@@ -1742,9 +1941,18 @@ export function createAssistantRuntime({
|
|
|
1742
1941
|
if (remote) live.record.brain = clampedBrain(live, live.record.brain);
|
|
1743
1942
|
const brain = live.record.brain;
|
|
1744
1943
|
const sink = { send: (packet) => onBrainPacket(live, packet) };
|
|
1745
|
-
const
|
|
1944
|
+
const basePersona = await personaFor(live);
|
|
1746
1945
|
if (stale()) return null;
|
|
1747
1946
|
if (live.brain) return live.brain;
|
|
1947
|
+
// What SynaBun has learned about the user rides after the persona, in the same system-level
|
|
1948
|
+
// slot (Claude systemPromptAppend, Codex developerInstructions, OpenCode agent prompt), frozen
|
|
1949
|
+
// per provider conversation so the prompt cache holds.
|
|
1950
|
+
let userSuffix = { text: '', key: null, nudges: false };
|
|
1951
|
+
if (userContext) {
|
|
1952
|
+
try { userSuffix = userContext.personaSuffix(live.record) || userSuffix; }
|
|
1953
|
+
catch (error) { log('assistant:user-context', error?.message || String(error)); }
|
|
1954
|
+
}
|
|
1955
|
+
const persona = userSuffix.text ? `${basePersona}\n\n${userSuffix.text}` : basePersona;
|
|
1748
1956
|
live.personaText = persona;
|
|
1749
1957
|
// Computer use: one unforgeable grant per brain (the MCP `computer` group is
|
|
1750
1958
|
// advertised only to callers presenting it). Revoked when the brain goes.
|
|
@@ -1762,11 +1970,15 @@ export function createAssistantRuntime({
|
|
|
1762
1970
|
live.computerGrantLive = false;
|
|
1763
1971
|
} catch (error) { log('assistant:desktop-grant-error', error?.message || String(error)); }
|
|
1764
1972
|
}
|
|
1973
|
+
// whatsapp_send_file: one credential per brain launch of a WhatsApp conversation (BRAIN_CREDENTIAL_RE),
|
|
1974
|
+
// for that brain's SynaBun MCP server alone. Every other session's brain gets none.
|
|
1975
|
+
live.brainCredential = (live.record.channel || live.record.remote?.channel) === 'whatsapp' ? mintBrainCredential() : null;
|
|
1976
|
+
const cliCap = brain.provider === 'claude-code' ? brainCliCap(live) : null;
|
|
1765
1977
|
const deps = {
|
|
1766
1978
|
ClaudeSession, sdkVersion, PACKAGE_ROOT, handleCodexSkinWebSocket, ensureIsolatedServe, stopIsolatedServe, setupOpencodeSidepanelConfig,
|
|
1767
1979
|
claudeAccountEnv: (accountId) => (claudeAccounts?.envFor?.(accountId) || {}),
|
|
1768
|
-
// The Claude CLI caps this process at what the
|
|
1769
|
-
maxBudgetUsd:
|
|
1980
|
+
// The Claude CLI caps this process at what the brain's provider share has left (none under No limit).
|
|
1981
|
+
maxBudgetUsd: cliCap ?? undefined,
|
|
1770
1982
|
// A Codex brain reports tokens: priced at the list price of its model (estimated).
|
|
1771
1983
|
priceCodexUsage: (usage) => {
|
|
1772
1984
|
const price = codexBrainPrice(live);
|
|
@@ -1782,6 +1994,7 @@ export function createAssistantRuntime({
|
|
|
1782
1994
|
// Also a resume into another session: what the meter counts as the main loop follows it.
|
|
1783
1995
|
onOpenCodeRoot: (sessionId) => { live.record.providerSessionId = sessionId; metered('meter-state', () => live.brainMeter?.setRoot?.(sessionId)); saveMeter(live); },
|
|
1784
1996
|
desktopGrant: live.desktopGrant || null,
|
|
1997
|
+
brainCredential: live.brainCredential || null,
|
|
1785
1998
|
// A remote session's level (lib/remote-policy.js): the Claude brain allows
|
|
1786
1999
|
// bypass only at autonomous. null for a desktop session.
|
|
1787
2000
|
remoteLevel: remote ? effectiveLevel(remote, { now: now() }) : null,
|
|
@@ -1790,6 +2003,8 @@ export function createAssistantRuntime({
|
|
|
1790
2003
|
// A Codex / OpenCode brain of a remote session is read-only for good: the
|
|
1791
2004
|
// read-only sandbox (Codex), the plan agent (OpenCode), no auto-accept.
|
|
1792
2005
|
remoteReadOnly: !!remote && remoteBrainLimited(brain.provider),
|
|
2006
|
+
// The engine keeps this conversation's thread record: pre-compact.mjs asks for no conversation memory.
|
|
2007
|
+
userContextThreads: brain.provider === 'claude-code' && userSuffix.nudges === true,
|
|
1793
2008
|
};
|
|
1794
2009
|
const gate = ensureGate(live);
|
|
1795
2010
|
// A remote session's hook / plugin refuses a call it could not check (fail closed).
|
|
@@ -1805,22 +2020,29 @@ export function createAssistantRuntime({
|
|
|
1805
2020
|
const memoryHooks = brain.provider === 'claude-code' && memory?.claudeHooks ? memory.claudeHooks({
|
|
1806
2021
|
session: live.record.id, project: live.record.project || 'global',
|
|
1807
2022
|
getGeneration: () => live.record.memoryGeneration || 1,
|
|
1808
|
-
bumpGeneration: () => { live.record.memoryGeneration = (live.record.memoryGeneration || 1) + 1; touch(live); },
|
|
2023
|
+
bumpGeneration: () => { live.record.memoryGeneration = (live.record.memoryGeneration || 1) + 1; try { userContext?.onCompaction(live.record); } catch {} touch(live); },
|
|
1809
2024
|
markRemembered: () => { live.rememberedThisTurn = true; live.obligations.clear(); },
|
|
1810
2025
|
hasPendingObligations: () => live.obligations.size > 0 && !live.rememberedThisTurn,
|
|
1811
2026
|
onRecall: (recalled) => { if (recalled?.results?.length || recalled?.alreadyPresent) sendToSockets(live, { type: 'event', event: { type: 'synabun.memories', memories: recalled.results || [], alreadyPresent: !!recalled.alreadyPresent } }); },
|
|
1812
2027
|
describeTurn: (text) => describeTurn(live, text),
|
|
2028
|
+
// User context: its categories stay out of Related Memories, and its per-turn block follows them.
|
|
2029
|
+
divert: () => userContext?.divertCategories() || null,
|
|
2030
|
+
turnContext: ({ recalled, meta }) => (userContext && meta?.uc ? userContext.turnBlock(live.record, meta.uc, recalled).text : ''),
|
|
1813
2031
|
}) : null;
|
|
1814
2032
|
// Claude: memory, the route gate, the browser policy (every session: plan mode and subagents too), a remote session's hook.
|
|
1815
2033
|
const claude = brain.provider === 'claude-code';
|
|
1816
2034
|
const hooks = mergeHooks(mergeHooks(mergeHooks(memoryHooks, claude && gate ? gateHooks(live) : null), claude ? browserHooks(live) : null), claude && (remote || live.record.remote) ? remoteHooks(live) : null);
|
|
1817
2035
|
live.brainProvider = brain.provider;
|
|
2036
|
+
// What the CLI cap was sized from: a later change of the budget restarts a Claude brain (runTurn).
|
|
2037
|
+
live.brainCliCap = cliCap;
|
|
2038
|
+
live.brainBudgetVersion = budgetSettingNow()?.version ?? null;
|
|
1818
2039
|
startMeter(live, brain.provider);
|
|
1819
2040
|
const created = factory({ session: live.record, sink, deps, persona, hooks });
|
|
1820
2041
|
live.brain = created;
|
|
1821
2042
|
live.brainSkipAllowed = remote ? deps.remoteLevel === 'autonomous' : null;
|
|
1822
2043
|
live.brainComputerMode = computerMode;
|
|
1823
2044
|
live.brainRemoteReadOnly = deps.remoteReadOnly === true;
|
|
2045
|
+
live.brainUserContext = userSuffix.key;
|
|
1824
2046
|
live.record.status = 'idle';
|
|
1825
2047
|
touch(live);
|
|
1826
2048
|
try { await created.start?.(); } catch (error) {
|
|
@@ -1845,6 +2067,8 @@ export function createAssistantRuntime({
|
|
|
1845
2067
|
if (identity.providerThreadId) live.record.providerThreadId = identity.providerThreadId;
|
|
1846
2068
|
} catch {}
|
|
1847
2069
|
live.brain = null;
|
|
2070
|
+
// Its credential dies with it: a file send presenting it is refused from here on.
|
|
2071
|
+
live.brainCredential = null;
|
|
1848
2072
|
if (live.brainProvider === 'codex') pollCodex(live, { force: true });
|
|
1849
2073
|
// No further turn will book them: a Claude brain's provisional tokens are booked as an estimate,
|
|
1850
2074
|
// any other brain's go (Codex was just polled, OpenCode settled at its turn's end).
|
|
@@ -1858,6 +2082,9 @@ export function createAssistantRuntime({
|
|
|
1858
2082
|
live.running = false;
|
|
1859
2083
|
closePlanCard(live, 'brain_disposed');
|
|
1860
2084
|
live.planTurn = false;
|
|
2085
|
+
live.brainUserContext = null;
|
|
2086
|
+
// A human turn the brain took with it never ends on its own: its trace and its learning close here.
|
|
2087
|
+
for (const handle of live.ucTurns.splice(0)) { try { userContext?.endTurn(live.record, handle, { outcome: 'aborted' }); } catch {} }
|
|
1861
2088
|
// A remote session's computer use ends with its brain: the turn's approval, its open request, the live grant.
|
|
1862
2089
|
endComputerTurn(live, reason === 'closed' ? 'session_closed' : 'brain_disposed');
|
|
1863
2090
|
settleNativeQuestions(live, 'brain_disposed');
|
|
@@ -1984,13 +2211,15 @@ export function createAssistantRuntime({
|
|
|
1984
2211
|
}
|
|
1985
2212
|
}
|
|
1986
2213
|
// `authority`: the WhatsApp bridge's capability; it alone makes a prompt count as the owner's own
|
|
1987
|
-
// plain message from the phone (fromPhone). `origin` below is audit text and
|
|
1988
|
-
//
|
|
2214
|
+
// plain message from the phone (fromPhone). `origin` below is audit text and where an answer goes
|
|
2215
|
+
// (a continuation's, to the chat its prompt came from); it grants nothing.
|
|
2216
|
+
// `origin`: who wrote a human prompt ('ui', 'whatsapp', 'api'), or for a route continuation
|
|
2217
|
+
// (`work.kind` 'continuation') who wrote the prompt it carries on; `untrusted`: its
|
|
1989
2218
|
// text carries content the owner did not write (a remote session caps it at ask).
|
|
1990
2219
|
// `offerPlan`: a system turn that revises a plan still ends with a plan card.
|
|
1991
2220
|
// `work`: the scheduler entry this system turn is; `request`: { id, reply } of a
|
|
1992
2221
|
// prompt a socket sent (refuse); `turnGen`: the generation runQuery entered in.
|
|
1993
|
-
async function runTurn(live, { text, images = [], cwd = null, model = null, effort = null, permissionMode = null, planMode = null, system = false, taskId = null, turnModel = null, turnEffort = null, gateOpen = false, gateCarry = false, origin = null, authority = null, untrusted = false, offerPlan = false, work = null, request = null, turnGen = live.gen, computerRoutes = null } = {}) {
|
|
2222
|
+
async function runTurn(live, { text, images = [], cwd = null, model = null, effort = null, permissionMode = null, planMode = null, system = false, taskId = null, turnModel = null, turnEffort = null, gateOpen = false, gateCarry = false, origin = null, authority = null, untrusted = false, offerPlan = false, work = null, request = null, turnGen = live.gen, computerRoutes = null, typed = null, greetingId = null } = {}) {
|
|
1994
2223
|
const prompt = String(text || '').trim();
|
|
1995
2224
|
if (!prompt && !(Array.isArray(images) && images.length)) return refuse(live, 'EMPTY_PROMPT', 'No prompt provided', { packet: { type: 'error', message: 'No prompt provided' }, request });
|
|
1996
2225
|
// The scheduler's turn goes once a Stop, the session closing or a newer
|
|
@@ -2012,13 +2241,26 @@ export function createAssistantRuntime({
|
|
|
2012
2241
|
try { await catalog.full(); } catch {}
|
|
2013
2242
|
if (cancelled()) return dropTurn(live, request);
|
|
2014
2243
|
}
|
|
2015
|
-
|
|
2244
|
+
let budgetBlock = brainBudgetBlock(live);
|
|
2245
|
+
// The brain's provider share is used up but the session has money left: it is divided again first.
|
|
2246
|
+
if (budgetBlock?.share && await coverBrainTurn(live)) {
|
|
2247
|
+
if (cancelled()) return dropTurn(live, request);
|
|
2248
|
+
budgetBlock = brainBudgetBlock(live);
|
|
2249
|
+
}
|
|
2016
2250
|
if (budgetBlock) return refuse(live, budgetBlock.code, budgetBlock.message, { request });
|
|
2251
|
+
// A Claude brain whose account cannot run (signed out, its key unreadable, removed) is refused with
|
|
2252
|
+
// why, before a process starts under it: the CLI would only answer "not logged in" mid-turn.
|
|
2253
|
+
if (!live.brain && ['claude-code', 'codex'].includes(live.record.brain?.provider)) {
|
|
2254
|
+
const unusable = live.record.brain.provider === 'codex' ? codexAccountUnusable(live.record.brain.accountId) : claudeAccountUnusable(live.record.brain.accountId);
|
|
2255
|
+
if (unusable) return refuse(live, 'ACCOUNT_SIGNED_OUT', `${unusable} Pick another account in the brain picker, or sign it in from its Account menu → Manage….`, { request });
|
|
2256
|
+
}
|
|
2017
2257
|
// A remote session: a human prompt sets the trust of this turn and the ones it leads to.
|
|
2018
2258
|
const remote = remotePolicyFor(live);
|
|
2019
2259
|
if (!system) live.turnUntrusted = untrusted === true;
|
|
2020
|
-
// A Claude brain its CLI cap stopped restarts with the budget it has now
|
|
2021
|
-
|
|
2260
|
+
// A Claude brain its CLI cap stopped restarts with the budget it has now, and so does one whose
|
|
2261
|
+
// budget setting changed since it started (a raise, a cut, No limit: no CLI cap at all).
|
|
2262
|
+
const budgetMoved = live.brainProvider === 'claude-code' && live.brainBudgetVersion !== undefined && live.brainBudgetVersion !== (budgetSettingNow()?.version ?? null);
|
|
2263
|
+
if ((live.brainCapHit || budgetMoved) && live.brain && !live.running) {
|
|
2022
2264
|
live.brainCapHit = false;
|
|
2023
2265
|
await disposeBrain(live, 'budget');
|
|
2024
2266
|
if (cancelled()) return dropTurn(live, request);
|
|
@@ -2049,15 +2291,19 @@ export function createAssistantRuntime({
|
|
|
2049
2291
|
return refuse(live, 'ASSISTANT_BUSY', BUSY_MESSAGE, { packet: { type: 'error', message: BUSY_MESSAGE, code: 'ASSISTANT_BUSY' }, request });
|
|
2050
2292
|
}
|
|
2051
2293
|
let finalText = prompt;
|
|
2294
|
+
// A Codex / OpenCode turn's recall result, for the user-context block below.
|
|
2295
|
+
let turnRecalled = null;
|
|
2052
2296
|
if (!system) {
|
|
2053
2297
|
live.lastPrompt = prompt;
|
|
2298
|
+
live.lastPromptOrigin = origin ? String(origin) : null;
|
|
2054
2299
|
// A prompt from elsewhere (WhatsApp, the API) is news to the panel: it goes live too.
|
|
2055
2300
|
const promptPacket = { type: 'event', event: { type: 'synabun.user_prompt', text: prompt, images: images.length, ...(origin && origin !== 'ui' ? { origin: String(origin) } : {}) } };
|
|
2056
2301
|
appendTranscript(live.record.id, { at: iso(now()), packet: promptPacket });
|
|
2057
2302
|
if (origin && origin !== 'ui') sendToSockets(live, promptPacket);
|
|
2058
2303
|
if (live.brainProvider !== 'claude-code' && memory?.recallForPrompt) {
|
|
2059
2304
|
let recalled = null;
|
|
2060
|
-
try { recalled = await memory.recallForPrompt({ prompt, project: live.record.project, session: live.record.id, generation: live.record.memoryGeneration || 1 }); } catch {}
|
|
2305
|
+
try { recalled = await memory.recallForPrompt({ prompt, project: live.record.project, session: live.record.id, generation: live.record.memoryGeneration || 1, divert: userContext?.divertCategories() || null }); } catch {}
|
|
2306
|
+
turnRecalled = recalled;
|
|
2061
2307
|
// Closed while it recalled: the session that reopened hears nothing of this prompt, and its record keeps what came since.
|
|
2062
2308
|
if (cancelled()) return dropTurn(live, request);
|
|
2063
2309
|
try {
|
|
@@ -2087,6 +2333,19 @@ export function createAssistantRuntime({
|
|
|
2087
2333
|
}
|
|
2088
2334
|
} catch {}
|
|
2089
2335
|
}
|
|
2336
|
+
// Test density changed since the brain last saw it (the same place, after the router's line).
|
|
2337
|
+
// Its prompt states the level it starts with (personaFor), so only a change is a line.
|
|
2338
|
+
try {
|
|
2339
|
+
const tests = testDensityStamp(testDensityOf(live.record));
|
|
2340
|
+
if (live.testDensityStamp === undefined) live.testDensityStamp = tests;
|
|
2341
|
+
else if (live.testDensityStamp !== tests) {
|
|
2342
|
+
finalText = finalText.endsWith(prompt) ? `${finalText.slice(0, finalText.length - prompt.length)}${tests}\n\n${prompt}` : `${tests}\n\n${finalText}`;
|
|
2343
|
+
live.testDensityStamp = tests;
|
|
2344
|
+
}
|
|
2345
|
+
} catch {}
|
|
2346
|
+
// The budget changed since the brain last saw it: one "[SynaBun Budget]" line, in the same place.
|
|
2347
|
+
const budgetNote = budgetLine(live);
|
|
2348
|
+
if (budgetNote) finalText = finalText.endsWith(prompt) ? `${finalText.slice(0, finalText.length - prompt.length)}${budgetNote}\n\n${prompt}` : `${budgetNote}\n\n${finalText}`;
|
|
2090
2349
|
// A new request cycle. Open clarify questions make this message the
|
|
2091
2350
|
// user's reply (assistant-clarify.js); otherwise the last brief retires.
|
|
2092
2351
|
live.promptCycle += 1;
|
|
@@ -2123,6 +2382,14 @@ export function createAssistantRuntime({
|
|
|
2123
2382
|
}
|
|
2124
2383
|
// The last check: from here on the turn is the brain's.
|
|
2125
2384
|
if (cancelled()) return dropTurn(live, request);
|
|
2385
|
+
// A route continuation starts: before any of its output, every host and observer hears where the
|
|
2386
|
+
// prompt it carries on came from (the WhatsApp bridge answers the chat that started the task).
|
|
2387
|
+
// It stays a system turn: no user_prompt, no new request cycle, nothing granted (computer use asks).
|
|
2388
|
+
if (system && work?.kind === 'continuation') {
|
|
2389
|
+
const continued = { type: 'event', event: { type: 'synabun.continuation', origin: origin ? String(origin) : null } };
|
|
2390
|
+
appendTranscript(live.record.id, { at: iso(now()), packet: continued });
|
|
2391
|
+
sendToSockets(live, continued);
|
|
2392
|
+
}
|
|
2126
2393
|
if (usage) {
|
|
2127
2394
|
taskId ||= live.record.taskId || ensureTaskId(live.record.id);
|
|
2128
2395
|
// record.taskId is the latest human task: a system turn that carries its own names only its turn.
|
|
@@ -2132,8 +2399,8 @@ export function createAssistantRuntime({
|
|
|
2132
2399
|
else live.turnTaskId = taskId;
|
|
2133
2400
|
scheduleUsage(live.record.id);
|
|
2134
2401
|
}
|
|
2135
|
-
// A remote session runs every turn at what its level allows now (an
|
|
2136
|
-
//
|
|
2402
|
+
// A remote session runs every turn at what its level allows now (a pause, an
|
|
2403
|
+
// untrusted prompt): the modes go to the brain with the turn.
|
|
2137
2404
|
if (remote) live.record.brain = clampedBrain(live, live.record.brain);
|
|
2138
2405
|
// A Codex / OpenCode plan turn ends with the plan, not an approval request: the runtime asks.
|
|
2139
2406
|
// (runQuery already closed the plan card this turn replaces.)
|
|
@@ -2149,7 +2416,8 @@ export function createAssistantRuntime({
|
|
|
2149
2416
|
live.record.lastActiveAt = iso(now());
|
|
2150
2417
|
live.turnStartedMs = now();
|
|
2151
2418
|
live.record.turns = (Number(live.record.turns) || 0) + 1;
|
|
2152
|
-
|
|
2419
|
+
// A name the user gave (updateSession) is never replaced, whatever it says.
|
|
2420
|
+
if (!live.record.userRenamed && isDefaultSessionTitle(live.record.title)) {
|
|
2153
2421
|
if (!system) { live.record.title = clip(prompt.split('\n')[0], 60); broadcast('assistant:session-updated', live); }
|
|
2154
2422
|
}
|
|
2155
2423
|
touch(live);
|
|
@@ -2157,12 +2425,48 @@ export function createAssistantRuntime({
|
|
|
2157
2425
|
// the record, and routed/continuation turns override it for one turn only.
|
|
2158
2426
|
const effectiveModel = turnModel || live.record.brain.model || null;
|
|
2159
2427
|
const effectiveEffort = brainEffort(live.record.brain.provider, effectiveModel, turnEffort || live.record.brain.effort || null);
|
|
2428
|
+
// User context (assistant-user-context.js): a human turn's trace and learning start here.
|
|
2429
|
+
// A Codex / OpenCode turn gets its per-turn block now (its recall result is known); a Claude
|
|
2430
|
+
// turn gets it in the in-process UserPromptSubmit hook, which finds the handle in turnMeta.
|
|
2431
|
+
let ucTurn = null;
|
|
2432
|
+
// Greeting (lib/greeting/): a human turn is the person being here, and the greeting the panel
|
|
2433
|
+
// showed when this conversation opened is told to the brain once, as background (never for a
|
|
2434
|
+
// slash command). The user-context block carries it when that engine runs; else it goes in
|
|
2435
|
+
// front of the user's words by itself.
|
|
2436
|
+
let greeted = null;
|
|
2437
|
+
if (!system && greeting) {
|
|
2438
|
+
try { greeting.noteActivity({ project: live.record.project || null, cwd: live.record.brain.cwd || null, surface: 'assistant', words: typed || prompt }); } catch { /* the turn goes on */ }
|
|
2439
|
+
if (greetingId) {
|
|
2440
|
+
try {
|
|
2441
|
+
const shown = greeting.peek(greetingId);
|
|
2442
|
+
const note = greeting.consumeNote(greetingId, { skip: prompt.trimStart().startsWith('/') });
|
|
2443
|
+
if (note && shown) greeted = { id: shown.id, text: shown.text, note, ids: (shown.basis || []).map((row) => row.id).filter(Boolean), source: shown.source, model: shown.model, costUsd: shown.costUsd };
|
|
2444
|
+
} catch { greeted = null; }
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
if (!system && userContext) {
|
|
2448
|
+
try {
|
|
2449
|
+
ucTurn = userContext.beginTurn(live.record, {
|
|
2450
|
+
prompt, typed, origin: origin || null, untrusted: untrusted === true, generation: live.record.memoryGeneration || 1,
|
|
2451
|
+
previousReply: live.lastReply || '', cwd: live.record.brain.cwd || null, taskId, greeting: greeted,
|
|
2452
|
+
emit: (event) => emitAutomation(live, event), persist: () => touch(live), openCards: () => openCards(live),
|
|
2453
|
+
});
|
|
2454
|
+
if (live.brainProvider !== 'claude-code') {
|
|
2455
|
+
const block = userContext.turnBlock(live.record, ucTurn, turnRecalled).text;
|
|
2456
|
+
// After the memories and the router / test lines, right before the user's words.
|
|
2457
|
+
if (block) finalText = finalText.endsWith(prompt) ? `${finalText.slice(0, finalText.length - prompt.length)}${block}\n\n${prompt}` : `${block}\n\n${finalText}`;
|
|
2458
|
+
}
|
|
2459
|
+
live.ucTurns.push(ucTurn);
|
|
2460
|
+
} catch (error) { ucTurn = null; log('assistant:user-context', error?.message || String(error)); }
|
|
2461
|
+
}
|
|
2462
|
+
// No user-context engine to carry it: the note goes right before the user's words.
|
|
2463
|
+
if (greeted && !ucTurn) finalText = finalText.endsWith(prompt) ? `${finalText.slice(0, finalText.length - prompt.length)}${greeted.note}\n\n${prompt}` : `${greeted.note}\n\n${finalText}`;
|
|
2160
2464
|
// The Claude brain recalls in its UserPromptSubmit hook, which sees only the
|
|
2161
2465
|
// final text: record who wrote it (mailbox / router = system) and the user's
|
|
2162
2466
|
// own words, so a router-stamped first turn recalls on the prompt itself.
|
|
2163
2467
|
if (live.brainProvider === 'claude-code' && finalText.trim()) {
|
|
2164
2468
|
live.turnMeta.delete(finalText.trim());
|
|
2165
|
-
live.turnMeta.set(finalText.trim(), { system: !!system, prompt: system ? null : prompt });
|
|
2469
|
+
live.turnMeta.set(finalText.trim(), { system: !!system, prompt: system ? null : prompt, ...(ucTurn ? { uc: ucTurn } : {}) });
|
|
2166
2470
|
while (live.turnMeta.size > 8) live.turnMeta.delete(live.turnMeta.keys().next().value);
|
|
2167
2471
|
}
|
|
2168
2472
|
// Every turn starts unrouted; a continuation or a route_decided "here" turn starts open.
|
|
@@ -2181,6 +2485,12 @@ export function createAssistantRuntime({
|
|
|
2181
2485
|
scheduleUsage(live.record.id);
|
|
2182
2486
|
live.planTurn = false;
|
|
2183
2487
|
live.record.status = 'idle';
|
|
2488
|
+
// The prompt never reached the brain: nothing is traced or learned from it.
|
|
2489
|
+
if (ucTurn) {
|
|
2490
|
+
const at = live.ucTurns.indexOf(ucTurn);
|
|
2491
|
+
if (at >= 0) live.ucTurns.splice(at, 1);
|
|
2492
|
+
try { userContext?.cancelTurn(live.record, ucTurn); } catch {}
|
|
2493
|
+
}
|
|
2184
2494
|
const message = error?.message || String(error);
|
|
2185
2495
|
sendToSockets(live, { type: 'error', message });
|
|
2186
2496
|
sendToSockets(live, { type: 'done', code: 1 });
|
|
@@ -2227,10 +2537,24 @@ export function createAssistantRuntime({
|
|
|
2227
2537
|
try { mark = dispatcher?.deliveryState?.(item.run.runId) || null; } catch { mark = null; }
|
|
2228
2538
|
return !!mark && Number(mark.turn) >= turn;
|
|
2229
2539
|
}
|
|
2230
|
-
/**
|
|
2540
|
+
/**
|
|
2541
|
+
* Out of money: the events wait (a raise or No limit re-schedules them), said once. The brain's
|
|
2542
|
+
* provider share used up while the session has money left: the budget is divided again, then the
|
|
2543
|
+
* mailbox goes.
|
|
2544
|
+
*/
|
|
2231
2545
|
function mailboxAllowed(live) {
|
|
2232
2546
|
const budgetBlock = brainBudgetBlock(live);
|
|
2233
2547
|
if (!budgetBlock) return true;
|
|
2548
|
+
if (budgetBlock.share && !live.budgetCovering && !(live.budgetShareTriedAt && now() - live.budgetShareTriedAt < 10_000)) {
|
|
2549
|
+
live.budgetCovering = true;
|
|
2550
|
+
coverBrainTurn(live).then((covered) => {
|
|
2551
|
+
live.budgetCovering = false;
|
|
2552
|
+
live.budgetShareTriedAt = covered ? 0 : now();
|
|
2553
|
+
if (live.mailbox.length) scheduleMailbox(live, 0);
|
|
2554
|
+
}).catch(() => { live.budgetCovering = false; live.budgetShareTriedAt = now(); });
|
|
2555
|
+
return false;
|
|
2556
|
+
}
|
|
2557
|
+
if (live.budgetCovering) return false;
|
|
2234
2558
|
if (!live.budgetHoldNotified) {
|
|
2235
2559
|
live.budgetHoldNotified = true;
|
|
2236
2560
|
sendToSockets(live, { type: 'error', code: budgetBlock.code, message: `${budgetBlock.message} ${live.mailbox.length} agent event${live.mailbox.length === 1 ? '' : 's'} will reach the brain after that.` });
|
|
@@ -2261,7 +2585,9 @@ export function createAssistantRuntime({
|
|
|
2261
2585
|
let sessionRuns = [];
|
|
2262
2586
|
try { sessionRuns = unread.some((item) => item?.kind === 'route_decided') ? (dispatcher?.list?.({ assistantSessionId: live.record.id, limit: 500 }) || []) : []; } catch { sessionRuns = []; }
|
|
2263
2587
|
const items = reconcileRouteItems(unread, sessionRuns);
|
|
2264
|
-
|
|
2588
|
+
// A budget change since the brain last saw it rides first (a raise is what released a held mailbox).
|
|
2589
|
+
const budgetNote = budgetLine(live);
|
|
2590
|
+
const text = budgetNote ? `${budgetNote}\n\n${formatMailbox(items)}` : formatMailbox(items);
|
|
2265
2591
|
sendToSockets(live, { type: 'event', event: { type: 'synabun.mailbox', items } });
|
|
2266
2592
|
appendTranscript(live.record.id, { at: iso(now()), packet: { type: 'event', event: { type: 'synabun.mailbox', items } } });
|
|
2267
2593
|
live.mailboxTurn = true;
|
|
@@ -2281,6 +2607,8 @@ export function createAssistantRuntime({
|
|
|
2281
2607
|
if (!run?.assistantSessionId) return;
|
|
2282
2608
|
const live = sessions.get(run.assistantSessionId);
|
|
2283
2609
|
if (!live) return;
|
|
2610
|
+
// The conversation's thread record: its runs, their outcomes and follow-ups.
|
|
2611
|
+
try { userContext?.onDispatch(live.record, payload); } catch {}
|
|
2284
2612
|
if (!live.record.dispatchRunIds?.includes(run.runId)) { live.record.dispatchRunIds = [...(live.record.dispatchRunIds || []), run.runId].slice(-100); touch(live); }
|
|
2285
2613
|
sendToSockets(live, { type: 'assistant:dispatch', reason: payload.reason, run });
|
|
2286
2614
|
scheduleUsage(live.record.id);
|
|
@@ -2316,6 +2644,7 @@ export function createAssistantRuntime({
|
|
|
2316
2644
|
if (!live) return;
|
|
2317
2645
|
const removed = new Set((payload.runIds || []).map(String));
|
|
2318
2646
|
if (!removed.size) return;
|
|
2647
|
+
try { userContext?.onDispatch(live.record, { type: 'assistant:run-removed', runIds: [...removed] }); } catch {}
|
|
2319
2648
|
for (const runId of removed) {
|
|
2320
2649
|
live.obligations.delete(runId);
|
|
2321
2650
|
live.nudgedRuns.delete(runId);
|
|
@@ -2351,15 +2680,25 @@ export function createAssistantRuntime({
|
|
|
2351
2680
|
* `fallback`: the UI's start path — a disabled model starts on its enabled stand-in (reported back) instead of refusing.
|
|
2352
2681
|
* `channel`: where the conversation happens ('whatsapp'); the persona adapts to it.
|
|
2353
2682
|
* `internal.remote` ({ channel, level, strictWorkerApprovals, autonomousUntil, computerUse }): an in-process
|
|
2354
|
-
* caller (the WhatsApp bridge) starts a remote session
|
|
2683
|
+
* caller (the WhatsApp bridge) starts a remote session (`autonomousUntil` limits Autonomous on a channel
|
|
2684
|
+
* other than WhatsApp only: on WhatsApp it has no time limit). REST callers pass only the body, so
|
|
2355
2685
|
* they cannot. The record is flagged before it is persisted: after a restart without a new
|
|
2356
2686
|
* registration the session reads as read-only.
|
|
2357
2687
|
*/
|
|
2688
|
+
async function resolveAutoAccount(brain) {
|
|
2689
|
+
if (brain.accountId !== 'auto' || !['claude-code', 'codex'].includes(brain.provider)) return brain;
|
|
2690
|
+
await accountUsage?.refresh(brain.provider);
|
|
2691
|
+
const rows = accountUsage?.list()?.[brain.provider] || [];
|
|
2692
|
+
const key = brain.provider === 'codex' ? 'codexAccount' : 'claudeAccount';
|
|
2693
|
+
const { row } = chooseUsageAccount(rows, 'auto', configStore?.workers?.()?.[key] || 'default');
|
|
2694
|
+
if (!row) throw Object.assign(new Error('Auto requires a signed-in subscription account.'), { code: 'ACCOUNT_NO_SUBSCRIPTION', status: 409 });
|
|
2695
|
+
return { ...brain, accountId: row.id };
|
|
2696
|
+
}
|
|
2358
2697
|
async function createSession({ brain = {}, label = null, cwd = null, windowId = null, fallback = false, channel = null } = {}, internal = {}) {
|
|
2359
2698
|
const id = `assistant-${randomUUID()}`;
|
|
2360
2699
|
const requested = normalizeBrain({ ...brain, cwd: brain?.cwd || cwd || settings.defaultBrain.cwd || PACKAGE_ROOT }, settings.defaultBrain, { catalog: catalog?.peek?.() || null });
|
|
2361
2700
|
const enabled = await enabledBrain(requested, { fallback: fallback === true, named: !!brain?.model });
|
|
2362
|
-
const normalized = enabled.brain;
|
|
2701
|
+
const normalized = await resolveAutoAccount(enabled.brain);
|
|
2363
2702
|
const record = {
|
|
2364
2703
|
id, brain: normalized, title: label ? clip(label, 60) : DEFAULT_SESSION_TITLE, project: detectProject(normalized.cwd) || 'global',
|
|
2365
2704
|
createdAt: iso(now()), updatedAt: iso(now()), lastActiveAt: null, status: 'idle', providerSessionId: null, providerThreadId: null,
|
|
@@ -2395,6 +2734,10 @@ export function createAssistantRuntime({
|
|
|
2395
2734
|
if (mode && !normalized) { const error = new Error(`Unknown route mode ${mode}`); error.code = 'ROUTE_MODE_INVALID'; error.status = 400; throw error; }
|
|
2396
2735
|
live.record.routing = { ...(live.record.routing || {}), mode: normalized };
|
|
2397
2736
|
}
|
|
2737
|
+
/** null, '' or an unknown value clears the session's own level: it reads as the default. */
|
|
2738
|
+
function applyTestDensity(live, level) {
|
|
2739
|
+
live.record.testDensity = normalizeTestDensity(level, null);
|
|
2740
|
+
}
|
|
2398
2741
|
function applyComputerUse(live, enabled) {
|
|
2399
2742
|
// A remote session's computer use is not a per-conversation choice: the owner's switch and
|
|
2400
2743
|
// the level decide (remoteComputerUse). A PATCH, the socket and updateSession change nothing.
|
|
@@ -2406,29 +2749,46 @@ export function createAssistantRuntime({
|
|
|
2406
2749
|
async function updateSession(id, patch = {}) {
|
|
2407
2750
|
const live = sessions.get(id);
|
|
2408
2751
|
if (!live) return null;
|
|
2409
|
-
|
|
2752
|
+
// A rename (tab, float header, sidepanel ✎) is the session's one name: every list reads it
|
|
2753
|
+
// here, and `userRenamed` tells a host to reopen the session under it. Calling it "SynaBun"
|
|
2754
|
+
// again gives the name up: the next prompt titles it, as for a session never renamed. The
|
|
2755
|
+
// mark stays, as `false`: a host holding the old name with a saved tab must not write it back
|
|
2756
|
+
// (no mark at all means a rename the record never heard of, which a host does write).
|
|
2757
|
+
const renamed = typeof patch.title === 'string' ? clip(patch.title, 60) : '';
|
|
2758
|
+
if (renamed && renamed !== DEFAULT_SESSION_TITLE) { live.record.title = renamed; live.record.userRenamed = true; }
|
|
2759
|
+
else if (renamed && live.record.userRenamed) { live.record.title = DEFAULT_SESSION_TITLE; live.record.userRenamed = false; }
|
|
2410
2760
|
if (patch.routing && typeof patch.routing === 'object' && 'mode' in patch.routing) applyRouteMode(live, patch.routing.mode);
|
|
2411
2761
|
if ('computerUse' in patch) applyComputerUse(live, patch.computerUse);
|
|
2762
|
+
if ('testDensity' in patch) applyTestDensity(live, patch.testDensity);
|
|
2412
2763
|
if (patch.brain && typeof patch.brain === 'object') {
|
|
2413
2764
|
// A provider switch that names no model starts from the new provider's
|
|
2414
2765
|
// default, never from the old provider's model id.
|
|
2415
2766
|
const switching = BRAIN_PROVIDERS.has(patch.brain.provider) && patch.brain.provider !== live.record.brain.provider;
|
|
2416
2767
|
const base = switching && !patch.brain.model ? { ...live.record.brain, model: null } : live.record.brain;
|
|
2417
2768
|
let next = normalizeBrain({ ...base, ...patch.brain, model: patch.brain.model || base.model }, base, { catalog: catalog?.peek?.() || null });
|
|
2769
|
+
next = await resolveAutoAccount(next);
|
|
2418
2770
|
const modelChanged = next.model !== live.record.brain.model || next.provider !== live.record.brain.provider;
|
|
2419
2771
|
if (modelChanged) next = (await enabledBrain(next)).brain;
|
|
2420
2772
|
// A remote session keeps what was asked for and runs what its level allows.
|
|
2421
2773
|
if ('permissionMode' in patch.brain || 'planMode' in patch.brain) next = clampModeWrite(live, next);
|
|
2422
2774
|
else next = clampedBrain(live, next);
|
|
2423
|
-
const providerChanged = next.provider !== live.record.brain.provider || next.accountId !== live.record.brain.accountId || next.cwd !== live.record.brain.cwd || next.mcpProfile !== live.record.brain.mcpProfile;
|
|
2775
|
+
const providerChanged = patch.restartBrain === true || next.provider !== live.record.brain.provider || next.accountId !== live.record.brain.accountId || next.cwd !== live.record.brain.cwd || next.mcpProfile !== live.record.brain.mcpProfile;
|
|
2424
2776
|
// A usage limit is the account's: another provider or account starts without the notice.
|
|
2425
2777
|
if (next.provider !== live.record.brain.provider || next.accountId !== live.record.brain.accountId) setLimit(live, null);
|
|
2426
2778
|
live.record.brain = next;
|
|
2427
|
-
|
|
2779
|
+
// The panel's brain names its working folder `project` and never sends `cwd`. A project picked
|
|
2780
|
+
// there (the toolbar's Working project, the empty state's picker) moves the folder the way a
|
|
2781
|
+
// prompt's cwd does (runTurn): a brain that is running keeps its conversation and gets the folder
|
|
2782
|
+
// with its next turn; one not started yet starts there, with it as its Default project.
|
|
2783
|
+
const folder = !patch.brain.cwd && typeof patch.brain.project === 'string' ? patch.brain.project.trim() : '';
|
|
2784
|
+
if (folder && folder !== next.cwd) live.record.brain = { ...next, cwd: folder };
|
|
2785
|
+
live.record.project = detectProject(live.record.brain.cwd) || live.record.project || 'global';
|
|
2428
2786
|
if (providerChanged) {
|
|
2429
2787
|
await disposeBrain(live, 'switched');
|
|
2430
2788
|
live.record.providerSessionId = null;
|
|
2431
2789
|
live.record.providerThreadId = null;
|
|
2790
|
+
// Another conversation: its user-context digest is built (and frozen) anew.
|
|
2791
|
+
try { userContext?.conversationReplaced(live.record); } catch {}
|
|
2432
2792
|
if (live.record.usageState) delete live.record.usageState[next.provider === 'claude-code' ? 'claude' : next.provider];
|
|
2433
2793
|
if (live.record.usageState) delete live.record.usageState.codexThreadId;
|
|
2434
2794
|
live.record.status = 'idle';
|
|
@@ -2472,6 +2832,8 @@ export function createAssistantRuntime({
|
|
|
2472
2832
|
live.record.status = 'ended';
|
|
2473
2833
|
touch(live);
|
|
2474
2834
|
persist({ force: true });
|
|
2835
|
+
// The conversation's thread memory is written now (not awaited: an embedding and a write).
|
|
2836
|
+
try { userContext?.sessionClosed(id); } catch {}
|
|
2475
2837
|
broadcast('assistant:session-ended', live);
|
|
2476
2838
|
} finally { closed(); }
|
|
2477
2839
|
return view(live);
|
|
@@ -2489,6 +2851,7 @@ export function createAssistantRuntime({
|
|
|
2489
2851
|
usageTimers.delete(id);
|
|
2490
2852
|
usageSentAt.delete(id);
|
|
2491
2853
|
metered('drop', () => usage?.dropSession?.(id));
|
|
2854
|
+
try { userContext?.sessionDestroyed(id); } catch {}
|
|
2492
2855
|
try { remotePolicy?.forgetSession?.(id); } catch {}
|
|
2493
2856
|
try { rmSync(resolve(dataDir, 'assistant', id), { recursive: true, force: true }); } catch {}
|
|
2494
2857
|
persist({ force: true });
|
|
@@ -2509,6 +2872,13 @@ export function createAssistantRuntime({
|
|
|
2509
2872
|
}
|
|
2510
2873
|
return null;
|
|
2511
2874
|
}
|
|
2875
|
+
// Why the Claude account a brain names cannot run now ('' when it can, or when that is not known yet).
|
|
2876
|
+
function claudeAccountUnusable(accountId) {
|
|
2877
|
+
try { return claudeAccounts?.unusableReason?.(accountId || 'default') || ''; } catch { return ''; }
|
|
2878
|
+
}
|
|
2879
|
+
function codexAccountUnusable(accountId) {
|
|
2880
|
+
try { return codexAccounts?.unusableReason?.(accountId || 'default') || ''; } catch { return ''; }
|
|
2881
|
+
}
|
|
2512
2882
|
function accountInUse(provider, accountId) {
|
|
2513
2883
|
for (const live of sessions.values()) {
|
|
2514
2884
|
if (live.brain && live.record.brain?.provider === provider && (live.record.brain?.accountId || 'default') === accountId) return true;
|
|
@@ -2535,6 +2905,23 @@ export function createAssistantRuntime({
|
|
|
2535
2905
|
}
|
|
2536
2906
|
/** Something runs, or the approved plan's turn or a compact is waiting to (a prompt would be refused). */
|
|
2537
2907
|
function isBusy(id) { const live = sessions.get(String(id || '')); return sessionBusy(live) || (!!live && !!workReserved(live)); }
|
|
2908
|
+
/**
|
|
2909
|
+
* The session whose live brain holds this credential (whatsapp_send_file's caller identity:
|
|
2910
|
+
* BRAIN_CREDENTIAL_RE), or null. Only a well-formed value is compared, in constant time and
|
|
2911
|
+
* against every live brain, so neither a match nor its position shows in the time taken.
|
|
2912
|
+
*/
|
|
2913
|
+
function sessionForBrainCredential(value) {
|
|
2914
|
+
const text = typeof value === 'string' ? value.trim() : '';
|
|
2915
|
+
if (!BRAIN_CREDENTIAL_RE.test(text)) return null;
|
|
2916
|
+
const given = createHash('sha256').update(text).digest();
|
|
2917
|
+
let found = null;
|
|
2918
|
+
for (const [id, live] of sessions) {
|
|
2919
|
+
if (!live.brain || typeof live.brainCredential !== 'string') continue;
|
|
2920
|
+
const held = createHash('sha256').update(live.brainCredential).digest();
|
|
2921
|
+
if (timingSafeEqual(given, held) && found === null) found = id;
|
|
2922
|
+
}
|
|
2923
|
+
return found;
|
|
2924
|
+
}
|
|
2538
2925
|
/** The session's remote policy (lib/remote-policy.js), or null. */
|
|
2539
2926
|
function sessionPolicy(id) { const live = sessions.get(String(id || '')); return live ? remotePolicyFor(live) : null; }
|
|
2540
2927
|
/** Where the session's conversation happens: 'whatsapp' (WhatsApp Link), or null for the panel. */
|
|
@@ -2752,6 +3139,8 @@ export function createAssistantRuntime({
|
|
|
2752
3139
|
try { ws.send(JSON.stringify(engine)); } catch {}
|
|
2753
3140
|
try { ws.send(JSON.stringify({ type: 'assistant:session', session: view(live), reason: 'attached' })); } catch {}
|
|
2754
3141
|
if (usage) try { ws.send(JSON.stringify({ type: 'assistant:usage', ...usageView(live.record.id) })); } catch {}
|
|
3142
|
+
// The session budget and its split as they are now.
|
|
3143
|
+
try { const budgetNow = sessionMoney(live.record.id); if (budgetNow) ws.send(JSON.stringify({ type: 'assistant:budget', sessionId: live.record.id, reason: 'attached', budget: budgetNow })); } catch {}
|
|
2755
3144
|
// The limit notice as it is now (null clears one this panel still shows from before).
|
|
2756
3145
|
sendLimit(live, [ws]);
|
|
2757
3146
|
const replay = live.buffer.splice(0, live.buffer.length);
|
|
@@ -2772,7 +3161,7 @@ export function createAssistantRuntime({
|
|
|
2772
3161
|
// `request_id`: the panel's id for this prompt. A refusal reaches this socket
|
|
2773
3162
|
// alone and carries it back, so this panel (only) puts the text back.
|
|
2774
3163
|
const request = { id: msg.request_id ? String(msg.request_id) : null, reply };
|
|
2775
|
-
const result = await runQuery(live, { text: msg.prompt, images: msg.images || [], cwd: msg.cwd || null, model: msg.model || null, effort: msg.effort || null, permissionMode: msg.permissionMode || null, planMode: typeof msg.planMode === 'boolean' ? msg.planMode : null, origin: 'ui', request });
|
|
3164
|
+
const result = await runQuery(live, { text: msg.prompt, images: msg.images || [], cwd: msg.cwd || null, model: msg.model || null, effort: msg.effort || null, permissionMode: msg.permissionMode || null, planMode: typeof msg.planMode === 'boolean' ? msg.planMode : null, origin: 'ui', request, typed: typeof msg.typed === 'string' ? msg.typed : null, greetingId: typeof msg.greetingId === 'string' ? msg.greetingId : null });
|
|
2776
3165
|
// Refused before it ran: the panel, "working" since it sent, shows what runs now.
|
|
2777
3166
|
if (result && !result.ok && result.code !== 'TURN_FAILED') reply({ type: 'reattach_result', ok: true, sessionId: live.record.id, running: isBusy(live.record.id) });
|
|
2778
3167
|
return;
|
|
@@ -2814,6 +3203,12 @@ export function createAssistantRuntime({
|
|
|
2814
3203
|
broadcast('assistant:session-updated', live);
|
|
2815
3204
|
sendToSockets(live, { type: 'assistant:session', session: view(live), reason: 'routing' });
|
|
2816
3205
|
return;
|
|
3206
|
+
case 'set_test_density':
|
|
3207
|
+
applyTestDensity(live, msg.level);
|
|
3208
|
+
touch(live);
|
|
3209
|
+
broadcast('assistant:session-updated', live);
|
|
3210
|
+
sendToSockets(live, { type: 'assistant:session', session: view(live), reason: 'tests' });
|
|
3211
|
+
return;
|
|
2817
3212
|
case 'set_computer_use':
|
|
2818
3213
|
applyComputerUse(live, msg.enabled);
|
|
2819
3214
|
touch(live);
|
|
@@ -2837,12 +3232,14 @@ export function createAssistantRuntime({
|
|
|
2837
3232
|
return;
|
|
2838
3233
|
case 'compact':
|
|
2839
3234
|
live.record.memoryGeneration = (live.record.memoryGeneration || 1) + 1;
|
|
3235
|
+
try { userContext?.onCompaction(live.record); } catch {}
|
|
2840
3236
|
touch(live);
|
|
2841
3237
|
// Session work: it starts once a running turn ends, and nothing else starts until it settles.
|
|
2842
3238
|
queueWork(live, { kind: 'compact', reply });
|
|
2843
3239
|
return;
|
|
2844
3240
|
case 'clear':
|
|
2845
3241
|
live.record.memoryGeneration = (live.record.memoryGeneration || 1) + 1;
|
|
3242
|
+
try { userContext?.onCompaction(live.record); } catch {}
|
|
2846
3243
|
touch(live);
|
|
2847
3244
|
return;
|
|
2848
3245
|
case 'heartbeat':
|
|
@@ -2869,6 +3266,8 @@ export function createAssistantRuntime({
|
|
|
2869
3266
|
live.record.providerSessionId = msg.providerSessionId;
|
|
2870
3267
|
// Another provider session: the meter's baseline was the old one's.
|
|
2871
3268
|
if (moved) resetMeterIdentity(live, msg.providerSessionId);
|
|
3269
|
+
// …and so was the frozen user-context digest.
|
|
3270
|
+
if (moved) { try { userContext?.conversationReplaced(live.record); } catch {} }
|
|
2872
3271
|
touch(live);
|
|
2873
3272
|
await live.brain?.resume?.(msg.providerSessionId);
|
|
2874
3273
|
}
|
|
@@ -2887,7 +3286,20 @@ export function createAssistantRuntime({
|
|
|
2887
3286
|
}
|
|
2888
3287
|
}
|
|
2889
3288
|
|
|
3289
|
+
/** The dispatcher's reading of a session's Test density (wired in server.js); null for an unknown session. */
|
|
3290
|
+
function testDensityFor(id) {
|
|
3291
|
+
const live = sessions.get(id);
|
|
3292
|
+
return live ? testDensityOf(live.record) : null;
|
|
3293
|
+
}
|
|
3294
|
+
|
|
2890
3295
|
// ── router sinks (wired lazily in server.js; see assistant-router.js) ──────
|
|
3296
|
+
/** What the budget planner needs of a session: its brain, project, and whether it is still open. */
|
|
3297
|
+
function budgetSession(id) {
|
|
3298
|
+
const live = sessions.get(String(id || ''));
|
|
3299
|
+
if (!live) return null;
|
|
3300
|
+
const brain = live.record.brain || {};
|
|
3301
|
+
return { provider: brain.provider || null, model: brain.model || null, accountId: brain.accountId || null, project: live.record.project || null, open: live.record.status !== 'ended' };
|
|
3302
|
+
}
|
|
2891
3303
|
function routerSession(id) {
|
|
2892
3304
|
const live = sessions.get(id);
|
|
2893
3305
|
if (!live) return null;
|
|
@@ -2962,7 +3374,8 @@ export function createAssistantRuntime({
|
|
|
2962
3374
|
const label = row?.label || target.model || 'the chosen model';
|
|
2963
3375
|
const summary = route?.summary || '';
|
|
2964
3376
|
const text = `[SynaBun Router] The user chose to run ${summary ? `"${summary}"` : 'this task'} here on ${label}${target.effort ? ` (effort ${target.effort})` : ''}. Continue that task now on this model, from where the conversation left off.`;
|
|
2965
|
-
|
|
3377
|
+
// `origin`: where the prompt this task came from was written, so the continuation answers there.
|
|
3378
|
+
queueWork(live, { kind: 'continuation', gen, options: { text, turnModel: target.model || null, turnEffort: target.effort || null, gateOpen: true, origin: lastPromptOrigin(live) } });
|
|
2966
3379
|
return true;
|
|
2967
3380
|
}
|
|
2968
3381
|
function routerSinks() {
|
|
@@ -2989,6 +3402,19 @@ export function createAssistantRuntime({
|
|
|
2989
3402
|
}
|
|
2990
3403
|
return '';
|
|
2991
3404
|
}
|
|
3405
|
+
/**
|
|
3406
|
+
* Where the last human prompt came from ('ui', 'whatsapp', 'api'): the transcript's after a restart
|
|
3407
|
+
* (a prompt journaled without an origin was typed in the panel). null when there is none.
|
|
3408
|
+
*/
|
|
3409
|
+
function lastPromptOrigin(live) {
|
|
3410
|
+
if (live.lastPromptOrigin) return live.lastPromptOrigin;
|
|
3411
|
+
const entries = readTranscript(live.record.id, 200);
|
|
3412
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
3413
|
+
const event = entries[i]?.packet?.event;
|
|
3414
|
+
if (event?.type === 'synabun.user_prompt') return event.origin ? String(event.origin) : 'ui';
|
|
3415
|
+
}
|
|
3416
|
+
return null;
|
|
3417
|
+
}
|
|
2992
3418
|
function clarifySession(id) {
|
|
2993
3419
|
const live = sessions.get(id);
|
|
2994
3420
|
if (!live) return null;
|
|
@@ -3204,7 +3630,7 @@ export function createAssistantRuntime({
|
|
|
3204
3630
|
? { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow', permissionDecisionReason: 'Computer use is on for this WhatsApp turn.' } }
|
|
3205
3631
|
: { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'ask', permissionDecisionReason: 'The user is asked once per task before the Mac is controlled from WhatsApp.' } };
|
|
3206
3632
|
}
|
|
3207
|
-
if (remoteLevel(live) === 'autonomous' || REMOTE_ASK_EXEMPT.has(tool)) return {};
|
|
3633
|
+
if (remoteLevel(live) === 'autonomous' || REMOTE_ASK_EXEMPT.has(tool) || isReplyTool(tool)) return {};
|
|
3208
3634
|
if (claudeReadOnlyPermission(tool, toolInput) !== 'deny') return {};
|
|
3209
3635
|
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'ask', permissionDecisionReason: `This session is driven from ${live.record.remote?.channel === 'whatsapp' || !live.record.remote?.channel ? 'WhatsApp' : live.record.remote.channel}: the user approves ${tool} first.` } };
|
|
3210
3636
|
}],
|
|
@@ -3303,6 +3729,7 @@ export function createAssistantRuntime({
|
|
|
3303
3729
|
live.record.status = 'idle';
|
|
3304
3730
|
}
|
|
3305
3731
|
persist({ force: true });
|
|
3732
|
+
try { userContext?.shutdown(); } catch {}
|
|
3306
3733
|
// Rows whose append failed get one more try before the process goes.
|
|
3307
3734
|
metered('flush', () => usage?.flush?.());
|
|
3308
3735
|
}
|
|
@@ -3326,12 +3753,14 @@ export function createAssistantRuntime({
|
|
|
3326
3753
|
handleWebSocket, listSessions, getSession, createSession, updateSession, closeSession, destroySession,
|
|
3327
3754
|
resolveTerminal, accountInUse, ownsIsolatedServe, noteDispatch, subscribe, shutdown,
|
|
3328
3755
|
observeBroadcast: onPermissionBroadcast,
|
|
3329
|
-
routerSession, routerSend, routerEvent, routerMailbox, routerContinue, routerSinks, routerRouted, routerRouteFailed, routerComputerApproved, gateCheck, isPlanning, isPlanReadOnly,
|
|
3756
|
+
testDensityFor, routerSession, routerSend, routerEvent, routerMailbox, routerContinue, routerSinks, routerRouted, routerRouteFailed, routerComputerApproved, gateCheck, isPlanning, isPlanReadOnly,
|
|
3330
3757
|
clarifySession, clarifyEvent, clarifyGate, clarifySinks,
|
|
3331
3758
|
getComputerUse, getComputerTurn, supersedeComputerApproval, abortTurn, notifySession,
|
|
3332
|
-
brainSpend,
|
|
3759
|
+
brainSpend, brainSpendByProvider, budgetSession, onBudgetChanged, onBudgetSplit, sendBudget, currentTask, usageView,
|
|
3333
3760
|
// Programmatic surface (the WhatsApp bridge) and remote sessions.
|
|
3334
3761
|
observe, submit, stopTurn, answerControl, answerDispatchControl, pendingControls, isBusy, sessionPolicy, sessionChannel,
|
|
3335
|
-
|
|
3762
|
+
// whatsapp_send_file's caller identity (POST /api/assistant/whatsapp/send-file).
|
|
3763
|
+
sessionForBrainCredential,
|
|
3764
|
+
_internals: { sessions, ensureBrain, disposeBrain, runQuery, enqueueMailbox, deliverMailbox, settings, ensureGate, onBrainPacket, onTurnFinished, remoteHooks, packetObservers, userContext },
|
|
3336
3765
|
};
|
|
3337
3766
|
}
|