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
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun — Assistant user context: the learning loop
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// What a human turn teaches about the user, off the reply path and at zero
|
|
6
|
+
// brain tokens:
|
|
7
|
+
//
|
|
8
|
+
// 1. prefilter (free): origin, the typed words only, not trivial, a cue
|
|
9
|
+
// lexicon in English and Portuguese, the session's caps, a content hash;
|
|
10
|
+
// 2. one Jev request (surface `user-learning`, origin `assistant`) over the
|
|
11
|
+
// message, its sentences, the previous reply (context only) and up to six
|
|
12
|
+
// known statements: durable signal, facet, scope, the evidence sentence,
|
|
13
|
+
// the known statement it matches, and whether it contradicts each;
|
|
14
|
+
// 3. the writer (assistant-user-writer.js), only when step 2 passed, checked
|
|
15
|
+
// by a second Jev question; a statement that adds anything is dropped for
|
|
16
|
+
// the user's own sentence;
|
|
17
|
+
// 4. exactly one write: create, update or supersede.
|
|
18
|
+
//
|
|
19
|
+
// Jev and the writer are injected and may be missing: without Jev nothing is
|
|
20
|
+
// learned (there is no heuristic that should write to the owner's memory),
|
|
21
|
+
// without the writer the statement is the user's sentence. Nothing here throws.
|
|
22
|
+
|
|
23
|
+
import { createHash } from 'node:crypto';
|
|
24
|
+
import { isTrivialPrompt } from '../../hooks/claude-code/prompt-origin.mjs';
|
|
25
|
+
import { FACETS, FACET_KIND, LEARN } from './assistant-user-context-budgets.js';
|
|
26
|
+
import { addEvidence, cleanQuote, cleanStatement, formatKnowledge, guessFacet, raiseConfidence } from './assistant-user-knowledge.js';
|
|
27
|
+
|
|
28
|
+
// ── typed words ─────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const FENCE_RE = /```[\s\S]*?(?:```|$)/g;
|
|
31
|
+
const FILE_RE = /<file path="[^"]*">[\s\S]*?<\/file>/g;
|
|
32
|
+
const MEMORY_RE = /<memory id="[^"]*"[^>]*>[\s\S]*?<\/memory>/g;
|
|
33
|
+
// asst-composer.js attachmentPromptLine.
|
|
34
|
+
const UPLOAD_RE = /^The user attached a file: .*$/gm;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The words the user typed: the composer's own report (`typed`: the text without its long pastes)
|
|
38
|
+
* when every line of it is in the prompt, else the prompt. Attachments (inline files, attached
|
|
39
|
+
* memories, upload lines) and fenced blocks are removed either way.
|
|
40
|
+
*/
|
|
41
|
+
export function typedEvidence({ prompt = '', typed = null } = {}) {
|
|
42
|
+
const full = String(prompt ?? '');
|
|
43
|
+
let source = full;
|
|
44
|
+
if (typeof typed === 'string') {
|
|
45
|
+
const lines = typed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
46
|
+
if (lines.every((line) => full.includes(line))) source = typed;
|
|
47
|
+
}
|
|
48
|
+
return source.replace(FENCE_RE, ' ').replace(FILE_RE, ' ').replace(MEMORY_RE, ' ').replace(UPLOAD_RE, ' ')
|
|
49
|
+
.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── cue lexicon ─────────────────────────────────────────────────────────────
|
|
53
|
+
// Words that mark a standing instruction, a habit, a repeated request or a
|
|
54
|
+
// correction. A prompt without one costs nothing; with one, Jev decides.
|
|
55
|
+
|
|
56
|
+
const B = '(?<![\\p{L}\\p{N}])';
|
|
57
|
+
const E = '(?![\\p{L}\\p{N}])';
|
|
58
|
+
const cue = (name, source) => [name, new RegExp(`${B}(?:${source})${E}`, 'iu')];
|
|
59
|
+
|
|
60
|
+
export const CUE_LEXICON = Object.freeze([
|
|
61
|
+
cue('always', 'always|every ?time|each time|whenever|sempre|toda vez|todas as vezes|cada vez'),
|
|
62
|
+
cue('never', "never|not ever|don'?t ever|nunca|jamais"),
|
|
63
|
+
cue('from now on', 'from now on|going forward|from here on|in (?:the )?future|next time|de agora em diante|daqui (?:pra|para) frente|a partir de agora|da pr[óo]xima vez|no futuro'),
|
|
64
|
+
cue('I told you', "i (?:already )?told you|i(?:'ve| have) (?:already )?(?:told|said|asked)|as i said|like i said|i said|how many times|again and again|j[áa] (?:te )?(?:falei|disse|pedi)|(?:eu )?te (?:falei|disse|pedi)|como eu (?:disse|falei)|quantas vezes"),
|
|
65
|
+
cue('stop', "stop (?:\\p{L}+ing|that|it)|quit (?:\\p{L}+ing|that|it)|no more|pare de|para de|chega de|n[ãa]o (?:fa[çc]a|use|coloque|adicione|mexa) mais"),
|
|
66
|
+
cue('by default', 'by default|as a rule|in general|as usual|por padr[ãa]o|como regra|em geral|como de costume'),
|
|
67
|
+
cue('prefer', "i(?:'d| would)? (?:much )?(?:prefer|rather)|my preference|i (?:really )?(?:hate|dislike|like|love) (?:it )?when|(?:eu )?prefiro|minha prefer[êe]ncia|(?:eu )?(?:odeio|detesto|gosto|adoro) quando"),
|
|
68
|
+
cue('remember', "remember (?:that|to)|keep in mind|don'?t forget|make sure (?:to|you) always|lembre(?:-se)?(?: de)? que|n[ãa]o (?:se )?esque[çc]a|tenha em mente"),
|
|
69
|
+
cue('must', "(?:you )?must (?:always|never|not)|should (?:always|never)|(?:you )?(?:deve|devem|precisa) (?:sempre|nunca)|tem que sempre"),
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
/** The cue names a text hits, in lexicon order. */
|
|
73
|
+
export function matchCues(text) {
|
|
74
|
+
const value = String(text ?? '');
|
|
75
|
+
return CUE_LEXICON.filter(([, re]) => re.test(value)).map(([name]) => name);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The sentences of a typed text: those with a cue first when there are too many, in their original order. */
|
|
79
|
+
export function splitSentences(text, { max = LEARN.maxSentences, clip = LEARN.sentenceClip } = {}) {
|
|
80
|
+
const parts = String(text ?? '').split(/(?<=[.!?…])\s+(?=[\p{Lu}\p{N}"'(\[¿¡])|\n+/u).map((part) => part.trim()).filter((part) => part.length >= 3);
|
|
81
|
+
let kept = parts.map((part, index) => ({ part, index }));
|
|
82
|
+
if (kept.length > max) {
|
|
83
|
+
const withCue = kept.filter(({ part }) => matchCues(part).length);
|
|
84
|
+
const rest = kept.filter(({ part }) => !matchCues(part).length);
|
|
85
|
+
kept = [...withCue, ...rest].slice(0, max).sort((a, b) => a.index - b.index);
|
|
86
|
+
}
|
|
87
|
+
return kept.map(({ part }) => (part.length > clip ? `${part.slice(0, clip - 1)}…` : part));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function contentHash(text) {
|
|
91
|
+
return createHash('sha256').update(String(text ?? '').replace(/\s+/g, ' ').trim().toLowerCase()).digest('hex').slice(0, 16);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The free filter. `turn`: { origin, untrusted, system, prompt, typed }; `session`:
|
|
96
|
+
* { counts:{ gate, writer, writes }, hashes:[…] } (read only).
|
|
97
|
+
* → { pass, reason, text, sentences, cues, hash, chars }
|
|
98
|
+
* reason: system | origin | untrusted | no-typed-text | trivial | short | no-cue | seen | gate-cap | write-cap | null
|
|
99
|
+
*/
|
|
100
|
+
export function prefilter(turn = {}, session = {}, limits = LEARN) {
|
|
101
|
+
const fail = (reason, extra = {}) => ({ pass: false, reason, text: '', sentences: [], cues: [], hash: null, chars: 0, ...extra });
|
|
102
|
+
if (turn.system) return fail('system');
|
|
103
|
+
if (turn.origin !== 'ui' && turn.origin !== 'whatsapp') return fail('origin');
|
|
104
|
+
if (turn.origin === 'whatsapp' && turn.untrusted === true) return fail('untrusted');
|
|
105
|
+
const text = typedEvidence(turn);
|
|
106
|
+
if (!text) return fail('no-typed-text');
|
|
107
|
+
if (isTrivialPrompt(text)) return fail('trivial', { chars: text.length });
|
|
108
|
+
if (text.length < limits.minChars) return fail('short', { chars: text.length });
|
|
109
|
+
const cues = matchCues(text);
|
|
110
|
+
if (!cues.length) return fail('no-cue', { chars: text.length });
|
|
111
|
+
const hash = contentHash(text);
|
|
112
|
+
if (Array.isArray(session.hashes) && session.hashes.includes(hash)) return fail('seen', { chars: text.length, cues, hash });
|
|
113
|
+
const counts = session.counts || {};
|
|
114
|
+
if ((Number(counts.gate) || 0) >= limits.gateCap) return fail('gate-cap', { chars: text.length, cues, hash });
|
|
115
|
+
if ((Number(counts.writes) || 0) >= limits.writeCap) return fail('write-cap', { chars: text.length, cues, hash });
|
|
116
|
+
const clipped = text.length > limits.messageClip ? `${text.slice(0, limits.messageClip - 1)}…` : text;
|
|
117
|
+
return { pass: true, reason: null, text: clipped, sentences: splitSentences(text, { max: limits.maxSentences, clip: limits.sentenceClip }), cues, hash, chars: text.length };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Jev questions ───────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
const noul = (instructions, criteria) => ({ type: 'noul', instructions, criteria });
|
|
123
|
+
const choice = (instructions, criteria) => ({ type: 'choice', instructions, criteria });
|
|
124
|
+
export const readNoul = (answer) => (answer?.type === 'noul' && Number.isFinite(answer.noul) ? answer.noul : null);
|
|
125
|
+
export const readChoice = (answer) => (answer?.type === 'choice' && typeof answer.choice === 'string'
|
|
126
|
+
? { choice: answer.choice, confidence: Number.isFinite(answer.confidence) ? answer.confidence : 0 } : null);
|
|
127
|
+
|
|
128
|
+
const FACET_CRITERIA = Object.freeze({
|
|
129
|
+
instructions: 'How to answer or act: language, format, length, steps, what to include or leave out.',
|
|
130
|
+
expectations: 'What the user expects of the work or the assistant: quality, thoroughness, initiative, how results are reported.',
|
|
131
|
+
decisions: 'A choice the user made that should stick: a tool, a library, an approach, a convention.',
|
|
132
|
+
corrections: 'The user corrects how the assistant behaves or repeats a request it ignored.',
|
|
133
|
+
expertise: 'What the user knows or does: role, skills, experience.',
|
|
134
|
+
rules: 'A hard limit: something that must always or must never happen.',
|
|
135
|
+
goals: 'What the user is trying to achieve beyond this request.',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The gate request. → { state, questions, labels:{ sentences:[label…], known:[label…] } }
|
|
140
|
+
* Questions that have nothing to choose from are left out (one sentence: no evidence question;
|
|
141
|
+
* no known statement: no match and no contradiction question).
|
|
142
|
+
*/
|
|
143
|
+
export function buildGateRequest({ text, sentences = [], previousReply = '', known = [], project = null } = {}, limits = LEARN) {
|
|
144
|
+
const sentenceLabels = sentences.map((_, index) => `s${index + 1}`);
|
|
145
|
+
const knownRows = known.slice(0, limits.knownStatements);
|
|
146
|
+
const knownLabels = knownRows.map((_, index) => `k${index + 1}`);
|
|
147
|
+
const state = {
|
|
148
|
+
message: String(text ?? ''),
|
|
149
|
+
sentences: Object.fromEntries(sentences.map((sentence, index) => [sentenceLabels[index], sentence])),
|
|
150
|
+
previous_reply: previousReply ? String(previousReply).slice(0, limits.replyClip) : null,
|
|
151
|
+
known: Object.fromEntries(knownRows.map((row, index) => [knownLabels[index], row.statement])),
|
|
152
|
+
project: project && project !== 'global' ? String(project) : null,
|
|
153
|
+
};
|
|
154
|
+
const questions = {
|
|
155
|
+
durable: noul('`message` is what a user typed to their AI assistant. `previous_reply` is only the assistant message it answers, for context. '
|
|
156
|
+
+ 'Does `message` say something about this user that should still hold in later conversations: a standing instruction, a lasting preference or expectation, '
|
|
157
|
+
+ 'a rule, a correction of how the assistant works, a decision meant to stick, their expertise, or a goal? Judge the meaning in any language.', {
|
|
158
|
+
true: 'A durable signal: "always answer in Portuguese", "never touch the dist folder", "I told you to stop adding comments", "from now on use pnpm", "I am a backend engineer".',
|
|
159
|
+
false: 'Only this request\'s content, a one-off instruction for the current task, a question, small talk, or text the user quotes rather than means.',
|
|
160
|
+
}),
|
|
161
|
+
facet: choice('What is the durable part of `message` about?', { ...FACET_CRITERIA }),
|
|
162
|
+
scope: choice('Does the durable part of `message` hold for all of the user\'s work, or only for the project named in `project`?', {
|
|
163
|
+
global: 'It is about the user in general, or no project is named.',
|
|
164
|
+
project: 'It only makes sense for this one project: its files, its tools, its conventions.',
|
|
165
|
+
}),
|
|
166
|
+
};
|
|
167
|
+
if (sentenceLabels.length > 1) {
|
|
168
|
+
questions.evidence = choice('Which entry of `sentences` carries the durable statement about the user?', Object.fromEntries(sentenceLabels.map((label) => [label, null])));
|
|
169
|
+
}
|
|
170
|
+
if (knownLabels.length) {
|
|
171
|
+
questions.match = choice('`known` lists statements already recorded about this user. Which one says the same thing as the durable part of `message`?', {
|
|
172
|
+
...Object.fromEntries(knownLabels.map((label) => [label, null])),
|
|
173
|
+
none: 'No statement in `known` says this.',
|
|
174
|
+
});
|
|
175
|
+
for (const label of knownLabels) {
|
|
176
|
+
questions[`contradicts_${label}`] = noul(`Does \`message\` contradict statement \`known.${label}\`: the user now wants the opposite, or says it no longer holds?`, {
|
|
177
|
+
true: 'A reversal: what the statement says is no longer what the user wants.',
|
|
178
|
+
false: 'The same, a refinement, or an unrelated matter.',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { state, questions, labels: { sentences: sentenceLabels, known: knownLabels } };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* What the answers mean, against the thresholds.
|
|
187
|
+
* → { durable, pass, facet, scope, sentence, sentenceIndex, match:{ row, confidence } | null,
|
|
188
|
+
* contradiction:{ row, probability } | null, contradictions:[{ id, probability }] }
|
|
189
|
+
*/
|
|
190
|
+
export function readGate(answers, { sentences = [], known = [], labels = { sentences: [], known: [] }, project = null, cues = [] } = {}, limits = LEARN) {
|
|
191
|
+
const durable = readNoul(answers?.durable);
|
|
192
|
+
const facetAnswer = readChoice(answers?.facet);
|
|
193
|
+
const scopeAnswer = readChoice(answers?.scope);
|
|
194
|
+
const evidenceAnswer = readChoice(answers?.evidence);
|
|
195
|
+
let sentenceIndex = evidenceAnswer ? labels.sentences.indexOf(evidenceAnswer.choice) : -1;
|
|
196
|
+
if (sentenceIndex < 0) sentenceIndex = sentences.findIndex((sentence) => matchCues(sentence).length);
|
|
197
|
+
if (sentenceIndex < 0) sentenceIndex = 0;
|
|
198
|
+
const sentence = sentences[sentenceIndex] || '';
|
|
199
|
+
const projectScoped = scopeAnswer?.choice === 'project' && scopeAnswer.confidence >= 0.6 && project && project !== 'global';
|
|
200
|
+
const contradictions = [];
|
|
201
|
+
labels.known.forEach((label, index) => {
|
|
202
|
+
const p = readNoul(answers?.[`contradicts_${label}`]);
|
|
203
|
+
if (p !== null && known[index]) contradictions.push({ row: known[index], probability: p });
|
|
204
|
+
});
|
|
205
|
+
const strongest = contradictions.filter((item) => item.probability >= limits.contradictMin).sort((a, b) => b.probability - a.probability)[0] || null;
|
|
206
|
+
const matchAnswer = readChoice(answers?.match);
|
|
207
|
+
const matchIndex = matchAnswer && matchAnswer.choice !== 'none' ? labels.known.indexOf(matchAnswer.choice) : -1;
|
|
208
|
+
const match = matchIndex >= 0 && matchAnswer.confidence >= limits.matchMin && known[matchIndex] ? { row: known[matchIndex], confidence: matchAnswer.confidence } : null;
|
|
209
|
+
return {
|
|
210
|
+
durable,
|
|
211
|
+
pass: durable !== null && durable >= limits.durableMin,
|
|
212
|
+
facet: facetAnswer && FACETS.includes(facetAnswer.choice) ? facetAnswer.choice : guessFacet(sentence),
|
|
213
|
+
scope: projectScoped ? String(project) : 'global',
|
|
214
|
+
sentence, sentenceIndex, cues,
|
|
215
|
+
match,
|
|
216
|
+
contradiction: strongest,
|
|
217
|
+
contradictions: contradictions.map((item) => ({ id: item.row.id, probability: item.probability })),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const FAITHFUL_QUESTION = noul('`statement` was written from `sentence`, which a user typed (and, when given, from `previous_statement`, already recorded about them). '
|
|
222
|
+
+ 'Does `statement` say only what they say about the user, adding no fact, reason, preference, scope or detail of its own and dropping no condition?', {
|
|
223
|
+
true: 'A faithful restatement in the third person.',
|
|
224
|
+
false: 'It adds, generalises, weakens or changes something.',
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const count = (value) => (Number.isFinite(Number(value)) && Number(value) > 0 ? Math.round(Number(value)) : 0);
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @param {object} deps
|
|
231
|
+
* @param {{ ask: Function, available?: Function } | null} deps.judge
|
|
232
|
+
* ask(state, questions, { sessionId, project, timeoutMs, onUsage, onLogged }) → answers | null
|
|
233
|
+
* @param {{ write: Function, available?: Function } | null} deps.writer
|
|
234
|
+
* @param {object} deps.store { known(text, { project, limit }), create(args), update(id, args), supersede(newId, oldId, { sessionId }) }
|
|
235
|
+
* @param {() => ({ input?: number, output?: number } | null)} [deps.rates] Jev's $ per million tokens
|
|
236
|
+
*/
|
|
237
|
+
export function createUserLearner({ judge = null, writer = null, store = null, rates = () => null, limits = LEARN, now = Date.now, log = () => {} } = {}) {
|
|
238
|
+
const today = () => new Date(now()).toISOString().slice(0, 10);
|
|
239
|
+
|
|
240
|
+
function judgeAvailable() {
|
|
241
|
+
if (!judge || typeof judge.ask !== 'function') return false;
|
|
242
|
+
try { return typeof judge.available === 'function' ? !!judge.available() : true; } catch { return false; }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* One captured turn → what was learned (always an object, for the trace).
|
|
247
|
+
* `capture`: { sessionId, project, text, sentences, cues, hash, previousReply };
|
|
248
|
+
* `session`: { counts:{ gate, writer, writes }, hashes:[…] } (updated in place).
|
|
249
|
+
*/
|
|
250
|
+
async function learn(capture = {}, session = { counts: {}, hashes: [] }) {
|
|
251
|
+
const counts = (session.counts ||= {});
|
|
252
|
+
const result = {
|
|
253
|
+
gate: null, writer: null, action: { type: 'none', reason: null },
|
|
254
|
+
cost: { jevRequests: 0, jevInputTokens: 0, jevOutputTokens: 0, jevUsd: 0, writerTokens: null, writerUsd: 0, totalUsd: 0 },
|
|
255
|
+
};
|
|
256
|
+
const finish = () => {
|
|
257
|
+
let rate = null;
|
|
258
|
+
try { rate = rates() || null; } catch { rate = null; }
|
|
259
|
+
const perMillion = (value, fallback) => (Number.isFinite(Number(value)) && Number(value) >= 0 ? Number(value) : fallback);
|
|
260
|
+
result.cost.jevUsd = Number(((result.cost.jevInputTokens * perMillion(rate?.input, 0.042) + result.cost.jevOutputTokens * perMillion(rate?.output, 0)) / 1e6).toFixed(8));
|
|
261
|
+
result.cost.totalUsd = Number((result.cost.jevUsd + (Number(result.cost.writerUsd) || 0)).toFixed(8));
|
|
262
|
+
return result;
|
|
263
|
+
};
|
|
264
|
+
const ask = async (state, questions) => {
|
|
265
|
+
const usage = { inputTokens: 0, outputTokens: 0, cached: false, ms: 0 };
|
|
266
|
+
let logId = null;
|
|
267
|
+
let answers = null;
|
|
268
|
+
try {
|
|
269
|
+
answers = await judge.ask(state, questions, {
|
|
270
|
+
sessionId: capture.sessionId || null, project: capture.project && capture.project !== 'global' ? capture.project : null, timeoutMs: limits.timeoutMs,
|
|
271
|
+
onUsage: (u) => { usage.inputTokens = count(u?.inputTokens); usage.outputTokens = count(u?.outputTokens); usage.cached = u?.cached === true; usage.ms = count(u?.latencyMs); },
|
|
272
|
+
onLogged: (id) => { logId = id ?? null; },
|
|
273
|
+
});
|
|
274
|
+
} catch { answers = null; }
|
|
275
|
+
if (!usage.cached) result.cost.jevRequests += 1;
|
|
276
|
+
result.cost.jevInputTokens += usage.inputTokens;
|
|
277
|
+
result.cost.jevOutputTokens += usage.outputTokens;
|
|
278
|
+
return { answers: answers && typeof answers === 'object' ? answers : null, usage, logId };
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
if (!store) { result.action.reason = 'no-store'; return finish(); }
|
|
282
|
+
if (!judgeAvailable()) { result.action.reason = 'jev-unavailable'; return finish(); }
|
|
283
|
+
if (!capture.text || !Array.isArray(capture.sentences) || !capture.sentences.length) { result.action.reason = 'no-typed-text'; return finish(); }
|
|
284
|
+
|
|
285
|
+
// The content hash is spent whether or not anything is learned: the same words are never judged twice.
|
|
286
|
+
if (capture.hash) session.hashes = [...(Array.isArray(session.hashes) ? session.hashes : []).filter((hash) => hash !== capture.hash), capture.hash].slice(-limits.hashesKept);
|
|
287
|
+
counts.gate = (Number(counts.gate) || 0) + 1;
|
|
288
|
+
|
|
289
|
+
let known = [];
|
|
290
|
+
try { known = (await store.known(capture.text, { project: capture.project || 'global', limit: limits.knownStatements })) || []; }
|
|
291
|
+
catch (error) { log('user-context:known-error', error?.message || String(error)); known = []; }
|
|
292
|
+
known = known.filter((row) => row?.id && row.statement).slice(0, limits.knownStatements);
|
|
293
|
+
|
|
294
|
+
const request = buildGateRequest({ text: capture.text, sentences: capture.sentences, previousReply: capture.previousReply, known, project: capture.project }, limits);
|
|
295
|
+
const asked = await ask(request.state, request.questions);
|
|
296
|
+
if (!asked.answers) {
|
|
297
|
+
result.gate = { asked: true, answered: false, logId: asked.logId, tokens: { input: asked.usage.inputTokens, output: asked.usage.outputTokens }, known: known.length };
|
|
298
|
+
result.action.reason = 'no-answer';
|
|
299
|
+
return finish();
|
|
300
|
+
}
|
|
301
|
+
const gate = readGate(asked.answers, { sentences: capture.sentences, known, labels: request.labels, project: capture.project, cues: capture.cues || [] }, limits);
|
|
302
|
+
result.gate = {
|
|
303
|
+
asked: true, answered: true, logId: asked.logId, ms: asked.usage.ms, cached: asked.usage.cached,
|
|
304
|
+
tokens: { input: asked.usage.inputTokens, output: asked.usage.outputTokens },
|
|
305
|
+
durable: gate.durable, pass: gate.pass, facet: gate.facet, scope: gate.scope, sentence: gate.sentence,
|
|
306
|
+
known: known.length,
|
|
307
|
+
match: gate.match ? { id: gate.match.row.id, statement: gate.match.row.statement, confidence: gate.match.confidence, seed: !gate.match.row.structured } : null,
|
|
308
|
+
contradicts: gate.contradiction ? { id: gate.contradiction.row.id, statement: gate.contradiction.row.statement, probability: gate.contradiction.probability } : null,
|
|
309
|
+
};
|
|
310
|
+
if (!gate.pass) { result.action.reason = gate.durable === null ? 'no-answer' : 'not-durable'; return finish(); }
|
|
311
|
+
if (!gate.sentence) { result.action.reason = 'no-evidence'; return finish(); }
|
|
312
|
+
if ((Number(counts.writes) || 0) >= limits.writeCap) { result.action.reason = 'write-cap'; return finish(); }
|
|
313
|
+
|
|
314
|
+
const target = gate.contradiction?.row || gate.match?.row || null;
|
|
315
|
+
const kind = gate.contradiction ? 'supersede' : (gate.match ? (target.structured ? 'update' : 'create') : 'create');
|
|
316
|
+
// An update refines the statement it matched; everything else starts from the user's sentence.
|
|
317
|
+
const refining = kind === 'update' ? target : null;
|
|
318
|
+
const own = cleanStatement(gate.sentence);
|
|
319
|
+
|
|
320
|
+
// ── the writer, checked ──
|
|
321
|
+
let statement = refining ? refining.statement : own;
|
|
322
|
+
let engine = refining ? 'kept' : 'user-sentence';
|
|
323
|
+
const writerInfo = { used: false, model: null, verified: null, reason: null, tokens: null, costUsd: 0, ms: 0 };
|
|
324
|
+
const writerOn = writer && typeof writer.write === 'function' && (typeof writer.available !== 'function' || writer.available());
|
|
325
|
+
if (!writerOn) writerInfo.reason = 'no-writer';
|
|
326
|
+
else if ((Number(counts.writer) || 0) >= limits.writerCap) writerInfo.reason = 'writer-cap';
|
|
327
|
+
else {
|
|
328
|
+
counts.writer = (Number(counts.writer) || 0) + 1;
|
|
329
|
+
writerInfo.used = true;
|
|
330
|
+
let drafted = null;
|
|
331
|
+
try { drafted = await writer.write({ sentence: gate.sentence, existing: refining ? refining.statement : null, sessionId: capture.sessionId || null, taskId: capture.taskId || null }); }
|
|
332
|
+
catch (error) { drafted = { statement: '', error: error?.message || String(error) }; }
|
|
333
|
+
writerInfo.model = drafted?.model || null;
|
|
334
|
+
writerInfo.tokens = drafted?.tokens || null;
|
|
335
|
+
writerInfo.costUsd = Number(drafted?.costUsd) || 0;
|
|
336
|
+
writerInfo.ms = count(drafted?.ms);
|
|
337
|
+
result.cost.writerTokens = writerInfo.tokens;
|
|
338
|
+
result.cost.writerUsd = writerInfo.costUsd;
|
|
339
|
+
if (!drafted?.statement) writerInfo.reason = drafted?.error || 'writer-failed';
|
|
340
|
+
else {
|
|
341
|
+
const checked = await ask({ sentence: gate.sentence, previous_statement: refining ? refining.statement : null, statement: drafted.statement }, { faithful: FAITHFUL_QUESTION });
|
|
342
|
+
const p = readNoul(checked.answers?.faithful);
|
|
343
|
+
writerInfo.verified = p;
|
|
344
|
+
writerInfo.draft = drafted.statement;
|
|
345
|
+
if (p !== null && p >= limits.faithfulMin) { statement = drafted.statement; engine = `claude-code/${drafted.model || 'claude'}`; }
|
|
346
|
+
else writerInfo.reason = p === null ? 'unverified' : 'not-faithful';
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
result.writer = writerInfo;
|
|
350
|
+
if (!statement || statement.length < 8) { result.action.reason = 'no-statement'; return finish(); }
|
|
351
|
+
|
|
352
|
+
// ── exactly one write ──
|
|
353
|
+
const evidence = { quote: cleanQuote(gate.sentence, limits.quoteMax), date: today() };
|
|
354
|
+
const action = { type: kind, reason: null, statement, evidence: evidence.quote, facet: gate.facet, scope: gate.scope, engine, memoryId: null };
|
|
355
|
+
try {
|
|
356
|
+
if (kind === 'update') {
|
|
357
|
+
const content = formatKnowledge({
|
|
358
|
+
statement, facet: target.facet,
|
|
359
|
+
evidence: addEvidence(target.evidence, evidence, limits.evidenceMax),
|
|
360
|
+
seen: (Number(target.seen) || 1) + 1,
|
|
361
|
+
confidence: raiseConfidence(target.confidence, gate.durable),
|
|
362
|
+
});
|
|
363
|
+
const updated = await store.update(target.id, { content, expectedRevision: target.revision || null, sessionId: capture.sessionId || null, project: target.project });
|
|
364
|
+
if (!updated?.ok) { action.type = 'none'; action.reason = updated?.error || 'update-failed'; }
|
|
365
|
+
else { action.memoryId = target.id; action.facet = target.facet; action.scope = target.project; action.seen = (Number(target.seen) || 1) + 1; }
|
|
366
|
+
} else {
|
|
367
|
+
// A matched seed counts as one earlier sighting; the new memory links to it and the seed leaves the digest.
|
|
368
|
+
const seed = kind === 'create' && gate.match && !target.structured ? target : null;
|
|
369
|
+
const content = formatKnowledge({
|
|
370
|
+
statement, facet: gate.facet, evidence: [evidence],
|
|
371
|
+
seen: seed ? 2 : 1,
|
|
372
|
+
confidence: seed ? raiseConfidence(Math.max(seed.confidence, 0.6), gate.durable) : Math.min(0.99, gate.durable),
|
|
373
|
+
});
|
|
374
|
+
const created = await store.create({
|
|
375
|
+
content, project: gate.scope, facet: gate.facet, kind: FACET_KIND[gate.facet] || 'preference',
|
|
376
|
+
sessionId: capture.sessionId || null, links: seed ? [seed.id] : [], hash: capture.hash || contentHash(capture.text),
|
|
377
|
+
});
|
|
378
|
+
if (!created?.id || created.duplicateOf) {
|
|
379
|
+
action.type = 'none';
|
|
380
|
+
action.reason = created?.duplicateOf ? 'duplicate' : (created?.error || 'create-failed');
|
|
381
|
+
if (created?.duplicateOf) action.duplicateOf = created.duplicateOf;
|
|
382
|
+
} else {
|
|
383
|
+
action.memoryId = created.id;
|
|
384
|
+
if (seed) action.seedId = seed.id;
|
|
385
|
+
if (kind === 'supersede') {
|
|
386
|
+
action.supersedes = target.id;
|
|
387
|
+
action.replaced = target.statement;
|
|
388
|
+
const linked = await store.supersede(created.id, target.id, { sessionId: capture.sessionId || null });
|
|
389
|
+
if (!linked?.ok) action.supersedeError = linked?.error || 'supersede-failed';
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} catch (error) {
|
|
394
|
+
log('user-context:write-error', error?.message || String(error));
|
|
395
|
+
action.type = 'none';
|
|
396
|
+
action.reason = 'write-error';
|
|
397
|
+
}
|
|
398
|
+
if (action.type !== 'none') counts.writes = (Number(counts.writes) || 0) + 1;
|
|
399
|
+
result.action = action;
|
|
400
|
+
return finish();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return { learn, available: judgeAvailable };
|
|
404
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun — Assistant threads (pure)
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// One memory per Assistant conversation (category `assistant-threads`, kind
|
|
6
|
+
// `session`), rewritten in place. Its content is assembled from what the
|
|
7
|
+
// runtime saw, never written by a model:
|
|
8
|
+
//
|
|
9
|
+
// Assistant conversation: Fix the login redirect
|
|
10
|
+
// Project synabun · 2026-10-11 21:31 → 22:05 UTC · 4 prompts
|
|
11
|
+
// Asks:
|
|
12
|
+
// - Fix the login redirect on Safari
|
|
13
|
+
// Runs:
|
|
14
|
+
// - Safari redirect fix (codex): done
|
|
15
|
+
// Open:
|
|
16
|
+
// - follow-up (Safari redirect fix): add a regression test
|
|
17
|
+
// Session assistant-1234
|
|
18
|
+
//
|
|
19
|
+
// Open items are a worker's follow_ups, a run that ended blocked, needs_input,
|
|
20
|
+
// failed or interrupted, and a card left unanswered. No I/O.
|
|
21
|
+
|
|
22
|
+
import { NUDGES, THREAD } from './assistant-user-context-budgets.js';
|
|
23
|
+
import { clipText } from './assistant-user-knowledge.js';
|
|
24
|
+
|
|
25
|
+
const HEAD = 'Assistant conversation: ';
|
|
26
|
+
const OPEN_OUTCOMES = new Set(['blocked', 'needs_input', 'failed', 'interrupted']);
|
|
27
|
+
|
|
28
|
+
const minute = (value) => {
|
|
29
|
+
const ms = Date.parse(value ?? '');
|
|
30
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString().slice(0, 16).replace('T', ' ') : null;
|
|
31
|
+
};
|
|
32
|
+
const oneLine = (value, max) => clipText(String(value ?? '').split(/\r?\n/).find((line) => line.trim()) || '', max);
|
|
33
|
+
|
|
34
|
+
/** A new working thread for a conversation. */
|
|
35
|
+
export function newThread({ sessionId, title = '', project = 'global', at = null } = {}) {
|
|
36
|
+
return { sessionId: String(sessionId || ''), memoryId: null, title: String(title || ''), project: String(project || 'global'), startedAt: at, lastAt: at, prompts: 0, asks: [], runs: {}, hash: '', writtenAt: null, dirty: false };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A typed prompt: its first line joins the asks (the last `THREAD.asks` are kept). */
|
|
40
|
+
export function noteAsk(thread, { text, at = null, title = null, project = null } = {}) {
|
|
41
|
+
const ask = oneLine(text, THREAD.askClip);
|
|
42
|
+
thread.prompts = (Number(thread.prompts) || 0) + 1;
|
|
43
|
+
if (ask) thread.asks = [...(thread.asks || []), { at, text: ask }].slice(-THREAD.asks);
|
|
44
|
+
if (at) { thread.startedAt ||= at; if (!thread.lastAt || at > thread.lastAt) thread.lastAt = at; }
|
|
45
|
+
if (title) thread.title = String(title);
|
|
46
|
+
if (project) thread.project = String(project);
|
|
47
|
+
thread.dirty = true;
|
|
48
|
+
return thread;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The outcome word of a dispatch event: the worker's own status, else the run's, else the event's reason. */
|
|
52
|
+
export function runOutcome(run = {}, reason = '') {
|
|
53
|
+
const status = String(run.lastResult?.status || '').toLowerCase();
|
|
54
|
+
if (reason === 'failed' || reason === 'interrupted') return reason;
|
|
55
|
+
if (reason === 'stopped') return 'stopped';
|
|
56
|
+
if (status) return status;
|
|
57
|
+
const outcome = String(run.outcome || '').toLowerCase();
|
|
58
|
+
if (outcome) return outcome;
|
|
59
|
+
return reason === 'completed' ? 'done' : String(run.status || reason || 'running').toLowerCase();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A dispatch event of the conversation: the run's title, outcome, follow-ups and question. */
|
|
63
|
+
export function noteRun(thread, run = {}, reason = '', at = null) {
|
|
64
|
+
if (!run?.runId) return thread;
|
|
65
|
+
const id = String(run.runId);
|
|
66
|
+
const previous = thread.runs?.[id] || {};
|
|
67
|
+
const followUps = Array.isArray(run.lastResult?.follow_ups)
|
|
68
|
+
? run.lastResult.follow_ups.map((item) => oneLine(item, THREAD.itemClip)).filter((item) => item && !/^none\.?$/i.test(item)).slice(0, 4)
|
|
69
|
+
: previous.followUps || [];
|
|
70
|
+
const next = {
|
|
71
|
+
title: oneLine(run.title || run.task || previous.title || id.slice(0, 8), 80),
|
|
72
|
+
provider: run.provider ? String(run.provider) : previous.provider || null,
|
|
73
|
+
outcome: runOutcome(run, reason),
|
|
74
|
+
followUps,
|
|
75
|
+
note: oneLine(run.lastResult?.question || run.error || previous.note || '', THREAD.itemClip),
|
|
76
|
+
at: at || previous.at || null,
|
|
77
|
+
};
|
|
78
|
+
if (JSON.stringify(previous) === JSON.stringify(next)) return thread;
|
|
79
|
+
thread.runs = { ...(thread.runs || {}), [id]: next };
|
|
80
|
+
// Bounded: the oldest runs go first.
|
|
81
|
+
const ids = Object.keys(thread.runs);
|
|
82
|
+
for (const old of ids.slice(0, Math.max(0, ids.length - THREAD.runs * 3))) delete thread.runs[old];
|
|
83
|
+
if (at && (!thread.lastAt || at > thread.lastAt)) thread.lastAt = at;
|
|
84
|
+
thread.dirty = true;
|
|
85
|
+
return thread;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A run that left the conversation (cleared by the user): it is not an open item any more. */
|
|
89
|
+
export function dropRun(thread, runId) {
|
|
90
|
+
if (!thread?.runs?.[String(runId)]) return thread;
|
|
91
|
+
const next = { ...thread.runs };
|
|
92
|
+
delete next[String(runId)];
|
|
93
|
+
thread.runs = next;
|
|
94
|
+
thread.dirty = true;
|
|
95
|
+
return thread;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* What is still open: [{ key, kind, text }] with stable keys, newest runs first.
|
|
100
|
+
* `cards`: the cards left unanswered right now ([{ id, kind, title }]).
|
|
101
|
+
*/
|
|
102
|
+
export function openItems(thread, { cards = [] } = {}) {
|
|
103
|
+
const items = [];
|
|
104
|
+
const runs = Object.entries(thread?.runs || {}).reverse();
|
|
105
|
+
for (const [id, run] of runs) {
|
|
106
|
+
if (OPEN_OUTCOMES.has(run.outcome)) {
|
|
107
|
+
items.push({ key: `run:${id}:${run.outcome}`, kind: run.outcome, text: clipText(`${run.outcome.replace('_', ' ')}: ${run.title}${run.note ? ` (${run.note})` : ''}`, THREAD.itemClip) });
|
|
108
|
+
}
|
|
109
|
+
(run.followUps || []).forEach((text, index) => {
|
|
110
|
+
items.push({ key: `follow:${id}:${index}`, kind: 'follow_up', text: clipText(`follow-up (${run.title}): ${text}`, THREAD.itemClip) });
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
for (const card of Array.isArray(cards) ? cards : []) {
|
|
114
|
+
if (!card?.id) continue;
|
|
115
|
+
items.push({ key: `card:${card.id}`, kind: 'card', text: clipText(`unanswered ${card.kind || 'question'} card${card.title ? `: ${card.title}` : ''}`, THREAD.itemClip) });
|
|
116
|
+
}
|
|
117
|
+
return items.slice(0, THREAD.openItems);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The memory content. Deterministic: the same thread and cards give the same text. */
|
|
121
|
+
export function threadContent(thread, { cards = [] } = {}) {
|
|
122
|
+
const lines = [`${HEAD}${clipText(thread.title || 'Untitled', 80)}`];
|
|
123
|
+
const start = minute(thread.startedAt);
|
|
124
|
+
const end = minute(thread.lastAt);
|
|
125
|
+
const span = start ? (end && end !== start ? `${start} → ${end.slice(0, 10) === start.slice(0, 10) ? end.slice(11) : end} UTC` : `${start} UTC`) : null;
|
|
126
|
+
const prompts = Number(thread.prompts) || 0;
|
|
127
|
+
lines.push([`Project ${thread.project || 'global'}`, span, `${prompts} ${prompts === 1 ? 'prompt' : 'prompts'}`].filter(Boolean).join(' · '));
|
|
128
|
+
if (thread.asks?.length) { lines.push('Asks:'); for (const ask of thread.asks.slice(-THREAD.asks)) lines.push(`- ${ask.text}`); }
|
|
129
|
+
const runs = Object.values(thread.runs || {}).slice(-THREAD.runs);
|
|
130
|
+
if (runs.length) { lines.push('Runs:'); for (const run of runs) lines.push(`- ${run.title}${run.provider ? ` (${run.provider})` : ''}: ${String(run.outcome || 'running').replace('_', ' ')}`); }
|
|
131
|
+
const open = openItems(thread, { cards });
|
|
132
|
+
if (open.length) { lines.push('Open:'); for (const item of open) lines.push(`- ${item.text}`); }
|
|
133
|
+
lines.push(`Session ${thread.sessionId}`);
|
|
134
|
+
return lines.join('\n');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** A thread memory's content read back: { title, project, endedAt, prompts, asks, runs, open, sessionId } or null. */
|
|
138
|
+
export function parseThread(content, { updatedAt = null } = {}) {
|
|
139
|
+
const lines = String(content ?? '').split(/\r?\n/).map((line) => line.trimEnd());
|
|
140
|
+
if (!lines[0]?.startsWith(HEAD)) return null;
|
|
141
|
+
const out = { title: lines[0].slice(HEAD.length).trim(), project: 'global', startedAt: null, endedAt: updatedAt, prompts: 0, asks: [], runs: [], open: [], sessionId: null };
|
|
142
|
+
let section = null;
|
|
143
|
+
for (const line of lines.slice(1)) {
|
|
144
|
+
if (line.startsWith('Project ')) {
|
|
145
|
+
const parts = line.split(' · ');
|
|
146
|
+
out.project = parts[0].slice('Project '.length).trim() || 'global';
|
|
147
|
+
const span = parts.find((part) => /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(part));
|
|
148
|
+
if (span) {
|
|
149
|
+
const [from, to] = span.replace(/ UTC$/, '').split(' → ');
|
|
150
|
+
out.startedAt = `${from.replace(' ', 'T')}:00.000Z`;
|
|
151
|
+
const end = to ? (to.length <= 5 ? `${from.slice(0, 10)} ${to}` : to) : from;
|
|
152
|
+
out.endedAt = `${end.replace(' ', 'T')}:00.000Z`;
|
|
153
|
+
}
|
|
154
|
+
const count = parts.find((part) => /^\d+ prompts?$/.test(part));
|
|
155
|
+
if (count) out.prompts = Number.parseInt(count, 10) || 0;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (line === 'Asks:' || line === 'Runs:' || line === 'Open:') { section = line.slice(0, -1).toLowerCase(); continue; }
|
|
159
|
+
if (line.startsWith('Session ')) { out.sessionId = line.slice('Session '.length).trim() || null; section = null; continue; }
|
|
160
|
+
if (line.startsWith('- ') && section) (section === 'open' ? out.open : out[section]).push(line.slice(2).trim());
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** "5 min ago", "3 h ago", "2 days ago". */
|
|
166
|
+
export function ago(fromMs, now = Date.now()) {
|
|
167
|
+
const minutes = Math.max(0, Math.round((now - fromMs) / 60_000));
|
|
168
|
+
if (minutes < 2) return 'just now';
|
|
169
|
+
if (minutes < 90) return `${minutes} min ago`;
|
|
170
|
+
const hours = Math.round(minutes / 60);
|
|
171
|
+
if (hours < 36) return `${hours} h ago`;
|
|
172
|
+
return `${Math.round(hours / 24)} days ago`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Is a past thread worth a word at session open: the same project, or recent enough? */
|
|
176
|
+
export function threadIsRecent(parsed, { project = 'global', now = Date.now(), hours = NUDGES.lastThreadHours } = {}) {
|
|
177
|
+
if (!parsed) return false;
|
|
178
|
+
const same = String(parsed.project || 'global').toLowerCase() === String(project || 'global').toLowerCase() && String(project || 'global').toLowerCase() !== 'global';
|
|
179
|
+
const ended = Date.parse(parsed.endedAt ?? '');
|
|
180
|
+
const recent = Number.isFinite(ended) && now - ended <= hours * 3_600_000;
|
|
181
|
+
return same || recent;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The session-open line for the last thread. `open`: the open item texts still to be said. */
|
|
185
|
+
export function lastThreadLine(parsed, { now = Date.now(), open = parsed?.open || [], shown = NUDGES.openItemsShown } = {}) {
|
|
186
|
+
if (!parsed?.title) return '';
|
|
187
|
+
const ended = Date.parse(parsed.endedAt ?? '');
|
|
188
|
+
const when = Number.isFinite(ended) ? ago(ended, now) : null;
|
|
189
|
+
const where = parsed.project && parsed.project !== 'global' ? parsed.project : null;
|
|
190
|
+
const head = `Last conversation${when || where ? ` (${[when, where].filter(Boolean).join(', ')})` : ''}: "${clipText(parsed.title, 70)}".`;
|
|
191
|
+
const items = (Array.isArray(open) ? open : []).slice(0, shown);
|
|
192
|
+
return items.length ? `${head} Still open: ${items.join('; ')}.` : head;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const STOPWORDS = new Set(('about after again also because been before being could does doing done each from have having here into just more most much only other over same should some such than that their them then there these they this those through under until very want were what when where which while will with would your yours '
|
|
196
|
+
+ 'para como mais muito também quando onde porque sobre depois antes ainda esse essa isso este esta isto aquele aquela pelo pela pelos pelas você voce vocês nosso nossa então entao fazer feito pode podem precisa').split(' '));
|
|
197
|
+
|
|
198
|
+
/** The significant words of a text: lowercase, four letters or more, no stopwords. */
|
|
199
|
+
export function significantWords(text) {
|
|
200
|
+
const words = String(text ?? '').toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((word) => word.length >= 4 && !STOPWORDS.has(word));
|
|
201
|
+
return new Set(words);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** How many significant words two texts share. */
|
|
205
|
+
export function overlap(a, b) {
|
|
206
|
+
const left = significantWords(a);
|
|
207
|
+
let shared = 0;
|
|
208
|
+
for (const word of significantWords(b)) if (left.has(word)) shared += 1;
|
|
209
|
+
return shared;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Unicode-aware word edges: \b does not see "é" or "ó" as letters.
|
|
213
|
+
const EDGE_L = '(?<![\\p{L}\\p{N}])';
|
|
214
|
+
const EDGE_R = '(?![\\p{L}\\p{N}])';
|
|
215
|
+
const CLOSING_RE = new RegExp(`${EDGE_L}(?:${[
|
|
216
|
+
"that(?:'s| is|s) (?:all|it)", 'all for (?:now|today|tonight)', 'done for (?:now|today|the day)', 'good ?night',
|
|
217
|
+
'see you', 'bye', 'wrap(?:ping)? (?:it |this |things )?up', "we(?:'re| are) done", 'call it a (?:day|night)', 'signing off',
|
|
218
|
+
'(?:é|e) isso', 'por hoje (?:é|e) s[óo]', 's[óo] isso', 'era isso', 'boa noite', 'at[ée] (?:amanh[ãa]|mais|logo)',
|
|
219
|
+
'tchau', 'por hoje chega', 'terminamos', 'encerr(?:ar|amos|ando)',
|
|
220
|
+
].join('|')})${EDGE_R}`, 'iu');
|
|
221
|
+
|
|
222
|
+
/** A turn that closes the conversation ("that's all for today", "por hoje é só"). */
|
|
223
|
+
export function isClosingTurn(prompt) {
|
|
224
|
+
const text = String(prompt ?? '').trim();
|
|
225
|
+
return text.length <= 200 && CLOSING_RE.test(text);
|
|
226
|
+
}
|