@clawpump/claw-agent 0.1.4 → 0.1.6
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/agent/.dockerignore +67 -0
- package/agent/.envrc +1 -1
- package/agent/.gitattributes +8 -0
- package/agent/AGENTS.md +216 -4
- package/agent/CONTRIBUTING.md +46 -8
- package/agent/Dockerfile +78 -35
- package/agent/MANIFEST.in +2 -0
- package/agent/README.md +12 -5
- package/agent/README.ur-pk.md +261 -0
- package/agent/README.zh-CN.md +11 -8
- package/agent/SECURITY.md +5 -4
- package/agent/acp_adapter/provenance.py +127 -0
- package/agent/acp_adapter/server.py +112 -5
- package/agent/acp_adapter/session.py +1 -6
- package/agent/acp_registry/agent.json +2 -2
- package/agent/agent/account_usage.py +313 -1
- package/agent/agent/agent_init.py +140 -37
- package/agent/agent/agent_runtime_helpers.py +342 -83
- package/agent/agent/anthropic_adapter.py +320 -33
- package/agent/agent/auxiliary_client.py +525 -105
- package/agent/agent/background_review.py +157 -19
- package/agent/agent/bedrock_adapter.py +71 -6
- package/agent/agent/billing_view.py +295 -0
- package/agent/agent/chat_completion_helpers.py +229 -4
- package/agent/agent/codex_responses_adapter.py +86 -10
- package/agent/agent/codex_runtime.py +153 -1
- package/agent/agent/coding_context.py +738 -0
- package/agent/agent/context_compressor.py +392 -44
- package/agent/agent/context_references.py +34 -1
- package/agent/agent/conversation_compression.py +159 -22
- package/agent/agent/conversation_loop.py +643 -908
- package/agent/agent/copilot_acp_client.py +4 -11
- package/agent/agent/credential_pool.py +5 -3
- package/agent/agent/credits_tracker.py +794 -0
- package/agent/agent/curator.py +91 -18
- package/agent/agent/curator_backup.py +26 -10
- package/agent/agent/display.py +42 -1
- package/agent/agent/error_classifier.py +52 -3
- package/agent/agent/errors.py +3 -0
- package/agent/agent/file_safety.py +0 -17
- package/agent/agent/gemini_native_adapter.py +31 -1
- package/agent/agent/i18n.py +48 -4
- package/agent/agent/image_gen_provider.py +74 -5
- package/agent/agent/image_routing.py +29 -0
- package/agent/agent/insights.py +8 -17
- package/agent/agent/lsp/install.py +3 -0
- package/agent/agent/memory_manager.py +326 -31
- package/agent/agent/message_content.py +50 -0
- package/agent/agent/model_metadata.py +214 -3
- package/agent/agent/moonshot_schema.py +8 -1
- package/agent/agent/onboarding.py +60 -0
- package/agent/agent/prompt_builder.py +327 -37
- package/agent/agent/redact.py +1 -0
- package/agent/agent/runtime_cwd.py +34 -5
- package/agent/agent/secret_scope.py +205 -0
- package/agent/agent/secret_sources/bitwarden.py +34 -2
- package/agent/agent/skill_commands.py +90 -1
- package/agent/agent/skill_preprocessing.py +1 -0
- package/agent/agent/skill_utils.py +209 -36
- package/agent/agent/ssl_guard.py +94 -0
- package/agent/agent/system_prompt.py +133 -5
- package/agent/agent/tool_executor.py +496 -70
- package/agent/agent/transports/anthropic.py +83 -21
- package/agent/agent/transports/chat_completions.py +94 -5
- package/agent/agent/transports/codex.py +67 -2
- package/agent/agent/transports/codex_app_server.py +1 -0
- package/agent/agent/transports/codex_app_server_session.py +30 -0
- package/agent/agent/transports/types.py +12 -0
- package/agent/agent/turn_context.py +408 -0
- package/agent/agent/turn_finalizer.py +428 -0
- package/agent/agent/turn_retry_state.py +68 -0
- package/agent/agent/usage_pricing.py +3 -0
- package/agent/apps/bootstrap-installer/package.json +6 -5
- package/agent/apps/bootstrap-installer/src/routes/failure.tsx +12 -5
- package/agent/apps/bootstrap-installer/src/routes/progress.tsx +1 -3
- package/agent/apps/bootstrap-installer/src/store.ts +3 -2
- package/agent/apps/bootstrap-installer/src-tauri/src/bootstrap.rs +172 -7
- package/agent/apps/bootstrap-installer/src-tauri/src/events.rs +14 -1
- package/agent/apps/bootstrap-installer/src-tauri/src/paths.rs +29 -0
- package/agent/apps/bootstrap-installer/src-tauri/src/powershell.rs +93 -3
- package/agent/apps/bootstrap-installer/src-tauri/src/update.rs +695 -39
- package/agent/apps/bootstrap-installer/tsconfig.json +3 -4
- package/agent/apps/desktop/DESIGN.md +167 -0
- package/agent/apps/desktop/README.md +20 -16
- package/agent/apps/desktop/assets/icon.icns +0 -0
- package/agent/apps/desktop/assets/icon.ico +0 -0
- package/agent/apps/desktop/assets/icon.png +0 -0
- package/agent/apps/desktop/electron/backend-env.cjs +112 -0
- package/agent/apps/desktop/electron/backend-env.test.cjs +111 -0
- package/agent/apps/desktop/electron/backend-probes.test.cjs +3 -1
- package/agent/apps/desktop/electron/backend-ready.cjs +66 -0
- package/agent/apps/desktop/electron/bootstrap-platform.cjs +52 -0
- package/agent/apps/desktop/electron/bootstrap-platform.test.cjs +59 -1
- package/agent/apps/desktop/electron/bootstrap-runner.cjs +176 -38
- package/agent/apps/desktop/electron/bootstrap-runner.test.cjs +112 -1
- package/agent/apps/desktop/electron/connection-config.cjs +288 -0
- package/agent/apps/desktop/electron/connection-config.test.cjs +396 -0
- package/agent/apps/desktop/electron/dashboard-token.cjs +99 -0
- package/agent/apps/desktop/electron/dashboard-token.test.cjs +142 -0
- package/agent/apps/desktop/electron/desktop-uninstall.cjs +232 -0
- package/agent/apps/desktop/electron/desktop-uninstall.test.cjs +246 -0
- package/agent/apps/desktop/electron/entitlements.mac.inherit.plist +2 -0
- package/agent/apps/desktop/electron/fs-read-dir.cjs +109 -0
- package/agent/apps/desktop/electron/fs-read-dir.test.cjs +364 -0
- package/agent/apps/desktop/electron/gateway-ws-probe.cjs +188 -0
- package/agent/apps/desktop/electron/gateway-ws-probe.test.cjs +122 -0
- package/agent/apps/desktop/electron/git-root.cjs +54 -0
- package/agent/apps/desktop/electron/git-root.test.cjs +40 -0
- package/agent/apps/desktop/electron/git-worktrees.cjs +174 -0
- package/agent/apps/desktop/electron/hardening.cjs +123 -28
- package/agent/apps/desktop/electron/hardening.test.cjs +163 -0
- package/agent/apps/desktop/electron/main.cjs +3121 -331
- package/agent/apps/desktop/electron/oauth-net-request.cjs +20 -0
- package/agent/apps/desktop/electron/oauth-net-request.test.cjs +34 -0
- package/agent/apps/desktop/electron/preload.cjs +52 -2
- package/agent/apps/desktop/electron/session-windows.cjs +124 -0
- package/agent/apps/desktop/electron/session-windows.test.cjs +199 -0
- package/agent/apps/desktop/electron/update-rebuild.cjs +29 -0
- package/agent/apps/desktop/electron/update-rebuild.test.cjs +55 -0
- package/agent/apps/desktop/electron/update-remote.cjs +56 -0
- package/agent/apps/desktop/electron/update-remote.test.cjs +78 -0
- package/agent/apps/desktop/electron/vscode-marketplace.cjs +331 -0
- package/agent/apps/desktop/electron/vscode-marketplace.test.cjs +113 -0
- package/agent/apps/desktop/electron/windows-child-process.test.cjs +57 -0
- package/agent/apps/desktop/electron/windows-user-env.cjs +76 -0
- package/agent/apps/desktop/electron/windows-user-env.test.cjs +90 -0
- package/agent/apps/desktop/electron/workspace-cwd.cjs +38 -0
- package/agent/apps/desktop/electron/workspace-cwd.test.cjs +45 -0
- package/agent/apps/desktop/eslint.config.mjs +0 -3
- package/agent/apps/desktop/index.html +27 -2
- package/agent/apps/desktop/package.json +31 -11
- package/agent/apps/desktop/pr-assets/session-source-folders.png +0 -0
- package/agent/apps/desktop/public/apple-touch-icon.png +0 -0
- package/agent/apps/desktop/public/nous-girl.jpg +0 -0
- package/agent/apps/desktop/scripts/assert-dist-built.cjs +70 -0
- package/agent/apps/desktop/scripts/assert-dist-built.test.cjs +84 -0
- package/agent/apps/desktop/scripts/before-pack.cjs +78 -0
- package/agent/apps/desktop/scripts/before-pack.test.cjs +53 -0
- package/agent/apps/desktop/scripts/diag-scroll-reset.mjs +229 -0
- package/agent/apps/desktop/scripts/patch-electron-builder-mac-binary.cjs +64 -0
- package/agent/apps/desktop/scripts/run-electron-builder.cjs +57 -0
- package/agent/apps/desktop/src/app/agents/index.tsx +53 -45
- package/agent/apps/desktop/src/app/artifacts/index.tsx +102 -83
- package/agent/apps/desktop/src/app/chat/chat-drop-overlay.tsx +29 -8
- package/agent/apps/desktop/src/app/chat/chat-swap-overlay.tsx +47 -0
- package/agent/apps/desktop/src/app/chat/composer/attachments.tsx +81 -45
- package/agent/apps/desktop/src/app/chat/composer/completion-drawer.tsx +13 -24
- package/agent/apps/desktop/src/app/chat/composer/context-menu.tsx +138 -88
- package/agent/apps/desktop/src/app/chat/composer/controls.tsx +138 -90
- package/agent/apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
- package/agent/apps/desktop/src/app/chat/composer/focus.ts +32 -0
- package/agent/apps/desktop/src/app/chat/composer/help-hint.tsx +38 -25
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-live-completion-adapter.ts +7 -0
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-mic-recorder.ts +22 -12
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts +142 -14
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-conversation.ts +14 -11
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-recorder.ts +9 -6
- package/agent/apps/desktop/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
- package/agent/apps/desktop/src/app/chat/composer/index.tsx +930 -180
- package/agent/apps/desktop/src/app/chat/composer/inline-refs.ts +136 -32
- package/agent/apps/desktop/src/app/chat/composer/model-pill.tsx +86 -0
- package/agent/apps/desktop/src/app/chat/composer/queue-panel.tsx +54 -75
- package/agent/apps/desktop/src/app/chat/composer/rich-editor.test.ts +117 -1
- package/agent/apps/desktop/src/app/chat/composer/rich-editor.ts +117 -6
- package/agent/apps/desktop/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
- package/agent/apps/desktop/src/app/chat/composer/status-stack/index.tsx +202 -0
- package/agent/apps/desktop/src/app/chat/composer/status-stack/status-row.tsx +155 -0
- package/agent/apps/desktop/src/app/chat/composer/text-utils.test.ts +104 -0
- package/agent/apps/desktop/src/app/chat/composer/text-utils.ts +37 -9
- package/agent/apps/desktop/src/app/chat/composer/trigger-popover.test.tsx +50 -0
- package/agent/apps/desktop/src/app/chat/composer/trigger-popover.tsx +105 -40
- package/agent/apps/desktop/src/app/chat/composer/types.ts +5 -0
- package/agent/apps/desktop/src/app/chat/composer/url-dialog.tsx +11 -15
- package/agent/apps/desktop/src/app/chat/composer/voice-activity.tsx +8 -4
- package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
- package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.ts +70 -16
- package/agent/apps/desktop/src/app/chat/hooks/use-file-drop-zone.ts +52 -16
- package/agent/apps/desktop/src/app/chat/index.tsx +234 -81
- package/agent/apps/desktop/src/app/chat/perf-probe.tsx +69 -21
- package/agent/apps/desktop/src/app/chat/right-rail/preview-console.tsx +44 -40
- package/agent/apps/desktop/src/app/chat/right-rail/preview-file.tsx +71 -25
- package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx +40 -1
- package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.tsx +55 -53
- package/agent/apps/desktop/src/app/chat/right-rail/preview.tsx +35 -17
- package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
- package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.tsx +74 -0
- package/agent/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx +356 -0
- package/agent/apps/desktop/src/app/chat/sidebar/index.tsx +1189 -364
- package/agent/apps/desktop/src/app/chat/sidebar/load-more-row.tsx +30 -0
- package/agent/apps/desktop/src/app/chat/sidebar/order.test.ts +21 -0
- package/agent/apps/desktop/src/app/chat/sidebar/order.ts +17 -0
- package/agent/apps/desktop/src/app/chat/sidebar/profile-switcher.tsx +524 -0
- package/agent/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx +80 -45
- package/agent/apps/desktop/src/app/chat/sidebar/session-row.tsx +120 -25
- package/agent/apps/desktop/src/app/chat/sidebar/virtual-session-list.tsx +7 -13
- package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
- package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.ts +326 -0
- package/agent/apps/desktop/src/app/chat/thread-loading.ts +7 -2
- package/agent/apps/desktop/src/app/command-center/index.tsx +320 -581
- package/agent/apps/desktop/src/app/command-palette/index.tsx +681 -0
- package/agent/apps/desktop/src/app/command-palette/marketplace-theme-page.tsx +157 -0
- package/agent/apps/desktop/src/app/cron/index.tsx +392 -324
- package/agent/apps/desktop/src/app/cron/job-state.ts +29 -0
- package/agent/apps/desktop/src/app/desktop-controller.tsx +618 -123
- package/agent/apps/desktop/src/app/floating-hud.ts +22 -0
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts +260 -14
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts +48 -4
- package/agent/apps/desktop/src/app/hooks/use-keybinds.ts +270 -0
- package/agent/apps/desktop/src/app/hooks/use-refresh-hotkey.ts +45 -0
- package/agent/apps/desktop/src/app/layout-constants.ts +19 -0
- package/agent/apps/desktop/src/app/messaging/index.tsx +136 -241
- package/agent/apps/desktop/src/app/messaging/platform-icon.tsx +95 -0
- package/agent/apps/desktop/src/app/model-visibility-overlay.tsx +31 -0
- package/agent/apps/desktop/src/app/overlays/overlay-search-input.tsx +18 -62
- package/agent/apps/desktop/src/app/overlays/overlay-split-layout.tsx +59 -7
- package/agent/apps/desktop/src/app/overlays/overlay-view.tsx +9 -5
- package/agent/apps/desktop/src/app/page-search-shell.tsx +42 -20
- package/agent/apps/desktop/src/app/profiles/create-profile-dialog.tsx +165 -0
- package/agent/apps/desktop/src/app/profiles/delete-profile-dialog.tsx +65 -0
- package/agent/apps/desktop/src/app/profiles/index.tsx +174 -199
- package/agent/apps/desktop/src/app/profiles/rename-profile-dialog.tsx +125 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/dnd-manager.ts +27 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/ipc.test.ts +100 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/ipc.ts +12 -18
- package/agent/apps/desktop/src/app/right-sidebar/files/remote-picker.tsx +177 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/tree.tsx +35 -21
- package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.test.ts +75 -3
- package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.ts +152 -5
- package/agent/apps/desktop/src/app/right-sidebar/index.test.tsx +75 -0
- package/agent/apps/desktop/src/app/right-sidebar/index.tsx +166 -129
- package/agent/apps/desktop/src/app/right-sidebar/store.ts +19 -4
- package/agent/apps/desktop/src/app/right-sidebar/terminal/buffer.ts +65 -0
- package/agent/apps/desktop/src/app/right-sidebar/terminal/index.tsx +29 -34
- package/agent/apps/desktop/src/app/right-sidebar/terminal/persistent.tsx +18 -6
- package/agent/apps/desktop/src/app/right-sidebar/terminal/selection.ts +93 -32
- package/agent/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts +381 -119
- package/agent/apps/desktop/src/app/routes.ts +9 -0
- package/agent/apps/desktop/src/app/session/hooks/use-cwd-actions.ts +17 -7
- package/agent/apps/desktop/src/app/session/hooks/use-message-stream.ts +365 -47
- package/agent/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx +198 -0
- package/agent/apps/desktop/src/app/session/hooks/use-model-controls.ts +70 -34
- package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx +1061 -0
- package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.ts +1143 -165
- package/agent/apps/desktop/src/app/session/hooks/use-route-resume.test.tsx +341 -2
- package/agent/apps/desktop/src/app/session/hooks/use-route-resume.ts +176 -5
- package/agent/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx +259 -0
- package/agent/apps/desktop/src/app/session/hooks/use-session-actions.ts +452 -149
- package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.test.tsx +327 -0
- package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.ts +133 -4
- package/agent/apps/desktop/src/app/session-picker-overlay.tsx +32 -0
- package/agent/apps/desktop/src/app/session-switcher.tsx +107 -0
- package/agent/apps/desktop/src/app/settings/about-settings.tsx +45 -36
- package/agent/apps/desktop/src/app/settings/appearance-settings.tsx +243 -162
- package/agent/apps/desktop/src/app/settings/config-settings.tsx +86 -66
- package/agent/apps/desktop/src/app/settings/constants.ts +459 -122
- package/agent/apps/desktop/src/app/settings/credential-key-ui.tsx +373 -0
- package/agent/apps/desktop/src/app/settings/env-credentials.tsx +198 -0
- package/agent/apps/desktop/src/app/settings/env-var-actions-menu.tsx +136 -0
- package/agent/apps/desktop/src/app/settings/field-copy.ts +56 -0
- package/agent/apps/desktop/src/app/settings/gateway-settings.tsx +385 -72
- package/agent/apps/desktop/src/app/settings/helpers.test.ts +156 -1
- package/agent/apps/desktop/src/app/settings/helpers.ts +30 -2
- package/agent/apps/desktop/src/app/settings/index.tsx +118 -84
- package/agent/apps/desktop/src/app/settings/keys-settings.tsx +62 -419
- package/agent/apps/desktop/src/app/settings/mcp-settings.tsx +65 -60
- package/agent/apps/desktop/src/app/settings/model-settings.test.tsx +129 -5
- package/agent/apps/desktop/src/app/settings/model-settings.tsx +370 -65
- package/agent/apps/desktop/src/app/settings/notifications-settings.tsx +150 -0
- package/agent/apps/desktop/src/app/settings/primitives.tsx +5 -11
- package/agent/apps/desktop/src/app/settings/provider-config-panel.test.tsx +142 -0
- package/agent/apps/desktop/src/app/settings/provider-config-panel.tsx +182 -0
- package/agent/apps/desktop/src/app/settings/providers-settings.test.tsx +171 -0
- package/agent/apps/desktop/src/app/settings/providers-settings.tsx +471 -0
- package/agent/apps/desktop/src/app/settings/sessions-settings.tsx +183 -71
- package/agent/apps/desktop/src/app/settings/toolset-config-panel.test.tsx +135 -1
- package/agent/apps/desktop/src/app/settings/toolset-config-panel.tsx +180 -57
- package/agent/apps/desktop/src/app/settings/types.ts +9 -6
- package/agent/apps/desktop/src/app/settings/uninstall-section.tsx +185 -0
- package/agent/apps/desktop/src/app/settings/use-deep-link-highlight.ts +60 -0
- package/agent/apps/desktop/src/app/shell/app-shell.tsx +59 -13
- package/agent/apps/desktop/src/app/shell/gateway-menu-panel.tsx +37 -32
- package/agent/apps/desktop/src/app/shell/hooks/use-overlay-routing.ts +6 -3
- package/agent/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +212 -53
- package/agent/apps/desktop/src/app/shell/keybind-panel.tsx +215 -0
- package/agent/apps/desktop/src/app/shell/model-edit-submenu.test.tsx +84 -0
- package/agent/apps/desktop/src/app/shell/model-edit-submenu.tsx +244 -0
- package/agent/apps/desktop/src/app/shell/model-menu-panel.tsx +392 -0
- package/agent/apps/desktop/src/app/shell/statusbar-controls.tsx +23 -33
- package/agent/apps/desktop/src/app/shell/titlebar-controls.tsx +79 -95
- package/agent/apps/desktop/src/app/shell/titlebar.ts +8 -2
- package/agent/apps/desktop/src/app/skills/index.test.tsx +11 -0
- package/agent/apps/desktop/src/app/skills/index.tsx +79 -64
- package/agent/apps/desktop/src/app/types.ts +85 -0
- package/agent/apps/desktop/src/app/updates-overlay.tsx +110 -105
- package/agent/apps/desktop/src/components/assistant-ui/ansi-text.tsx +34 -0
- package/agent/apps/desktop/src/components/assistant-ui/block-direction.test.tsx +129 -0
- package/agent/apps/desktop/src/components/assistant-ui/clarify-tool.tsx +102 -81
- package/agent/apps/desktop/src/components/assistant-ui/directive-text.tsx +92 -15
- package/agent/apps/desktop/src/components/assistant-ui/markdown-text.test.ts +38 -0
- package/agent/apps/desktop/src/components/assistant-ui/markdown-text.tsx +304 -45
- package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
- package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.tsx +48 -0
- package/agent/apps/desktop/src/components/assistant-ui/streaming.test.tsx +142 -90
- package/agent/apps/desktop/src/components/assistant-ui/thread-list.tsx +337 -0
- package/agent/apps/desktop/src/components/assistant-ui/thread.tsx +667 -190
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval-group.test.tsx +299 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval.test.tsx +133 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval.tsx +239 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.test.ts +31 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.ts +152 -134
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback.tsx +142 -150
- package/agent/apps/desktop/src/components/assistant-ui/tooltip-icon-button.tsx +14 -12
- package/agent/apps/desktop/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
- package/agent/apps/desktop/src/components/assistant-ui/user-message-text.tsx +152 -0
- package/agent/apps/desktop/src/components/boot-failure-overlay.tsx +150 -33
- package/agent/apps/desktop/src/components/boot-failure-reauth.test.ts +100 -0
- package/agent/apps/desktop/src/components/boot-failure-reauth.ts +81 -0
- package/agent/apps/desktop/src/components/brand-mark.tsx +19 -0
- package/agent/apps/desktop/src/components/chat/code-card.tsx +1 -1
- package/agent/apps/desktop/src/components/chat/composer-dock.ts +31 -0
- package/agent/apps/desktop/src/components/chat/diff-lines.tsx +1 -1
- package/agent/apps/desktop/src/components/chat/disclosure-row.tsx +13 -3
- package/agent/apps/desktop/src/components/chat/expandable-block.tsx +52 -0
- package/agent/apps/desktop/src/components/chat/generated-image-result.tsx +174 -0
- package/agent/apps/desktop/src/components/chat/image-generation-placeholder.tsx +70 -37
- package/agent/apps/desktop/src/components/chat/intro.tsx +8 -7
- package/agent/apps/desktop/src/components/chat/preview-attachment.tsx +4 -2
- package/agent/apps/desktop/src/components/chat/shiki-highlighter.test.ts +37 -0
- package/agent/apps/desktop/src/components/chat/shiki-highlighter.tsx +96 -22
- package/agent/apps/desktop/src/components/chat/status-row.tsx +70 -0
- package/agent/apps/desktop/src/components/chat/status-section.tsx +42 -0
- package/agent/apps/desktop/src/components/chat/terminal-output.tsx +54 -0
- package/agent/apps/desktop/src/components/chat/zoomable-image.tsx +70 -109
- package/agent/apps/desktop/src/components/desktop-install-overlay.tsx +154 -84
- package/agent/apps/desktop/src/components/desktop-onboarding-overlay.test.tsx +38 -8
- package/agent/apps/desktop/src/components/desktop-onboarding-overlay.tsx +789 -233
- package/agent/apps/desktop/src/components/error-boundary.tsx +77 -0
- package/agent/apps/desktop/src/components/gateway-connecting-overlay.test.tsx +144 -0
- package/agent/apps/desktop/src/components/gateway-connecting-overlay.tsx +7 -1
- package/agent/apps/desktop/src/components/haptics-provider.tsx +24 -0
- package/agent/apps/desktop/src/components/language-switcher.test.tsx +53 -0
- package/agent/apps/desktop/src/components/language-switcher.tsx +175 -0
- package/agent/apps/desktop/src/components/model-picker.tsx +42 -40
- package/agent/apps/desktop/src/components/model-visibility-dialog.tsx +166 -0
- package/agent/apps/desktop/src/components/notifications.tsx +48 -27
- package/agent/apps/desktop/src/components/pane-shell/index.ts +1 -1
- package/agent/apps/desktop/src/components/pane-shell/pane-shell.tsx +146 -9
- package/agent/apps/desktop/src/components/prompt-overlays.tsx +234 -0
- package/agent/apps/desktop/src/components/session-picker.tsx +108 -0
- package/agent/apps/desktop/src/components/ui/action-status.tsx +25 -0
- package/agent/apps/desktop/src/components/ui/badge.tsx +35 -0
- package/agent/apps/desktop/src/components/ui/button.tsx +37 -13
- package/agent/apps/desktop/src/components/ui/confirm-dialog.tsx +109 -0
- package/agent/apps/desktop/src/components/ui/control.ts +25 -0
- package/agent/apps/desktop/src/components/ui/copy-button.test.tsx +36 -0
- package/agent/apps/desktop/src/components/ui/copy-button.tsx +38 -27
- package/agent/apps/desktop/src/components/ui/dialog.tsx +39 -11
- package/agent/apps/desktop/src/components/ui/dropdown-menu.tsx +98 -24
- package/agent/apps/desktop/src/components/ui/error-state.tsx +50 -0
- package/agent/apps/desktop/src/components/ui/fade-text.tsx +9 -2
- package/agent/apps/desktop/src/components/ui/{braille-spinner.tsx → glyph-spinner.tsx} +15 -13
- package/agent/apps/desktop/src/components/ui/input.tsx +5 -2
- package/agent/apps/desktop/src/components/ui/kbd.tsx +83 -12
- package/agent/apps/desktop/src/components/ui/log-view.tsx +19 -0
- package/agent/apps/desktop/src/components/ui/pagination.tsx +12 -5
- package/agent/apps/desktop/src/components/ui/popover.tsx +44 -0
- package/agent/apps/desktop/src/components/ui/search-field.tsx +80 -0
- package/agent/apps/desktop/src/components/ui/segmented-control.tsx +51 -0
- package/agent/apps/desktop/src/components/ui/select.tsx +10 -3
- package/agent/apps/desktop/src/components/ui/sheet.tsx +8 -2
- package/agent/apps/desktop/src/components/ui/sidebar.tsx +18 -25
- package/agent/apps/desktop/src/components/ui/switch.tsx +38 -15
- package/agent/apps/desktop/src/components/ui/textarea.tsx +4 -11
- package/agent/apps/desktop/src/components/ui/tool-icon.tsx +65 -0
- package/agent/apps/desktop/src/components/ui/tooltip.tsx +31 -4
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
- package/agent/apps/desktop/src/global.d.ts +181 -4
- package/agent/apps/desktop/src/hermes.test.ts +60 -0
- package/agent/apps/desktop/src/hermes.ts +190 -13
- package/agent/apps/desktop/src/hooks/use-image-download.ts +85 -0
- package/agent/apps/desktop/src/hooks/use-resize-observer.ts +13 -4
- package/agent/apps/desktop/src/hooks/use-worktree-info.ts +68 -0
- package/agent/apps/desktop/src/i18n/catalog.ts +12 -0
- package/agent/apps/desktop/src/i18n/context.test.tsx +232 -0
- package/agent/apps/desktop/src/i18n/context.tsx +183 -0
- package/agent/apps/desktop/src/i18n/define-locale.ts +41 -0
- package/agent/apps/desktop/src/i18n/en.ts +1921 -0
- package/agent/apps/desktop/src/i18n/index.ts +20 -0
- package/agent/apps/desktop/src/i18n/ja.ts +2053 -0
- package/agent/apps/desktop/src/i18n/languages.test.ts +43 -0
- package/agent/apps/desktop/src/i18n/languages.ts +86 -0
- package/agent/apps/desktop/src/i18n/runtime.test.ts +75 -0
- package/agent/apps/desktop/src/i18n/runtime.ts +53 -0
- package/agent/apps/desktop/src/i18n/types.ts +1559 -0
- package/agent/apps/desktop/src/i18n/zh-hant.ts +1992 -0
- package/agent/apps/desktop/src/i18n/zh.ts +2099 -0
- package/agent/apps/desktop/src/lib/ansi.test.ts +123 -0
- package/agent/apps/desktop/src/lib/ansi.ts +186 -0
- package/agent/apps/desktop/src/lib/chat-messages.test.ts +79 -0
- package/agent/apps/desktop/src/lib/chat-messages.ts +68 -29
- package/agent/apps/desktop/src/lib/chat-runtime.test.ts +65 -1
- package/agent/apps/desktop/src/lib/chat-runtime.ts +39 -3
- package/agent/apps/desktop/src/lib/completion-sound.ts +519 -0
- package/agent/apps/desktop/src/lib/desktop-fs.test.ts +116 -0
- package/agent/apps/desktop/src/lib/desktop-fs.ts +113 -0
- package/agent/apps/desktop/src/lib/desktop-slash-commands.test.ts +89 -6
- package/agent/apps/desktop/src/lib/desktop-slash-commands.ts +270 -131
- package/agent/apps/desktop/src/lib/external-link.test.tsx +27 -0
- package/agent/apps/desktop/src/lib/external-link.tsx +9 -2
- package/agent/apps/desktop/src/lib/gateway-events.test.ts +27 -0
- package/agent/apps/desktop/src/lib/gateway-events.ts +16 -0
- package/agent/apps/desktop/src/lib/gateway-ws-url.test.ts +78 -0
- package/agent/apps/desktop/src/lib/gateway-ws-url.ts +91 -0
- package/agent/apps/desktop/src/lib/generated-images.test.ts +97 -0
- package/agent/apps/desktop/src/lib/generated-images.ts +116 -0
- package/agent/apps/desktop/src/lib/haptics.ts +17 -0
- package/agent/apps/desktop/src/lib/icons.ts +10 -2
- package/agent/apps/desktop/src/lib/keybinds/actions.ts +137 -0
- package/agent/apps/desktop/src/lib/keybinds/combo.test.ts +86 -0
- package/agent/apps/desktop/src/lib/keybinds/combo.ts +195 -0
- package/agent/apps/desktop/src/lib/local-preview.ts +23 -2
- package/agent/apps/desktop/src/lib/markdown-preprocess.ts +20 -7
- package/agent/apps/desktop/src/lib/media.remote.test.ts +90 -0
- package/agent/apps/desktop/src/lib/media.ts +40 -1
- package/agent/apps/desktop/src/lib/model-status-label.test.ts +59 -0
- package/agent/apps/desktop/src/lib/model-status-label.ts +122 -0
- package/agent/apps/desktop/src/lib/mutable-ref.ts +6 -0
- package/agent/apps/desktop/src/lib/profile-color.ts +58 -0
- package/agent/apps/desktop/src/lib/query-client.ts +13 -0
- package/agent/apps/desktop/src/lib/remend-tail.test.ts +105 -0
- package/agent/apps/desktop/src/lib/remend-tail.ts +108 -0
- package/agent/apps/desktop/src/lib/session-export.ts +6 -3
- package/agent/apps/desktop/src/lib/session-ids.test.ts +44 -0
- package/agent/apps/desktop/src/lib/session-ids.ts +26 -0
- package/agent/apps/desktop/src/lib/session-search.test.ts +66 -0
- package/agent/apps/desktop/src/lib/session-search.ts +21 -0
- package/agent/apps/desktop/src/lib/session-source.ts +126 -0
- package/agent/apps/desktop/src/lib/storage.test.ts +25 -0
- package/agent/apps/desktop/src/lib/storage.ts +35 -1
- package/agent/apps/desktop/src/lib/todos.test.ts +46 -1
- package/agent/apps/desktop/src/lib/todos.ts +37 -0
- package/agent/apps/desktop/src/lib/tool-result-summary.ts +5 -1
- package/agent/apps/desktop/src/lib/update-copy.test.ts +38 -0
- package/agent/apps/desktop/src/lib/update-copy.ts +44 -0
- package/agent/apps/desktop/src/lib/use-enter-animation.ts +2 -2
- package/agent/apps/desktop/src/lib/yolo-session.ts +50 -0
- package/agent/apps/desktop/src/main.tsx +19 -19
- package/agent/apps/desktop/src/store/boot.ts +4 -3
- package/agent/apps/desktop/src/store/clarify.test.ts +81 -0
- package/agent/apps/desktop/src/store/clarify.ts +50 -13
- package/agent/apps/desktop/src/store/command-palette.ts +20 -0
- package/agent/apps/desktop/src/store/compaction.test.ts +53 -0
- package/agent/apps/desktop/src/store/compaction.ts +38 -0
- package/agent/apps/desktop/src/store/completion-sound.ts +32 -0
- package/agent/apps/desktop/src/store/composer-input-history.test.ts +147 -0
- package/agent/apps/desktop/src/store/composer-input-history.ts +158 -0
- package/agent/apps/desktop/src/store/composer-queue.test.ts +68 -0
- package/agent/apps/desktop/src/store/composer-queue.ts +76 -0
- package/agent/apps/desktop/src/store/composer-status.test.ts +99 -0
- package/agent/apps/desktop/src/store/composer-status.ts +277 -0
- package/agent/apps/desktop/src/store/composer.test.ts +106 -0
- package/agent/apps/desktop/src/store/composer.ts +116 -0
- package/agent/apps/desktop/src/store/cron.ts +19 -0
- package/agent/apps/desktop/src/store/gateway.ts +280 -6
- package/agent/apps/desktop/src/store/keybinds.ts +143 -0
- package/agent/apps/desktop/src/store/layout.ts +107 -9
- package/agent/apps/desktop/src/store/model-presets.test.ts +51 -0
- package/agent/apps/desktop/src/store/model-presets.ts +86 -0
- package/agent/apps/desktop/src/store/model-visibility.test.ts +99 -0
- package/agent/apps/desktop/src/store/model-visibility.ts +161 -0
- package/agent/apps/desktop/src/store/native-notifications.test.ts +192 -0
- package/agent/apps/desktop/src/store/native-notifications.ts +203 -0
- package/agent/apps/desktop/src/store/notifications.ts +10 -7
- package/agent/apps/desktop/src/store/onboarding.test.ts +271 -1
- package/agent/apps/desktop/src/store/onboarding.ts +268 -38
- package/agent/apps/desktop/src/store/preview.ts +10 -1
- package/agent/apps/desktop/src/store/profile.test.ts +89 -0
- package/agent/apps/desktop/src/store/profile.ts +395 -0
- package/agent/apps/desktop/src/store/prompts.test.ts +127 -0
- package/agent/apps/desktop/src/store/prompts.ts +117 -0
- package/agent/apps/desktop/src/store/session-switcher.test.ts +115 -0
- package/agent/apps/desktop/src/store/session-switcher.ts +128 -0
- package/agent/apps/desktop/src/store/session-sync.ts +25 -0
- package/agent/apps/desktop/src/store/session.test.ts +268 -2
- package/agent/apps/desktop/src/store/session.ts +392 -18
- package/agent/apps/desktop/src/store/subagents.ts +3 -0
- package/agent/apps/desktop/src/store/system-actions.ts +48 -0
- package/agent/apps/desktop/src/store/thread-scroll.ts +58 -5
- package/agent/apps/desktop/src/store/todos.test.ts +47 -0
- package/agent/apps/desktop/src/store/todos.ts +64 -0
- package/agent/apps/desktop/src/store/tool-dismiss.ts +45 -0
- package/agent/apps/desktop/src/store/translucency.ts +38 -0
- package/agent/apps/desktop/src/store/updates.test.ts +187 -2
- package/agent/apps/desktop/src/store/updates.ts +268 -18
- package/agent/apps/desktop/src/store/windows.test.ts +143 -0
- package/agent/apps/desktop/src/store/windows.ts +115 -0
- package/agent/apps/desktop/src/styles.css +510 -119
- package/agent/apps/desktop/src/themes/color.ts +142 -0
- package/agent/apps/desktop/src/themes/context.tsx +128 -75
- package/agent/apps/desktop/src/themes/install.test.ts +119 -0
- package/agent/apps/desktop/src/themes/install.ts +95 -0
- package/agent/apps/desktop/src/themes/presets.test.ts +33 -0
- package/agent/apps/desktop/src/themes/presets.ts +13 -4
- package/agent/apps/desktop/src/themes/profile-theme.test.ts +41 -0
- package/agent/apps/desktop/src/themes/types.ts +35 -0
- package/agent/apps/desktop/src/themes/user-themes.test.ts +63 -0
- package/agent/apps/desktop/src/themes/user-themes.ts +122 -0
- package/agent/apps/desktop/src/themes/vscode.test.ts +171 -0
- package/agent/apps/desktop/src/themes/vscode.ts +343 -0
- package/agent/apps/desktop/src/types/hermes.ts +138 -1
- package/agent/apps/desktop/tsconfig.json +2 -2
- package/agent/apps/desktop/vite.config.ts +18 -0
- package/agent/apps/shared/package.json +1 -1
- package/agent/apps/shared/src/json-rpc-gateway.ts +63 -2
- package/agent/apps/shared/tsconfig.json +2 -2
- package/agent/cli-config.yaml.example +78 -1
- package/agent/cli.py +2294 -3146
- package/agent/cron/blueprint_catalog.py +713 -0
- package/agent/cron/jobs.py +226 -110
- package/agent/cron/scheduler.py +468 -193
- package/agent/cron/scheduler_provider.py +177 -0
- package/agent/cron/scripts/__init__.py +1 -0
- package/agent/cron/scripts/classify_items.py +226 -0
- package/agent/cron/suggestion_catalog.py +154 -0
- package/agent/cron/suggestions.py +257 -0
- package/agent/docs/chronos-managed-cron-contract.md +196 -0
- package/agent/docs/design/profile-builder.md +146 -0
- package/agent/docs/middleware/README.md +260 -0
- package/agent/docs/observability/README.md +316 -0
- package/agent/docs/plans/2026-06-09-003-fix-telegram-stream-overflow-continuations-plan.md +240 -0
- package/agent/docs/rca-ssl-cacert-post-git-pull.md +54 -0
- package/agent/docs/relay-connector-contract.md +285 -0
- package/agent/gateway/authz_mixin.py +536 -0
- package/agent/gateway/channel_directory.py +65 -3
- package/agent/gateway/config.py +222 -12
- package/agent/gateway/display_config.py +10 -0
- package/agent/gateway/hooks.py +17 -0
- package/agent/gateway/kanban_watchers.py +1146 -0
- package/agent/gateway/message_timestamps.py +166 -0
- package/agent/gateway/platforms/ADDING_A_PLATFORM.md +29 -0
- package/agent/gateway/platforms/api_server.py +216 -38
- package/agent/gateway/platforms/base.py +210 -58
- package/agent/gateway/platforms/email.py +122 -12
- package/agent/gateway/platforms/feishu.py +80 -11
- package/agent/gateway/platforms/feishu_meeting_invite.py +212 -0
- package/agent/gateway/platforms/matrix.py +1498 -297
- package/agent/gateway/platforms/qqbot/adapter.py +6 -0
- package/agent/gateway/platforms/signal.py +8 -0
- package/agent/gateway/platforms/slack.py +308 -12
- package/agent/gateway/platforms/telegram.py +831 -24
- package/agent/gateway/platforms/webhook.py +109 -21
- package/agent/gateway/platforms/weixin.py +113 -2
- package/agent/gateway/platforms/whatsapp.py +94 -288
- package/agent/gateway/platforms/whatsapp_cloud.py +1956 -0
- package/agent/gateway/platforms/whatsapp_common.py +367 -0
- package/agent/gateway/platforms/yuanbao.py +608 -191
- package/agent/gateway/platforms/yuanbao_proto.py +232 -23
- package/agent/gateway/relay/__init__.py +375 -0
- package/agent/gateway/relay/adapter.py +222 -0
- package/agent/gateway/relay/auth.py +168 -0
- package/agent/gateway/relay/descriptor.py +118 -0
- package/agent/gateway/relay/transport.py +101 -0
- package/agent/gateway/relay/ws_transport.py +327 -0
- package/agent/gateway/response_filters.py +53 -0
- package/agent/gateway/rich_sent_store.py +80 -0
- package/agent/gateway/run.py +2940 -5001
- package/agent/gateway/session.py +109 -8
- package/agent/gateway/session_context.py +22 -4
- package/agent/gateway/slash_commands.py +3854 -0
- package/agent/gateway/status.py +141 -21
- package/agent/gateway/stream_consumer.py +288 -31
- package/agent/hermes-already-has-routines.md +1 -1
- package/agent/hermes_cli/__init__.py +62 -17
- package/agent/hermes_cli/_parser.py +30 -0
- package/agent/hermes_cli/_subprocess_compat.py +61 -0
- package/agent/hermes_cli/active_sessions.py +320 -0
- package/agent/hermes_cli/auth.py +707 -59
- package/agent/hermes_cli/auth_commands.py +39 -22
- package/agent/hermes_cli/backup.py +109 -7
- package/agent/hermes_cli/banner.py +88 -0
- package/agent/hermes_cli/blueprint_cmd.py +318 -0
- package/agent/hermes_cli/clawpump_cli.py +3 -3
- package/agent/hermes_cli/cli_agent_setup_mixin.py +684 -0
- package/agent/hermes_cli/cli_commands_mixin.py +2293 -0
- package/agent/hermes_cli/commands.py +216 -91
- package/agent/hermes_cli/config.py +967 -130
- package/agent/hermes_cli/container_boot.py +76 -11
- package/agent/hermes_cli/cron.py +5 -11
- package/agent/hermes_cli/curator.py +21 -0
- package/agent/hermes_cli/dashboard_auth/__init__.py +2 -0
- package/agent/hermes_cli/dashboard_auth/base.py +62 -0
- package/agent/hermes_cli/dashboard_auth/cookies.py +32 -19
- package/agent/hermes_cli/dashboard_auth/login_page.py +156 -6
- package/agent/hermes_cli/dashboard_auth/middleware.py +28 -4
- package/agent/hermes_cli/dashboard_auth/prefix.py +46 -2
- package/agent/hermes_cli/dashboard_auth/public_paths.py +6 -0
- package/agent/hermes_cli/dashboard_auth/routes.py +158 -2
- package/agent/hermes_cli/dashboard_auth/ws_tickets.py +85 -11
- package/agent/hermes_cli/dashboard_register.py +427 -0
- package/agent/hermes_cli/debug.py +155 -50
- package/agent/hermes_cli/distribution.py +227 -0
- package/agent/hermes_cli/doctor.py +255 -14
- package/agent/hermes_cli/dump.py +60 -6
- package/agent/hermes_cli/env_loader.py +33 -0
- package/agent/hermes_cli/gateway.py +755 -103
- package/agent/hermes_cli/gateway_enroll.py +250 -0
- package/agent/hermes_cli/gateway_windows.py +254 -11
- package/agent/hermes_cli/gui_uninstall.py +285 -0
- package/agent/hermes_cli/inventory.py +105 -4
- package/agent/hermes_cli/kanban.py +58 -71
- package/agent/hermes_cli/kanban_db.py +391 -14
- package/agent/hermes_cli/kanban_decompose.py +2 -2
- package/agent/hermes_cli/kanban_specify.py +3 -1
- package/agent/hermes_cli/logs.py +2 -0
- package/agent/hermes_cli/main.py +2889 -5287
- package/agent/hermes_cli/managed_scope.py +214 -0
- package/agent/hermes_cli/managed_uv.py +254 -0
- package/agent/hermes_cli/mcp_catalog.py +6 -3
- package/agent/hermes_cli/mcp_config.py +145 -21
- package/agent/hermes_cli/mcp_security.py +96 -0
- package/agent/hermes_cli/mcp_startup.py +32 -3
- package/agent/hermes_cli/memory_providers.py +149 -0
- package/agent/hermes_cli/memory_setup.py +97 -42
- package/agent/hermes_cli/middleware.py +313 -0
- package/agent/hermes_cli/model_catalog.py +31 -0
- package/agent/hermes_cli/model_cost_guard.py +134 -0
- package/agent/hermes_cli/model_normalize.py +2 -1
- package/agent/hermes_cli/model_setup_flows.py +2759 -0
- package/agent/hermes_cli/model_switch.py +242 -27
- package/agent/hermes_cli/models.py +284 -44
- package/agent/hermes_cli/nous_account.py +33 -6
- package/agent/hermes_cli/nous_billing.py +406 -0
- package/agent/hermes_cli/nous_subscription.py +202 -5
- package/agent/hermes_cli/platforms.py +1 -0
- package/agent/hermes_cli/plugins.py +218 -18
- package/agent/hermes_cli/plugins_cmd.py +249 -105
- package/agent/hermes_cli/portal_cli.py +56 -16
- package/agent/hermes_cli/profile_distribution.py +6 -1
- package/agent/hermes_cli/profiles.py +283 -32
- package/agent/hermes_cli/provider_catalog.py +170 -0
- package/agent/hermes_cli/providers.py +4 -1
- package/agent/hermes_cli/pty_bridge.py +53 -4
- package/agent/hermes_cli/runtime_provider.py +216 -34
- package/agent/hermes_cli/secret_prompt.py +4 -4
- package/agent/hermes_cli/secrets_cli.py +24 -0
- package/agent/hermes_cli/send_cmd.py +28 -2
- package/agent/hermes_cli/service_manager.py +166 -19
- package/agent/hermes_cli/session_listing.py +97 -0
- package/agent/hermes_cli/setup.py +158 -94
- package/agent/hermes_cli/setup_whatsapp_cloud.py +541 -0
- package/agent/hermes_cli/skills_config.py +8 -2
- package/agent/hermes_cli/skills_hub.py +149 -7
- package/agent/hermes_cli/status.py +2 -2
- package/agent/hermes_cli/subcommands/__init__.py +18 -0
- package/agent/hermes_cli/subcommands/_shared.py +29 -0
- package/agent/hermes_cli/subcommands/acp.py +52 -0
- package/agent/hermes_cli/subcommands/auth.py +109 -0
- package/agent/hermes_cli/subcommands/backup.py +38 -0
- package/agent/hermes_cli/subcommands/claw.py +92 -0
- package/agent/hermes_cli/subcommands/config.py +49 -0
- package/agent/hermes_cli/subcommands/cron.py +163 -0
- package/agent/hermes_cli/subcommands/dashboard.py +143 -0
- package/agent/hermes_cli/subcommands/debug.py +77 -0
- package/agent/hermes_cli/subcommands/doctor.py +35 -0
- package/agent/hermes_cli/subcommands/dump.py +28 -0
- package/agent/hermes_cli/subcommands/gateway.py +332 -0
- package/agent/hermes_cli/subcommands/gui.py +63 -0
- package/agent/hermes_cli/subcommands/hooks.py +77 -0
- package/agent/hermes_cli/subcommands/import_cmd.py +31 -0
- package/agent/hermes_cli/subcommands/insights.py +25 -0
- package/agent/hermes_cli/subcommands/login.py +78 -0
- package/agent/hermes_cli/subcommands/logout.py +28 -0
- package/agent/hermes_cli/subcommands/logs.py +78 -0
- package/agent/hermes_cli/subcommands/mcp.py +108 -0
- package/agent/hermes_cli/subcommands/memory.py +53 -0
- package/agent/hermes_cli/subcommands/model.py +72 -0
- package/agent/hermes_cli/subcommands/pairing.py +36 -0
- package/agent/hermes_cli/subcommands/plugins.py +94 -0
- package/agent/hermes_cli/subcommands/postinstall.py +23 -0
- package/agent/hermes_cli/subcommands/profile.py +203 -0
- package/agent/hermes_cli/subcommands/prompt_size.py +36 -0
- package/agent/hermes_cli/subcommands/security.py +62 -0
- package/agent/hermes_cli/subcommands/setup.py +58 -0
- package/agent/hermes_cli/subcommands/skills.py +298 -0
- package/agent/hermes_cli/subcommands/slack.py +60 -0
- package/agent/hermes_cli/subcommands/status.py +28 -0
- package/agent/hermes_cli/subcommands/tools.py +95 -0
- package/agent/hermes_cli/subcommands/uninstall.py +41 -0
- package/agent/hermes_cli/subcommands/update.py +70 -0
- package/agent/hermes_cli/subcommands/version.py +18 -0
- package/agent/hermes_cli/subcommands/webhook.py +76 -0
- package/agent/hermes_cli/subcommands/whatsapp.py +22 -0
- package/agent/hermes_cli/suggestions_cmd.py +153 -0
- package/agent/hermes_cli/telegram_managed_bot.py +358 -0
- package/agent/hermes_cli/tips.py +3 -4
- package/agent/hermes_cli/tools_config.py +155 -28
- package/agent/hermes_cli/uninstall.py +231 -35
- package/agent/hermes_cli/web_server.py +6188 -975
- package/agent/hermes_cli/win_pty_bridge.py +179 -0
- package/agent/hermes_cli/write_approval_commands.py +209 -0
- package/agent/hermes_constants.py +164 -33
- package/agent/hermes_logging.py +74 -2
- package/agent/hermes_state.py +919 -106
- package/agent/hermes_time.py +20 -0
- package/agent/locales/af.yaml +23 -0
- package/agent/locales/de.yaml +23 -0
- package/agent/locales/en.yaml +20 -0
- package/agent/locales/es.yaml +23 -0
- package/agent/locales/fr.yaml +23 -0
- package/agent/locales/ga.yaml +23 -0
- package/agent/locales/hu.yaml +23 -0
- package/agent/locales/it.yaml +23 -0
- package/agent/locales/ja.yaml +23 -0
- package/agent/locales/ko.yaml +23 -0
- package/agent/locales/pt.yaml +23 -0
- package/agent/locales/ru.yaml +23 -0
- package/agent/locales/tr.yaml +23 -0
- package/agent/locales/uk.yaml +23 -0
- package/agent/locales/zh-hant.yaml +23 -0
- package/agent/locales/zh.yaml +23 -0
- package/agent/model_tools.py +204 -40
- package/agent/optional-mcps/clawpump/manifest.yaml +15 -5
- package/agent/optional-mcps/clawpump-stdio/manifest.yaml +14 -4
- package/agent/optional-mcps/unreal-engine/manifest.yaml +54 -0
- package/agent/optional-skills/blockchain/hyperliquid/SKILL.md +2 -2
- package/agent/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py +1 -1
- package/agent/optional-skills/creative/kanban-video-orchestrator/SKILL.md +1 -1
- package/agent/optional-skills/creative/kanban-video-orchestrator/assets/setup.sh.tmpl +4 -3
- package/agent/optional-skills/creative/kanban-video-orchestrator/references/kanban-setup.md +6 -4
- package/agent/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +2 -2
- package/agent/{skills/software-development → optional-skills/devops}/hermes-s6-container-supervision/SKILL.md +2 -0
- package/agent/optional-skills/devops/watchers/SKILL.md +1 -1
- package/agent/optional-skills/devops/watchers/scripts/watch_github.py +2 -1
- package/agent/optional-skills/payments/mpp-agent/SKILL.md +124 -0
- package/agent/optional-skills/payments/stripe-link-cli/SKILL.md +184 -0
- package/agent/optional-skills/payments/stripe-projects/SKILL.md +120 -0
- package/agent/optional-skills/productivity/canvas/SKILL.md +1 -1
- package/agent/optional-skills/productivity/canvas/scripts/canvas_api.py +4 -1
- package/agent/optional-skills/productivity/shop/SKILL.md +224 -0
- package/agent/optional-skills/productivity/shop/references/catalog-mcp.md +236 -0
- package/agent/optional-skills/productivity/shop/references/direct-api.md +278 -0
- package/agent/optional-skills/productivity/shop/references/legal.md +3 -0
- package/agent/optional-skills/productivity/shop/references/safety.md +36 -0
- package/agent/optional-skills/productivity/shopify/SKILL.md +1 -1
- package/agent/optional-skills/productivity/siyuan/SKILL.md +1 -1
- package/agent/optional-skills/productivity/telephony/SKILL.md +4 -4
- package/agent/optional-skills/productivity/telephony/scripts/telephony.py +15 -15
- package/agent/optional-skills/security/1password/SKILL.md +1 -1
- package/agent/{skills/red-teaming → optional-skills/security}/godmode/SKILL.md +3 -4
- package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/auto_jailbreak.py +3 -1
- package/agent/optional-skills/software-development/rest-graphql-debug/SKILL.md +1 -1
- package/agent/{skills → optional-skills}/software-development/subagent-driven-development/SKILL.md +5 -5
- package/agent/package-lock.json +4082 -7907
- package/agent/package.json +18 -3
- package/agent/plugins/browser/firecrawl/provider.py +4 -1
- package/agent/plugins/cron/__init__.py +344 -0
- package/agent/plugins/cron/chronos/__init__.py +241 -0
- package/agent/plugins/cron/chronos/_nas_client.py +123 -0
- package/agent/plugins/cron/chronos/plugin.yaml +9 -0
- package/agent/plugins/cron/chronos/verify.py +103 -0
- package/agent/plugins/dashboard_auth/basic/__init__.py +491 -0
- package/agent/plugins/dashboard_auth/basic/plugin.yaml +7 -0
- package/agent/plugins/dashboard_auth/nous/__init__.py +12 -14
- package/agent/plugins/dashboard_auth/self_hosted/__init__.py +736 -0
- package/agent/plugins/dashboard_auth/self_hosted/plugin.yaml +8 -0
- package/agent/plugins/disk-cleanup/disk_cleanup.py +100 -20
- package/agent/plugins/google_meet/audio_bridge.py +4 -0
- package/agent/plugins/google_meet/meet_bot.py +7 -1
- package/agent/plugins/hermes-achievements/dashboard/dist/index.js +9 -15
- package/agent/plugins/image_gen/fal/__init__.py +35 -6
- package/agent/plugins/image_gen/krea/__init__.py +56 -13
- package/agent/plugins/image_gen/openai/__init__.py +122 -24
- package/agent/plugins/image_gen/openai-codex/__init__.py +28 -2
- package/agent/plugins/image_gen/xai/__init__.py +92 -12
- package/agent/plugins/kanban/dashboard/dist/index.js +63 -48
- package/agent/plugins/kanban/dashboard/plugin_api.py +39 -35
- package/agent/plugins/memory/__init__.py +48 -5
- package/agent/plugins/memory/byterover/__init__.py +1 -0
- package/agent/plugins/memory/hindsight/README.md +1 -1
- package/agent/plugins/memory/hindsight/__init__.py +138 -24
- package/agent/plugins/memory/hindsight/plugin.yaml +1 -1
- package/agent/plugins/memory/honcho/README.md +13 -10
- package/agent/plugins/memory/honcho/cli.py +247 -122
- package/agent/plugins/memory/honcho/client.py +112 -102
- package/agent/plugins/memory/openviking/README.md +12 -1
- package/agent/plugins/memory/openviking/__init__.py +2281 -107
- package/agent/plugins/memory/openviking/plugin.yaml +1 -2
- package/agent/plugins/memory/supermemory/README.md +22 -10
- package/agent/plugins/memory/supermemory/__init__.py +142 -37
- package/agent/plugins/memory/supermemory/plugin.yaml +1 -1
- package/agent/plugins/model-providers/anthropic/__init__.py +1 -0
- package/agent/plugins/model-providers/bedrock/__init__.py +1 -0
- package/agent/plugins/model-providers/copilot-acp/__init__.py +1 -0
- package/agent/plugins/model-providers/custom/__init__.py +8 -2
- package/agent/plugins/model-providers/kimi-coding/__init__.py +16 -7
- package/agent/plugins/model-providers/minimax/__init__.py +60 -8
- package/agent/plugins/model-providers/opencode-zen/__init__.py +12 -3
- package/agent/plugins/model-providers/openrouter/__init__.py +75 -4
- package/agent/plugins/model-providers/xiaomi/__init__.py +2 -0
- package/agent/plugins/model-providers/zai/__init__.py +1 -0
- package/agent/plugins/observability/langfuse/__init__.py +147 -14
- package/agent/plugins/observability/nemo_relay/README.md +559 -0
- package/agent/plugins/observability/nemo_relay/__init__.py +962 -0
- package/agent/plugins/observability/nemo_relay/plugin.yaml +20 -0
- package/agent/plugins/platforms/discord/adapter.py +932 -61
- package/agent/plugins/platforms/discord/voice_mixer.py +379 -0
- package/agent/plugins/platforms/google_chat/adapter.py +9 -3
- package/agent/plugins/platforms/google_chat/oauth.py +1 -1
- package/agent/plugins/platforms/homeassistant/__init__.py +3 -0
- package/agent/{gateway/platforms/homeassistant.py → plugins/platforms/homeassistant/adapter.py} +128 -0
- package/agent/plugins/platforms/homeassistant/plugin.yaml +22 -0
- package/agent/plugins/platforms/irc/adapter.py +4 -1
- package/agent/plugins/platforms/line/adapter.py +16 -1
- package/agent/plugins/platforms/mattermost/adapter.py +100 -24
- package/agent/plugins/platforms/photon/README.md +179 -0
- package/agent/plugins/platforms/photon/__init__.py +4 -0
- package/agent/plugins/platforms/photon/adapter.py +1586 -0
- package/agent/plugins/platforms/photon/auth.py +1046 -0
- package/agent/plugins/platforms/photon/cli.py +439 -0
- package/agent/plugins/platforms/photon/plugin.yaml +88 -0
- package/agent/plugins/platforms/photon/sidecar/README.md +52 -0
- package/agent/plugins/platforms/photon/sidecar/index.mjs +720 -0
- package/agent/plugins/platforms/photon/sidecar/package-lock.json +1730 -0
- package/agent/plugins/platforms/photon/sidecar/package.json +25 -0
- package/agent/plugins/platforms/photon/sidecar/patch-spectrum-mixed-attachments.mjs +155 -0
- package/agent/plugins/platforms/raft/__init__.py +3 -0
- package/agent/plugins/platforms/raft/adapter.py +774 -0
- package/agent/plugins/platforms/raft/plugin.yaml +19 -0
- package/agent/plugins/platforms/simplex/adapter.py +777 -220
- package/agent/plugins/platforms/simplex/plugin.yaml +21 -2
- package/agent/plugins/platforms/teams/adapter.py +175 -5
- package/agent/plugins/plugin_utils.py +135 -0
- package/agent/plugins/video_gen/fal/__init__.py +10 -3
- package/agent/plugins/web/searxng/provider.py +15 -2
- package/agent/plugins/web/xai/provider.py +2 -2
- package/agent/providers/base.py +22 -3
- package/agent/pyproject.toml +115 -21
- package/agent/run_agent.py +733 -39
- package/agent/scripts/build_skills_index.py +51 -19
- package/agent/scripts/check_subprocess_stdin.py +177 -0
- package/agent/scripts/contributor_audit.py +2 -0
- package/agent/scripts/docker_config_migrate.py +67 -0
- package/agent/scripts/install.cmd +3 -3
- package/agent/scripts/install.ps1 +580 -154
- package/agent/scripts/install.sh +402 -185
- package/agent/scripts/lib/node-bootstrap.sh +39 -4
- package/agent/scripts/release.py +183 -0
- package/agent/scripts/run_tests.sh +1 -0
- package/agent/scripts/run_tests_parallel.py +18 -23
- package/agent/scripts/whatsapp-bridge/bridge.js +25 -4
- package/agent/setup.py +59 -0
- package/agent/skills/autonomous-ai-agents/codex/SKILL.md +19 -0
- package/agent/skills/autonomous-ai-agents/hermes-agent/SKILL.md +10 -3
- package/agent/skills/{mcp/native-mcp/SKILL.md → autonomous-ai-agents/hermes-agent/references/native-mcp.md} +0 -13
- package/agent/skills/{devops/webhook-subscriptions/SKILL.md → autonomous-ai-agents/hermes-agent/references/webhooks.md} +1 -11
- package/agent/skills/clawpump/SKILL.md +53 -5
- package/agent/skills/devops/kanban-orchestrator/SKILL.md +1 -0
- package/agent/skills/devops/kanban-worker/SKILL.md +1 -0
- package/agent/skills/github/github-auth/SKILL.md +2 -2
- package/agent/skills/github/github-auth/scripts/gh-env.sh +2 -2
- package/agent/skills/github/github-code-review/SKILL.md +2 -2
- package/agent/skills/github/github-issues/SKILL.md +2 -2
- package/agent/skills/github/github-pr-workflow/SKILL.md +2 -2
- package/agent/skills/github/github-repo-management/SKILL.md +2 -2
- package/agent/skills/media/gif-search/SKILL.md +1 -1
- package/agent/skills/media/youtube-content/SKILL.md +10 -7
- package/agent/skills/media/youtube-content/scripts/fetch_transcript.py +3 -3
- package/agent/skills/note-taking/obsidian/SKILL.md +1 -1
- package/agent/skills/productivity/airtable/SKILL.md +2 -2
- package/agent/skills/productivity/google-workspace/scripts/setup.py +33 -7
- package/agent/skills/productivity/notion/SKILL.md +2 -2
- package/agent/skills/productivity/teams-meeting-pipeline/SKILL.md +1 -1
- package/agent/skills/research/llm-wiki/SKILL.md +1 -1
- package/agent/skills/social-media/xurl/SKILL.md +9 -0
- package/agent/skills/software-development/hermes-agent-skill-authoring/SKILL.md +1 -1
- package/agent/skills/software-development/plan/SKILL.md +285 -5
- package/agent/skills/software-development/requesting-code-review/SKILL.md +2 -2
- package/agent/skills/software-development/simplify-code/SKILL.md +212 -0
- package/agent/skills/software-development/spike/SKILL.md +2 -2
- package/agent/skills/software-development/systematic-debugging/SKILL.md +1 -1
- package/agent/skills/software-development/test-driven-development/SKILL.md +1 -1
- package/agent/tools/approval.py +302 -4
- package/agent/tools/async_delegation.py +386 -0
- package/agent/tools/blueprints.py +325 -0
- package/agent/tools/browser_cdp_tool.py +3 -3
- package/agent/tools/browser_tool.py +34 -6
- package/agent/tools/checkpoint_manager.py +31 -1
- package/agent/tools/clarify_tool.py +55 -5
- package/agent/tools/code_execution_tool.py +31 -14
- package/agent/tools/computer_use/cua_backend.py +81 -3
- package/agent/tools/computer_use/tool.py +79 -5
- package/agent/tools/computer_use/vision_routing.py +55 -3
- package/agent/tools/credential_files.py +31 -12
- package/agent/tools/cronjob_tools.py +30 -20
- package/agent/tools/delegate_tool.py +356 -31
- package/agent/tools/env_probe.py +1 -0
- package/agent/tools/environments/docker.py +163 -8
- package/agent/tools/environments/file_sync.py +2 -1
- package/agent/tools/environments/local.py +74 -23
- package/agent/tools/environments/singularity.py +4 -1
- package/agent/tools/environments/ssh.py +78 -11
- package/agent/tools/file_operations.py +277 -41
- package/agent/tools/file_tools.py +166 -28
- package/agent/tools/image_generation_tool.py +515 -29
- package/agent/tools/kanban_tools.py +99 -0
- package/agent/tools/lazy_deps.py +33 -2
- package/agent/tools/mcp_oauth.py +5 -5
- package/agent/tools/mcp_oauth_manager.py +7 -5
- package/agent/tools/mcp_tool.py +840 -33
- package/agent/tools/memory_tool.py +335 -38
- package/agent/tools/osv_check.py +15 -1
- package/agent/tools/process_registry.py +155 -11
- package/agent/tools/read_extract.py +248 -0
- package/agent/tools/read_terminal_tool.py +93 -0
- package/agent/tools/schema_sanitizer.py +38 -0
- package/agent/tools/send_message_tool.py +163 -49
- package/agent/tools/session_search_tool.py +189 -7
- package/agent/tools/skill_manager_tool.py +202 -3
- package/agent/tools/skill_usage.py +52 -4
- package/agent/tools/skills_hub.py +184 -44
- package/agent/tools/skills_sync.py +232 -5
- package/agent/tools/skills_tool.py +125 -11
- package/agent/tools/terminal_tool.py +148 -26
- package/agent/tools/tirith_security.py +2 -0
- package/agent/tools/todo_tool.py +32 -1
- package/agent/tools/transcription_tools.py +13 -5
- package/agent/tools/tts_tool.py +332 -38
- package/agent/tools/url_safety.py +52 -1
- package/agent/tools/vision_tools.py +124 -39
- package/agent/tools/voice_mode.py +4 -3
- package/agent/tools/web_tools.py +45 -15
- package/agent/tools/write_approval.py +493 -0
- package/agent/toolsets.py +34 -10
- package/agent/trajectory_compressor.py +81 -10
- package/agent/tui_gateway/entry.py +43 -6
- package/agent/tui_gateway/server.py +3335 -330
- package/agent/tui_gateway/slash_worker.py +61 -0
- package/agent/tui_gateway/ws.py +67 -9
- package/agent/ui-tui/eslint.config.mjs +0 -4
- package/agent/ui-tui/package.json +6 -6
- package/agent/ui-tui/packages/hermes-ink/package.json +1 -1
- package/agent/ui-tui/packages/hermes-ink/src/ink/app-mouse.test.ts +34 -1
- package/agent/ui-tui/packages/hermes-ink/src/ink/app-rawmode-mouse.test.ts +91 -0
- package/agent/ui-tui/packages/hermes-ink/src/ink/components/App.tsx +35 -2
- package/agent/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts +4 -11
- package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts +23 -57
- package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts +11 -135
- package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.test.ts +185 -0
- package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.ts +37 -3
- package/agent/ui-tui/packages/hermes-ink/src/utils/execFileNoThrow.ts +5 -5
- package/agent/ui-tui/src/__tests__/appChromeStatusRule.test.tsx +217 -0
- package/agent/ui-tui/src/__tests__/appChromeStatusRuleDevCredits.test.tsx +73 -0
- package/agent/ui-tui/src/__tests__/approvalAction.test.ts +11 -0
- package/agent/ui-tui/src/__tests__/billingCommand.test.ts +301 -0
- package/agent/ui-tui/src/__tests__/blockLayout.test.ts +122 -0
- package/agent/ui-tui/src/__tests__/brandingMcpCount.test.ts +111 -0
- package/agent/ui-tui/src/__tests__/completionApply.test.ts +51 -0
- package/agent/ui-tui/src/__tests__/createGatewayEventHandler.test.ts +487 -2
- package/agent/ui-tui/src/__tests__/createSlashHandler.test.ts +54 -0
- package/agent/ui-tui/src/__tests__/creditsCommand.test.ts +144 -0
- package/agent/ui-tui/src/__tests__/gatewayClient.test.ts +120 -99
- package/agent/ui-tui/src/__tests__/gracefulExit.test.ts +11 -0
- package/agent/ui-tui/src/__tests__/memoryMonitor.test.ts +102 -0
- package/agent/ui-tui/src/__tests__/paths.test.ts +41 -1
- package/agent/ui-tui/src/__tests__/terminalModes.test.ts +22 -0
- package/agent/ui-tui/src/__tests__/text.test.ts +23 -0
- package/agent/ui-tui/src/__tests__/textInputFastEcho.test.ts +37 -0
- package/agent/ui-tui/src/__tests__/turnControllerNotice.test.ts +43 -0
- package/agent/ui-tui/src/__tests__/useInputHandlers.test.ts +38 -1
- package/agent/ui-tui/src/__tests__/virtualHeights.test.ts +8 -0
- package/agent/ui-tui/src/app/createGatewayEventHandler.ts +102 -7
- package/agent/ui-tui/src/app/interfaces.ts +64 -1
- package/agent/ui-tui/src/app/overlayStore.ts +18 -2
- package/agent/ui-tui/src/app/slash/commands/billing.ts +332 -0
- package/agent/ui-tui/src/app/slash/commands/core.ts +31 -2
- package/agent/ui-tui/src/app/slash/commands/credits.ts +57 -0
- package/agent/ui-tui/src/app/slash/commands/ops.ts +28 -0
- package/agent/ui-tui/src/app/slash/commands/session.ts +32 -4
- package/agent/ui-tui/src/app/slash/registry.ts +4 -0
- package/agent/ui-tui/src/app/turnController.ts +145 -2
- package/agent/ui-tui/src/app/uiStore.ts +2 -0
- package/agent/ui-tui/src/app/useInputHandlers.ts +42 -4
- package/agent/ui-tui/src/app/useMainApp.ts +54 -8
- package/agent/ui-tui/src/app/useSessionLifecycle.ts +40 -31
- package/agent/ui-tui/src/app/useSubmission.ts +23 -31
- package/agent/ui-tui/src/components/appChrome.tsx +112 -5
- package/agent/ui-tui/src/components/appLayout.tsx +9 -0
- package/agent/ui-tui/src/components/appOverlays.tsx +25 -1
- package/agent/ui-tui/src/components/billingOverlay.tsx +684 -0
- package/agent/ui-tui/src/components/branding.tsx +15 -3
- package/agent/ui-tui/src/components/messageLine.tsx +25 -3
- package/agent/ui-tui/src/components/pluginsHub.tsx +238 -0
- package/agent/ui-tui/src/components/prompts.tsx +31 -17
- package/agent/ui-tui/src/components/streamingAssistant.tsx +63 -55
- package/agent/ui-tui/src/components/textInput.tsx +16 -0
- package/agent/ui-tui/src/config/env.ts +12 -0
- package/agent/ui-tui/src/config/limits.ts +13 -0
- package/agent/ui-tui/src/domain/blockLayout.ts +146 -0
- package/agent/ui-tui/src/domain/paths.ts +24 -0
- package/agent/ui-tui/src/domain/slash.ts +40 -0
- package/agent/ui-tui/src/entry.tsx +35 -4
- package/agent/ui-tui/src/gatewayClient.ts +22 -10
- package/agent/ui-tui/src/gatewayTypes.ts +130 -1
- package/agent/ui-tui/src/lib/gracefulExit.ts +24 -4
- package/agent/ui-tui/src/lib/memory.test.ts +162 -0
- package/agent/ui-tui/src/lib/memory.ts +60 -1
- package/agent/ui-tui/src/lib/memoryMonitor.ts +79 -4
- package/agent/ui-tui/src/lib/osc52.ts +1 -1
- package/agent/ui-tui/src/lib/text.test.ts +32 -1
- package/agent/ui-tui/src/lib/text.ts +29 -2
- package/agent/ui-tui/src/lib/virtualHeights.ts +13 -0
- package/agent/ui-tui/src/types.ts +5 -0
- package/agent/ui-tui/tsconfig.build.json +0 -1
- package/agent/ui-tui/tsconfig.json +2 -1
- package/agent/utils.py +66 -2
- package/agent/uv.lock +308 -696
- package/agent/web/index.html +2 -2
- package/agent/web/package.json +11 -6
- package/agent/web/public/claw-bg.webp +0 -0
- package/agent/web/public/claw-logo.webp +0 -0
- package/agent/web/src/App.tsx +138 -48
- package/agent/web/src/components/AutomationBlueprints.tsx +225 -0
- package/agent/web/src/components/Backdrop.tsx +15 -0
- package/agent/web/src/components/ChatSessionList.tsx +260 -0
- package/agent/web/src/components/ChatSidebar.tsx +262 -78
- package/agent/web/src/components/ConfirmDialog.tsx +122 -0
- package/agent/web/src/components/ModelPickerDialog.tsx +111 -16
- package/agent/web/src/components/ModelReloadConfirm.tsx +40 -0
- package/agent/web/src/components/ProfileScopeBanner.tsx +30 -0
- package/agent/web/src/components/ProfileSwitcher.tsx +67 -0
- package/agent/web/src/components/ReasoningPicker.tsx +167 -0
- package/agent/web/src/components/SkillEditorDialog.tsx +215 -0
- package/agent/web/src/components/ThemeSwitcher.tsx +119 -4
- package/agent/web/src/components/ToolsetConfigDrawer.tsx +457 -0
- package/agent/web/src/contexts/PageHeaderProvider.tsx +7 -4
- package/agent/web/src/contexts/ProfileProvider.tsx +137 -0
- package/agent/web/src/contexts/SystemActions.tsx +6 -8
- package/agent/web/src/contexts/profile-context.ts +19 -0
- package/agent/web/src/contexts/useProfileScope.ts +6 -0
- package/agent/web/src/i18n/af.ts +5 -4
- package/agent/web/src/i18n/de.ts +5 -4
- package/agent/web/src/i18n/en.ts +58 -4
- package/agent/web/src/i18n/es.ts +5 -3
- package/agent/web/src/i18n/fr.ts +5 -3
- package/agent/web/src/i18n/ga.ts +5 -4
- package/agent/web/src/i18n/hu.ts +5 -4
- package/agent/web/src/i18n/it.ts +5 -4
- package/agent/web/src/i18n/ja.ts +5 -4
- package/agent/web/src/i18n/ko.ts +5 -4
- package/agent/web/src/i18n/pt.ts +5 -3
- package/agent/web/src/i18n/ru.ts +5 -4
- package/agent/web/src/i18n/tr.ts +5 -4
- package/agent/web/src/i18n/types.ts +59 -1
- package/agent/web/src/i18n/uk.ts +5 -3
- package/agent/web/src/i18n/zh-hant.ts +5 -4
- package/agent/web/src/i18n/zh.ts +5 -4
- package/agent/web/src/index.css +2 -2
- package/agent/web/src/lib/api.ts +819 -52
- package/agent/web/src/lib/dashboard-flags.ts +16 -7
- package/agent/web/src/lib/reasoning-effort.test.ts +48 -0
- package/agent/web/src/lib/reasoning-effort.ts +36 -0
- package/agent/web/src/lib/session-refresh.test.ts +21 -0
- package/agent/web/src/lib/session-refresh.ts +26 -0
- package/agent/web/src/pages/ChannelsPage.tsx +529 -68
- package/agent/web/src/pages/ChatPage.tsx +249 -56
- package/agent/web/src/pages/ConfigPage.tsx +11 -1
- package/agent/web/src/pages/CronPage.tsx +219 -31
- package/agent/web/src/pages/EnvPage.tsx +25 -6
- package/agent/web/src/pages/FilesPage.tsx +525 -0
- package/agent/web/src/pages/McpPage.tsx +80 -3
- package/agent/web/src/pages/ModelsPage.tsx +97 -12
- package/agent/web/src/pages/PluginsPage.tsx +1 -1
- package/agent/web/src/pages/ProfileBuilderPage.tsx +611 -0
- package/agent/web/src/pages/ProfilesPage.tsx +1038 -172
- package/agent/web/src/pages/SessionsPage.tsx +144 -13
- package/agent/web/src/pages/SkillsPage.tsx +851 -70
- package/agent/web/src/pages/SystemPage.tsx +340 -4
- package/agent/web/src/pages/WalletPage.tsx +401 -0
- package/agent/web/src/pages/WebhooksPage.tsx +145 -15
- package/agent/web/src/pages/X402Page.tsx +207 -0
- package/agent/web/src/plugins/registry.ts +28 -11
- package/agent/web/src/plugins/sdk.d.ts +160 -0
- package/agent/web/src/themes/context.tsx +112 -5
- package/agent/web/src/themes/fonts.ts +167 -0
- package/agent/web/src/themes/index.ts +7 -0
- package/agent/web/tsconfig.app.json +0 -1
- package/agent/web/vite.config.ts +1 -8
- package/agent/web/vitest.config.ts +16 -0
- package/package.json +1 -1
- package/agent/apps/desktop/package-lock.json +0 -18363
- package/agent/apps/desktop/src/app/chat/composer/skin-slash-popover.tsx +0 -56
- package/agent/apps/desktop/src/components/assistant-ui/thread-virtualizer.tsx +0 -382
- package/agent/apps/desktop/src/components/assistant-ui/todo-tool.tsx +0 -109
- package/agent/apps/desktop/src/components/chat/generated-image-context.tsx +0 -19
- package/agent/optional-skills/productivity/shop-app/SKILL.md +0 -340
- package/agent/skills/autonomous-ai-agents/kanban-codex-lane/SKILL.md +0 -277
- package/agent/skills/autonomous-ai-agents/kanban-codex-lane/templates/pmb-codex-lane-prompt.md +0 -57
- package/agent/skills/diagramming/DESCRIPTION.md +0 -3
- package/agent/skills/domain/DESCRIPTION.md +0 -24
- package/agent/skills/gifs/DESCRIPTION.md +0 -3
- package/agent/skills/inference-sh/DESCRIPTION.md +0 -19
- package/agent/skills/mcp/DESCRIPTION.md +0 -3
- package/agent/skills/media/spotify/SKILL.md +0 -135
- package/agent/skills/mlops/training/DESCRIPTION.md +0 -3
- package/agent/skills/mlops/vector-databases/DESCRIPTION.md +0 -3
- package/agent/skills/productivity/linear/SKILL.md +0 -380
- package/agent/skills/productivity/linear/scripts/linear_api.py +0 -445
- package/agent/skills/software-development/debugging-hermes-tui-commands/SKILL.md +0 -152
- package/agent/skills/software-development/writing-plans/SKILL.md +0 -297
- package/agent/ui-tui/package-lock.json +0 -7449
- package/agent/ui-tui/packages/hermes-ink/package-lock.json +0 -1289
- package/agent/web/package-lock.json +0 -8887
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/PORT_NOTES.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/prompts/system.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/macaron.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/mono-ink.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/neon.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/prompt-construction.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/style-presets.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/blueprint.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/chalkboard.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/editorial.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/elegant.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/fantasy-animation.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat-doodle.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/ink-notes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/intuition-machine.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/minimal.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/nature.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/notion.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/pixel-art.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/playful.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/retro.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/scientific.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/screen-print.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch-notes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vector-illustration.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vintage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/watercolor.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/usage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/workflow.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/PORT_NOTES.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/analysis-framework.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/chalk.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ink-brush.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ligne-claire.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/manga.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/minimalist.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/realistic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/auto-selection.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/base-prompt.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/character-template.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/cinematic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/dense.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/four-panel.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/mixed.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/splash.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/standard.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/webtoon.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/ohmsha-guide.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/partial-workflows.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/concept-story.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/four-panel.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/ohmsha.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/shoujo.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/wuxia.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/storyboard-template.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/action.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/dramatic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/energetic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/neutral.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/romantic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/vintage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/workflow.md +0 -0
- /package/agent/{skills → optional-skills}/creative/creative-ideation/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/creative-ideation/references/full-prompt-library.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/ATTRIBUTION.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/references/palettes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/__init__.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/palettes.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art_video.py +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/SKILL.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/analysis-modules.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/methods-guide.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/abliteration-config.yaml +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/analysis-study.yaml +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/batch-abliteration.yaml +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/DESCRIPTION.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/examples.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/modules.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/optimizers.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/jailbreak-templates.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/refusal-detection.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/godmode_race.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/load_godmode.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/parseltongue.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill-subtle.json +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill.json +0 -0
- /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/context-budget-discipline.md +0 -0
- /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/gates-taxonomy.md +0 -0
|
@@ -7,12 +7,13 @@ automatic memory extraction, and session management.
|
|
|
7
7
|
Original PR #3369 by Mibayy, rewritten to use the full OpenViking session
|
|
8
8
|
lifecycle instead of read-only search endpoints.
|
|
9
9
|
|
|
10
|
-
Config via environment variables (profile-scoped via each profile's .env)
|
|
10
|
+
Config via environment variables (profile-scoped via each profile's .env)
|
|
11
|
+
or a linked OpenViking CLI config:
|
|
11
12
|
OPENVIKING_ENDPOINT — Server URL (default: http://127.0.0.1:1933)
|
|
12
13
|
OPENVIKING_API_KEY — API key (required for authenticated servers)
|
|
13
|
-
OPENVIKING_ACCOUNT — Tenant account (default: default)
|
|
14
|
-
OPENVIKING_USER — Tenant user (default: default)
|
|
15
|
-
OPENVIKING_AGENT
|
|
14
|
+
OPENVIKING_ACCOUNT — Tenant account for local/trusted mode (default: default)
|
|
15
|
+
OPENVIKING_USER — Tenant user for local/trusted mode (default: default)
|
|
16
|
+
OPENVIKING_AGENT — Hermes peer ID in OpenViking (default: hermes)
|
|
16
17
|
|
|
17
18
|
Capabilities:
|
|
18
19
|
- Automatic memory extraction on session commit (6 categories)
|
|
@@ -29,23 +30,48 @@ import json
|
|
|
29
30
|
import logging
|
|
30
31
|
import mimetypes
|
|
31
32
|
import os
|
|
33
|
+
import re
|
|
34
|
+
import shutil
|
|
35
|
+
import stat
|
|
36
|
+
import subprocess
|
|
32
37
|
import tempfile
|
|
33
38
|
import threading
|
|
39
|
+
import time
|
|
34
40
|
import uuid
|
|
35
41
|
import zipfile
|
|
42
|
+
from dataclasses import dataclass, replace
|
|
36
43
|
from pathlib import Path
|
|
37
|
-
from typing import Any, Dict, List, Optional
|
|
44
|
+
from typing import Any, Callable, Dict, List, Optional, Set
|
|
38
45
|
from urllib.parse import urlparse
|
|
39
46
|
from urllib.request import url2pathname
|
|
40
47
|
|
|
48
|
+
from agent.message_content import flatten_message_text
|
|
41
49
|
from agent.memory_provider import MemoryProvider
|
|
50
|
+
from agent.skill_commands import extract_user_instruction_from_skill_message
|
|
42
51
|
from tools.registry import tool_error
|
|
52
|
+
from utils import atomic_json_write, env_var_enabled
|
|
43
53
|
|
|
44
54
|
logger = logging.getLogger(__name__)
|
|
45
55
|
|
|
46
56
|
_DEFAULT_ENDPOINT = "http://127.0.0.1:1933"
|
|
57
|
+
_OPENVIKING_SERVICE_ENDPOINT = "https://api.vikingdb.cn-beijing.volces.com/openviking"
|
|
58
|
+
_DEFAULT_AGENT = "hermes"
|
|
59
|
+
_AGENT_PROMPT_LABEL = "Hermes peer ID in OpenViking"
|
|
60
|
+
_OVCLI_CONFIG_ENV = "OPENVIKING_CLI_CONFIG_FILE"
|
|
61
|
+
_OVCLI_DEFAULT_RELATIVE_PATH = ".openviking/ovcli.conf"
|
|
62
|
+
_OVCLI_SAVED_PREFIX = "ovcli.conf."
|
|
63
|
+
_OPENVIKING_ENV_KEYS = (
|
|
64
|
+
"OPENVIKING_ENDPOINT",
|
|
65
|
+
"OPENVIKING_API_KEY",
|
|
66
|
+
"OPENVIKING_ACCOUNT",
|
|
67
|
+
"OPENVIKING_USER",
|
|
68
|
+
"OPENVIKING_AGENT",
|
|
69
|
+
)
|
|
47
70
|
_TIMEOUT = 30.0
|
|
71
|
+
_SESSION_DRAIN_TIMEOUT = 10.0
|
|
72
|
+
_DEFERRED_COMMIT_TIMEOUT = (_TIMEOUT * 2) + 5.0
|
|
48
73
|
_REMOTE_RESOURCE_PREFIXES = ("http://", "https://", "git@", "ssh://", "git://")
|
|
74
|
+
_SYNC_TRACE_ENV = "HERMES_OPENVIKING_SYNC_TRACE"
|
|
49
75
|
|
|
50
76
|
# Maps the viking_remember `category` enum to a viking:// subdirectory.
|
|
51
77
|
# Keep in sync with REMEMBER_SCHEMA.parameters.properties.category.enum.
|
|
@@ -65,6 +91,83 @@ _MEMORY_WRITE_TARGET_SUBDIR_MAP = {
|
|
|
65
91
|
"user": "preferences",
|
|
66
92
|
"memory": "patterns",
|
|
67
93
|
}
|
|
94
|
+
_LOCAL_OPENVIKING_HOSTS = {"localhost", "127.0.0.1", "::1"}
|
|
95
|
+
_LOCAL_OPENVIKING_AUTOSTART_TIMEOUT = 60.0
|
|
96
|
+
_OPENVIKING_SERVER_LOG_RELATIVE_PATH = Path("logs") / "openviking-server.log"
|
|
97
|
+
_OPENVIKING_RESPONDED_FAILURE_PREFIX = "OpenViking server responded"
|
|
98
|
+
_SETUP_CANCELLED = object()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class _OvcliProfile:
|
|
103
|
+
source: str
|
|
104
|
+
name: str
|
|
105
|
+
path: Path
|
|
106
|
+
data: dict
|
|
107
|
+
values: dict
|
|
108
|
+
is_active: bool = False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class _OpenVikingHTTPError(RuntimeError):
|
|
112
|
+
def __init__(self, message: str, status_code: Optional[int] = None):
|
|
113
|
+
super().__init__(message)
|
|
114
|
+
self.status_code = status_code
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _sanitize_openviking_error_message(message: str, status_code: Optional[int] = None) -> str:
|
|
118
|
+
text = (message or "").strip()
|
|
119
|
+
status = f"HTTP {status_code}" if status_code else "HTTP error"
|
|
120
|
+
looks_like_html = bool(re.search(r"^\s*<(!doctype|html|head|body)\b", text, flags=re.IGNORECASE))
|
|
121
|
+
if looks_like_html:
|
|
122
|
+
title_match = re.search(r"<title[^>]*>(.*?)</title>", text, flags=re.IGNORECASE | re.DOTALL)
|
|
123
|
+
if title_match:
|
|
124
|
+
title = re.sub(r"\s+", " ", title_match.group(1)).strip()
|
|
125
|
+
if "|" in title:
|
|
126
|
+
title = title.split("|", 1)[1].strip()
|
|
127
|
+
if status_code and title.startswith(f"{status_code}:"):
|
|
128
|
+
title = title.split(":", 1)[1].strip()
|
|
129
|
+
if title:
|
|
130
|
+
return f"{status}: {title}"
|
|
131
|
+
return f"{status}: OpenViking endpoint returned an HTML error page."
|
|
132
|
+
|
|
133
|
+
if len(text) > 300:
|
|
134
|
+
return text[:297].rstrip() + "..."
|
|
135
|
+
return text or status
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _format_openviking_exception(error: Exception) -> str:
|
|
139
|
+
status_code = None
|
|
140
|
+
if isinstance(error, _OpenVikingHTTPError):
|
|
141
|
+
status_code = error.status_code
|
|
142
|
+
else:
|
|
143
|
+
response = getattr(error, "response", None)
|
|
144
|
+
status_code = getattr(response, "status_code", None)
|
|
145
|
+
return _sanitize_openviking_error_message(str(error), status_code)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _derive_openviking_user_text(content: Any) -> str:
|
|
149
|
+
"""Strip Hermes slash-skill scaffolding before sending content to OpenViking.
|
|
150
|
+
|
|
151
|
+
Defense-in-depth: MemoryManager already strips skill scaffolding for the
|
|
152
|
+
whole provider fan-out (see ``MemoryManager._strip_skill_scaffolding``), so
|
|
153
|
+
in normal operation this receives already-clean text and passes it through
|
|
154
|
+
unchanged. It stays here so OpenViking is correct if its hooks are ever
|
|
155
|
+
invoked outside the manager. Delegates to the canonical extractor in
|
|
156
|
+
``agent.skill_commands`` — no duplicated marker literals, no drift risk.
|
|
157
|
+
"""
|
|
158
|
+
return extract_user_instruction_from_skill_message(content) or ""
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _sync_trace_enabled() -> bool:
|
|
162
|
+
return env_var_enabled(_SYNC_TRACE_ENV)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _preview(value: Any, limit: int = 160) -> str:
|
|
166
|
+
text = "" if value is None else str(value)
|
|
167
|
+
text = text.replace("\n", "\\n")
|
|
168
|
+
if len(text) > limit:
|
|
169
|
+
return text[:limit] + "..."
|
|
170
|
+
return text
|
|
68
171
|
|
|
69
172
|
|
|
70
173
|
# ---------------------------------------------------------------------------
|
|
@@ -108,31 +211,32 @@ class _VikingClient:
|
|
|
108
211
|
"""Thin HTTP client for the OpenViking REST API."""
|
|
109
212
|
|
|
110
213
|
def __init__(self, endpoint: str, api_key: str = "",
|
|
111
|
-
account: str =
|
|
214
|
+
account: Optional[str] = None, user: Optional[str] = None,
|
|
215
|
+
agent: Optional[str] = None):
|
|
112
216
|
self._endpoint = endpoint.rstrip("/")
|
|
113
217
|
self._api_key = api_key
|
|
218
|
+
# Account/user are local/trusted-mode tenant identity. API-key requests
|
|
219
|
+
# omit these headers by default; trusted-mode retry may send them only
|
|
220
|
+
# after OpenViking explicitly asks for asserted tenant identity.
|
|
114
221
|
self._account = account or os.environ.get("OPENVIKING_ACCOUNT", "default")
|
|
115
222
|
self._user = user or os.environ.get("OPENVIKING_USER", "default")
|
|
116
|
-
self._agent = agent
|
|
223
|
+
self._agent = agent if agent is not None else os.environ.get("OPENVIKING_AGENT", _DEFAULT_AGENT)
|
|
117
224
|
self._httpx = _get_httpx()
|
|
118
225
|
if self._httpx is None:
|
|
119
226
|
raise ImportError("httpx is required for OpenViking: pip install httpx")
|
|
120
227
|
|
|
121
|
-
def _headers(self) -> dict:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
h["X-OpenViking-Account"] = self._account
|
|
134
|
-
if self._user:
|
|
135
|
-
h["X-OpenViking-User"] = self._user
|
|
228
|
+
def _headers(self, *, include_tenant: bool | None = None) -> dict:
|
|
229
|
+
if include_tenant is None:
|
|
230
|
+
include_tenant = not bool(self._api_key)
|
|
231
|
+
|
|
232
|
+
h = {"Content-Type": "application/json"}
|
|
233
|
+
if self._agent:
|
|
234
|
+
h["X-OpenViking-Actor-Peer"] = self._agent
|
|
235
|
+
if include_tenant:
|
|
236
|
+
if self._account:
|
|
237
|
+
h["X-OpenViking-Account"] = self._account
|
|
238
|
+
if self._user:
|
|
239
|
+
h["X-OpenViking-User"] = self._user
|
|
136
240
|
if self._api_key:
|
|
137
241
|
h["X-API-Key"] = self._api_key
|
|
138
242
|
h["Authorization"] = "Bearer " + self._api_key
|
|
@@ -141,11 +245,33 @@ class _VikingClient:
|
|
|
141
245
|
def _url(self, path: str) -> str:
|
|
142
246
|
return f"{self._endpoint}{path}"
|
|
143
247
|
|
|
144
|
-
def _multipart_headers(self) -> dict:
|
|
145
|
-
headers = self._headers()
|
|
248
|
+
def _multipart_headers(self, *, include_tenant: bool | None = None) -> dict:
|
|
249
|
+
headers = self._headers(include_tenant=include_tenant)
|
|
146
250
|
headers.pop("Content-Type", None)
|
|
147
251
|
return headers
|
|
148
252
|
|
|
253
|
+
@staticmethod
|
|
254
|
+
def _needs_trusted_identity_retry(exc: Exception) -> bool:
|
|
255
|
+
message = str(exc)
|
|
256
|
+
return (
|
|
257
|
+
"Trusted mode requests must include X-OpenViking-Account" in message
|
|
258
|
+
or "Trusted mode requests must include X-OpenViking-User" in message
|
|
259
|
+
or "Trusted mode requests must include X-OpenViking-Account or explicit account_id" in message
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
def _send_with_trusted_identity_retry(self, send, *, multipart: bool = False) -> dict:
|
|
263
|
+
try:
|
|
264
|
+
headers = self._multipart_headers() if multipart else self._headers()
|
|
265
|
+
return self._parse_response(send(headers))
|
|
266
|
+
except Exception as exc:
|
|
267
|
+
if not self._api_key or not self._needs_trusted_identity_retry(exc):
|
|
268
|
+
raise
|
|
269
|
+
headers = (
|
|
270
|
+
self._multipart_headers(include_tenant=True)
|
|
271
|
+
if multipart else self._headers(include_tenant=True)
|
|
272
|
+
)
|
|
273
|
+
return self._parse_response(send(headers))
|
|
274
|
+
|
|
149
275
|
def _parse_response(self, resp) -> dict:
|
|
150
276
|
try:
|
|
151
277
|
data = resp.json()
|
|
@@ -153,15 +279,19 @@ class _VikingClient:
|
|
|
153
279
|
data = None
|
|
154
280
|
|
|
155
281
|
if resp.status_code >= 400:
|
|
282
|
+
message = _sanitize_openviking_error_message(
|
|
283
|
+
getattr(resp, "text", ""),
|
|
284
|
+
resp.status_code,
|
|
285
|
+
)
|
|
156
286
|
if isinstance(data, dict):
|
|
157
287
|
error = data.get("error")
|
|
158
288
|
if isinstance(error, dict):
|
|
159
289
|
code = error.get("code", "HTTP_ERROR")
|
|
160
|
-
message = error.get(
|
|
161
|
-
raise
|
|
290
|
+
message = f"{code}: {error.get('message', message)}"
|
|
291
|
+
raise _OpenVikingHTTPError(message, resp.status_code)
|
|
162
292
|
if data.get("status") == "error":
|
|
163
|
-
raise
|
|
164
|
-
resp.
|
|
293
|
+
raise _OpenVikingHTTPError(str(data), resp.status_code)
|
|
294
|
+
raise _OpenVikingHTTPError(message or f"HTTP {resp.status_code}", resp.status_code)
|
|
165
295
|
|
|
166
296
|
if isinstance(data, dict) and data.get("status") == "error":
|
|
167
297
|
error = data.get("error")
|
|
@@ -176,28 +306,33 @@ class _VikingClient:
|
|
|
176
306
|
return data
|
|
177
307
|
|
|
178
308
|
def get(self, path: str, **kwargs) -> dict:
|
|
179
|
-
|
|
180
|
-
|
|
309
|
+
return self._send_with_trusted_identity_retry(
|
|
310
|
+
lambda headers: self._httpx.get(
|
|
311
|
+
self._url(path), headers=headers, timeout=_TIMEOUT, **kwargs
|
|
312
|
+
)
|
|
181
313
|
)
|
|
182
|
-
return self._parse_response(resp)
|
|
183
314
|
|
|
184
315
|
def post(self, path: str, payload: dict = None, **kwargs) -> dict:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
316
|
+
return self._send_with_trusted_identity_retry(
|
|
317
|
+
lambda headers: self._httpx.post(
|
|
318
|
+
self._url(path), json=payload or {}, headers=headers,
|
|
319
|
+
timeout=_TIMEOUT, **kwargs
|
|
320
|
+
)
|
|
188
321
|
)
|
|
189
|
-
return self._parse_response(resp)
|
|
190
322
|
|
|
191
323
|
def upload_temp_file(self, file_path: Path) -> str:
|
|
192
324
|
mime_type = mimetypes.guess_type(file_path.name)[0] or "application/octet-stream"
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
325
|
+
|
|
326
|
+
def _send(headers):
|
|
327
|
+
with file_path.open("rb") as f:
|
|
328
|
+
return self._httpx.post(
|
|
329
|
+
self._url("/api/v1/resources/temp_upload"),
|
|
330
|
+
files={"file": (file_path.name, f, mime_type)},
|
|
331
|
+
headers=headers,
|
|
332
|
+
timeout=_TIMEOUT,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
data = self._send_with_trusted_identity_retry(_send, multipart=True)
|
|
201
336
|
result = data.get("result", {})
|
|
202
337
|
temp_file_id = result.get("temp_file_id", "")
|
|
203
338
|
if not temp_file_id:
|
|
@@ -213,6 +348,20 @@ class _VikingClient:
|
|
|
213
348
|
except Exception:
|
|
214
349
|
return False
|
|
215
350
|
|
|
351
|
+
def health_payload(self) -> dict:
|
|
352
|
+
resp = self._httpx.get(
|
|
353
|
+
self._url("/health"), headers=self._headers(), timeout=3.0
|
|
354
|
+
)
|
|
355
|
+
return self._parse_response(resp)
|
|
356
|
+
|
|
357
|
+
def validate_auth(self) -> dict:
|
|
358
|
+
"""Validate authenticated OpenViking access without mutating state."""
|
|
359
|
+
return self.get("/api/v1/system/status")
|
|
360
|
+
|
|
361
|
+
def validate_root_access(self) -> dict:
|
|
362
|
+
"""Validate ROOT access against a read-only admin endpoint."""
|
|
363
|
+
return self.get("/api/v1/admin/accounts")
|
|
364
|
+
|
|
216
365
|
|
|
217
366
|
# ---------------------------------------------------------------------------
|
|
218
367
|
# Tool schemas
|
|
@@ -353,6 +502,25 @@ ADD_RESOURCE_SCHEMA = {
|
|
|
353
502
|
}
|
|
354
503
|
|
|
355
504
|
|
|
505
|
+
# Recall tools (read-only) whose results we never re-ingest into OpenViking —
|
|
506
|
+
# echoing recalled memory back into the session transcript would re-store it.
|
|
507
|
+
# Write tools (viking_remember / viking_add_resource) are intentionally NOT
|
|
508
|
+
# here. Derived from the canonical schema names so renames can't desync.
|
|
509
|
+
_OPENVIKING_RECALL_TOOL_NAMES = {
|
|
510
|
+
SEARCH_SCHEMA["name"],
|
|
511
|
+
READ_SCHEMA["name"],
|
|
512
|
+
BROWSE_SCHEMA["name"],
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
# Canonical tool_status values emitted in OpenViking batch tool parts.
|
|
516
|
+
_TOOL_STATUS_COMPLETED = "completed"
|
|
517
|
+
_TOOL_STATUS_ERROR = "error"
|
|
518
|
+
_TOOL_STATUS_PENDING = "pending"
|
|
519
|
+
# Inbound status aliases (from varied tool-result shapes) -> canonical above.
|
|
520
|
+
_TOOL_STATUS_ERROR_ALIASES = {"error", "failed", "failure"}
|
|
521
|
+
_TOOL_STATUS_COMPLETED_ALIASES = {"completed", "complete", "success", "succeeded"}
|
|
522
|
+
|
|
523
|
+
|
|
356
524
|
def _zip_directory(dir_path: Path) -> Path:
|
|
357
525
|
"""Create a temporary zip file containing a directory tree."""
|
|
358
526
|
root = dir_path.resolve()
|
|
@@ -405,6 +573,1104 @@ def _path_from_file_uri(uri: str) -> Path | str:
|
|
|
405
573
|
return Path(url2pathname(parsed.path)).expanduser()
|
|
406
574
|
|
|
407
575
|
|
|
576
|
+
def _clean_config_value(value: Any) -> str:
|
|
577
|
+
return value.strip() if isinstance(value, str) else ""
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _default_ovcli_config_path() -> Path:
|
|
581
|
+
return Path.home() / _OVCLI_DEFAULT_RELATIVE_PATH
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _resolve_ovcli_config_path(config_path: str = "") -> Path:
|
|
585
|
+
env_path = os.environ.get(_OVCLI_CONFIG_ENV, "").strip()
|
|
586
|
+
if env_path:
|
|
587
|
+
return Path(env_path).expanduser()
|
|
588
|
+
if config_path:
|
|
589
|
+
return Path(config_path).expanduser()
|
|
590
|
+
return _default_ovcli_config_path()
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _ovcli_config_dir() -> Path:
|
|
594
|
+
return _default_ovcli_config_path().parent
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _load_ovcli_config(path: Optional[Path] = None) -> dict:
|
|
598
|
+
config_path = path or _resolve_ovcli_config_path()
|
|
599
|
+
if not config_path.exists():
|
|
600
|
+
return {}
|
|
601
|
+
with config_path.open(encoding="utf-8") as f:
|
|
602
|
+
data = json.load(f)
|
|
603
|
+
if not isinstance(data, dict):
|
|
604
|
+
raise ValueError(f"OpenViking CLI config must be a JSON object: {config_path}")
|
|
605
|
+
return data
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _connection_values_from_ovcli(data: dict) -> dict:
|
|
609
|
+
api_key = _clean_config_value(data.get("api_key")) or _clean_config_value(data.get("root_api_key"))
|
|
610
|
+
root_api_key = _clean_config_value(data.get("root_api_key"))
|
|
611
|
+
send_identity = not api_key or api_key == root_api_key
|
|
612
|
+
account = _clean_config_value(data.get("account") or data.get("account_id"))
|
|
613
|
+
user = _clean_config_value(data.get("user") or data.get("user_id"))
|
|
614
|
+
return {
|
|
615
|
+
"endpoint": _normalize_openviking_url(data.get("url")),
|
|
616
|
+
"api_key": api_key,
|
|
617
|
+
"root_api_key": root_api_key,
|
|
618
|
+
"account": account if send_identity else "",
|
|
619
|
+
"user": user if send_identity else "",
|
|
620
|
+
"agent": _clean_config_value(data.get("actor_peer_id") or data.get("agent_id")),
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _is_valid_ovcli_profile_name(name: str) -> bool:
|
|
625
|
+
if not name or name.strip() != name or name.startswith("."):
|
|
626
|
+
return False
|
|
627
|
+
if "/" in name or "\\" in name:
|
|
628
|
+
return False
|
|
629
|
+
return all(ch.isascii() and (ch.isalnum() or ch in {"-", "_"}) for ch in name)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _validate_openviking_identity_value(value: str, *, field: str) -> tuple[bool, str, str]:
|
|
633
|
+
label = "Account ID" if field == "account" else "User ID"
|
|
634
|
+
identifier = "account_id" if field == "account" else "user_id"
|
|
635
|
+
trimmed = value.strip()
|
|
636
|
+
if not trimmed:
|
|
637
|
+
return False, f"{label} cannot be empty.", ""
|
|
638
|
+
if trimmed != value:
|
|
639
|
+
return False, f"{label} cannot start or end with whitespace.", ""
|
|
640
|
+
if field == "account" and trimmed.startswith("_"):
|
|
641
|
+
return False, "Account ID cannot start with '_'.", ""
|
|
642
|
+
if not all(ch.isascii() and (ch.isalnum() or ch in {"_", "-", ".", "@"}) for ch in trimmed):
|
|
643
|
+
return False, f"{label} can only contain letters, numbers, '_', '-', '.', and '@'.", ""
|
|
644
|
+
if trimmed.count("@") > 1:
|
|
645
|
+
return False, f"{identifier} must have at most one '@'.", ""
|
|
646
|
+
return True, "", trimmed
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _normalize_openviking_url(url: str) -> str:
|
|
650
|
+
trimmed = _clean_config_value(url).rstrip("/")
|
|
651
|
+
if not trimmed:
|
|
652
|
+
return _DEFAULT_ENDPOINT
|
|
653
|
+
lower = trimmed.lower()
|
|
654
|
+
if lower in {"::1", "[::1]"}:
|
|
655
|
+
return "http://[::1]:1933"
|
|
656
|
+
if lower.startswith("[::1]:"):
|
|
657
|
+
return f"http://[::1]:{trimmed.rsplit(':', 1)[1]}"
|
|
658
|
+
if lower.startswith("::1:"):
|
|
659
|
+
return f"http://[::1]:{trimmed.rsplit(':', 1)[1]}"
|
|
660
|
+
if "://" in trimmed:
|
|
661
|
+
return trimmed
|
|
662
|
+
host, _sep, port = trimmed.partition(":")
|
|
663
|
+
if host.lower() in {"localhost", "127.0.0.1"}:
|
|
664
|
+
return f"http://{host}:{port or '1933'}"
|
|
665
|
+
return trimmed
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _load_profile(path: Path, *, source: str, name: str) -> Optional[_OvcliProfile]:
|
|
669
|
+
try:
|
|
670
|
+
data = _load_ovcli_config(path)
|
|
671
|
+
except Exception as e:
|
|
672
|
+
logger.debug("Skipping invalid OpenViking CLI config %s: %s", path, e)
|
|
673
|
+
return None
|
|
674
|
+
return _OvcliProfile(
|
|
675
|
+
source=source,
|
|
676
|
+
name=name,
|
|
677
|
+
path=path,
|
|
678
|
+
data=data,
|
|
679
|
+
values=_connection_values_from_ovcli(data),
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _profile_identity(path: Path) -> str:
|
|
684
|
+
try:
|
|
685
|
+
return str(path.expanduser().resolve())
|
|
686
|
+
except OSError:
|
|
687
|
+
return str(path.expanduser())
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _profiles_equivalent(left: _OvcliProfile, right: _OvcliProfile) -> bool:
|
|
691
|
+
return left.values == right.values
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _discover_ovcli_profiles() -> list[_OvcliProfile]:
|
|
695
|
+
profiles: list[_OvcliProfile] = []
|
|
696
|
+
seen_paths: set[str] = set()
|
|
697
|
+
|
|
698
|
+
def add(path: Path, *, source: str, name: str) -> None:
|
|
699
|
+
if not path.exists() or not path.is_file():
|
|
700
|
+
return
|
|
701
|
+
identity = _profile_identity(path)
|
|
702
|
+
if identity in seen_paths:
|
|
703
|
+
return
|
|
704
|
+
profile = _load_profile(path, source=source, name=name)
|
|
705
|
+
if profile is None:
|
|
706
|
+
return
|
|
707
|
+
seen_paths.add(identity)
|
|
708
|
+
profiles.append(profile)
|
|
709
|
+
|
|
710
|
+
env_path = os.environ.get(_OVCLI_CONFIG_ENV, "").strip()
|
|
711
|
+
if env_path:
|
|
712
|
+
add(Path(env_path).expanduser(), source="env", name=_OVCLI_CONFIG_ENV)
|
|
713
|
+
|
|
714
|
+
active_path = _default_ovcli_config_path()
|
|
715
|
+
active_profile = _load_profile(active_path, source="active", name="active") if active_path.exists() else None
|
|
716
|
+
|
|
717
|
+
config_dir = _ovcli_config_dir()
|
|
718
|
+
saved_start = len(profiles)
|
|
719
|
+
if config_dir.exists():
|
|
720
|
+
for path in sorted(config_dir.iterdir(), key=lambda item: item.name):
|
|
721
|
+
if not path.is_file():
|
|
722
|
+
continue
|
|
723
|
+
name = path.name.removeprefix(_OVCLI_SAVED_PREFIX)
|
|
724
|
+
if name == path.name or name == "bak" or not _is_valid_ovcli_profile_name(name):
|
|
725
|
+
continue
|
|
726
|
+
add(path, source="saved", name=name)
|
|
727
|
+
|
|
728
|
+
if active_profile is not None:
|
|
729
|
+
marked_active = False
|
|
730
|
+
for idx in range(saved_start, len(profiles)):
|
|
731
|
+
if profiles[idx].source == "saved" and _profiles_equivalent(profiles[idx], active_profile):
|
|
732
|
+
profiles[idx] = replace(profiles[idx], is_active=True)
|
|
733
|
+
marked_active = True
|
|
734
|
+
break
|
|
735
|
+
has_env_profile = any(profile.source == "env" for profile in profiles)
|
|
736
|
+
has_saved_profile = any(profile.source == "saved" for profile in profiles)
|
|
737
|
+
active_identity = _profile_identity(active_profile.path)
|
|
738
|
+
if not marked_active and not has_env_profile and not has_saved_profile and active_identity not in seen_paths:
|
|
739
|
+
profiles.append(active_profile)
|
|
740
|
+
|
|
741
|
+
return profiles
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def _is_local_openviking_url(value: str) -> bool:
|
|
745
|
+
candidate = _normalize_openviking_url(value)
|
|
746
|
+
if not candidate:
|
|
747
|
+
return False
|
|
748
|
+
if "://" not in candidate:
|
|
749
|
+
candidate = f"//{candidate}"
|
|
750
|
+
parsed = urlparse(candidate)
|
|
751
|
+
scheme = (parsed.scheme or "http").lower()
|
|
752
|
+
return scheme == "http" and (parsed.hostname or "").lower() in _LOCAL_OPENVIKING_HOSTS
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _load_hermes_openviking_config() -> dict:
|
|
756
|
+
try:
|
|
757
|
+
from hermes_cli.config import load_config
|
|
758
|
+
|
|
759
|
+
config = load_config()
|
|
760
|
+
memory_config = config.get("memory", {}) if isinstance(config, dict) else {}
|
|
761
|
+
provider_config = memory_config.get("openviking", {}) if isinstance(memory_config, dict) else {}
|
|
762
|
+
return dict(provider_config) if isinstance(provider_config, dict) else {}
|
|
763
|
+
except Exception:
|
|
764
|
+
return {}
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _env_value(name: str) -> Optional[str]:
|
|
768
|
+
return os.environ[name].strip() if name in os.environ else None
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _first_nonempty(*values: Optional[str], default: str = "") -> str:
|
|
772
|
+
for value in values:
|
|
773
|
+
if value:
|
|
774
|
+
return value
|
|
775
|
+
return default
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def _resolve_connection_settings(provider_config: Optional[dict] = None) -> dict:
|
|
779
|
+
provider_config = dict(provider_config or {})
|
|
780
|
+
ovcli_values: dict = {}
|
|
781
|
+
if provider_config.get("use_ovcli_config"):
|
|
782
|
+
ovcli_path = _resolve_ovcli_config_path(str(provider_config.get("ovcli_config_path") or ""))
|
|
783
|
+
ovcli_values = _connection_values_from_ovcli(_load_ovcli_config(ovcli_path))
|
|
784
|
+
|
|
785
|
+
endpoint_env = _env_value("OPENVIKING_ENDPOINT")
|
|
786
|
+
api_key_env = _env_value("OPENVIKING_API_KEY")
|
|
787
|
+
account_env = _env_value("OPENVIKING_ACCOUNT")
|
|
788
|
+
user_env = _env_value("OPENVIKING_USER")
|
|
789
|
+
agent_env = _env_value("OPENVIKING_AGENT")
|
|
790
|
+
|
|
791
|
+
return {
|
|
792
|
+
"endpoint": _first_nonempty(endpoint_env, ovcli_values.get("endpoint"), default=_DEFAULT_ENDPOINT),
|
|
793
|
+
"api_key": api_key_env if api_key_env is not None else ovcli_values.get("api_key", ""),
|
|
794
|
+
"account": account_env if account_env is not None else ovcli_values.get("account", ""),
|
|
795
|
+
"user": user_env if user_env is not None else ovcli_values.get("user", ""),
|
|
796
|
+
"agent": _first_nonempty(agent_env, ovcli_values.get("agent"), default=_DEFAULT_AGENT),
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def _env_writes_from_connection_values(values: dict) -> dict:
|
|
801
|
+
writes = {}
|
|
802
|
+
mapping = {
|
|
803
|
+
"OPENVIKING_ENDPOINT": "endpoint",
|
|
804
|
+
"OPENVIKING_API_KEY": "api_key",
|
|
805
|
+
"OPENVIKING_ACCOUNT": "account",
|
|
806
|
+
"OPENVIKING_USER": "user",
|
|
807
|
+
"OPENVIKING_AGENT": "agent",
|
|
808
|
+
}
|
|
809
|
+
for env_key, value_key in mapping.items():
|
|
810
|
+
value = _clean_config_value(values.get(value_key))
|
|
811
|
+
if value:
|
|
812
|
+
writes[env_key] = value
|
|
813
|
+
return writes
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _restrict_secret_file_permissions(path: Path) -> None:
|
|
817
|
+
try:
|
|
818
|
+
path.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
|
819
|
+
except OSError as e:
|
|
820
|
+
logger.debug("Could not restrict permissions on %s: %s", path, e)
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def _precreate_secret_file(path: Path) -> None:
|
|
824
|
+
"""Create (or tighten) a secret-bearing file with 0600 BEFORE writing.
|
|
825
|
+
|
|
826
|
+
Writing the file first and chmod-ing afterwards leaves a window where a
|
|
827
|
+
freshly-created file is world-readable under the default umask (e.g. 0644),
|
|
828
|
+
briefly exposing the api_key/root_api_key. Pre-creating with 0600 closes
|
|
829
|
+
that window; an existing file is tightened to 0600 here too.
|
|
830
|
+
"""
|
|
831
|
+
try:
|
|
832
|
+
if not path.exists():
|
|
833
|
+
os.close(os.open(str(path), os.O_CREAT | os.O_WRONLY, 0o600))
|
|
834
|
+
_restrict_secret_file_permissions(path)
|
|
835
|
+
except OSError as e:
|
|
836
|
+
logger.debug("Could not pre-create secret file %s: %s", path, e)
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def _write_env_vars(env_path: Path, env_writes: dict, remove_keys: tuple[str, ...] = ()) -> None:
|
|
840
|
+
env_path.parent.mkdir(parents=True, exist_ok=True)
|
|
841
|
+
remove_set = set(remove_keys) - set(env_writes)
|
|
842
|
+
existing_lines = env_path.read_text(encoding="utf-8").splitlines() if env_path.exists() else []
|
|
843
|
+
updated_keys = set()
|
|
844
|
+
new_lines = []
|
|
845
|
+
for line in existing_lines:
|
|
846
|
+
key_match = line.split("=", 1)[0].strip() if "=" in line else ""
|
|
847
|
+
if key_match in remove_set:
|
|
848
|
+
continue
|
|
849
|
+
if key_match in env_writes:
|
|
850
|
+
new_lines.append(f"{key_match}={env_writes[key_match]}")
|
|
851
|
+
updated_keys.add(key_match)
|
|
852
|
+
else:
|
|
853
|
+
new_lines.append(line)
|
|
854
|
+
for key, val in env_writes.items():
|
|
855
|
+
if key not in updated_keys:
|
|
856
|
+
new_lines.append(f"{key}={val}")
|
|
857
|
+
# Pre-create with 0600 so secrets are never briefly world-readable.
|
|
858
|
+
_precreate_secret_file(env_path)
|
|
859
|
+
env_path.write_text("\n".join(new_lines) + ("\n" if new_lines else ""), encoding="utf-8")
|
|
860
|
+
_restrict_secret_file_permissions(env_path)
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def _remember_ovcli_path(provider_config: dict, ovcli_path: Path) -> None:
|
|
864
|
+
default_path = _default_ovcli_config_path().expanduser()
|
|
865
|
+
if os.environ.get(_OVCLI_CONFIG_ENV, "").strip() or ovcli_path.expanduser() != default_path:
|
|
866
|
+
provider_config["ovcli_config_path"] = str(ovcli_path)
|
|
867
|
+
else:
|
|
868
|
+
provider_config.pop("ovcli_config_path", None)
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def _ovcli_data_from_connection_values(values: dict) -> dict:
|
|
872
|
+
data = {"url": _normalize_openviking_url(_clean_config_value(values.get("endpoint")) or _DEFAULT_ENDPOINT)}
|
|
873
|
+
api_key = _clean_config_value(values.get("api_key"))
|
|
874
|
+
root_api_key = _clean_config_value(values.get("root_api_key"))
|
|
875
|
+
account = _clean_config_value(values.get("account"))
|
|
876
|
+
user = _clean_config_value(values.get("user"))
|
|
877
|
+
agent = _clean_config_value(values.get("agent")) or _DEFAULT_AGENT
|
|
878
|
+
if api_key:
|
|
879
|
+
data["api_key"] = api_key
|
|
880
|
+
if root_api_key:
|
|
881
|
+
data["root_api_key"] = root_api_key
|
|
882
|
+
if account:
|
|
883
|
+
data["account"] = account
|
|
884
|
+
if user:
|
|
885
|
+
data["user"] = user
|
|
886
|
+
if agent:
|
|
887
|
+
data["actor_peer_id"] = agent
|
|
888
|
+
return data
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _write_ovcli_config(path: Path, values: dict) -> None:
|
|
892
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
893
|
+
# atomic_json_write creates the temp file with mode 0o600 and os.replace()s
|
|
894
|
+
# it into place — no half-written config on crash and no chmod-after-write
|
|
895
|
+
# TOCTOU window for the api_key/root_api_key it carries.
|
|
896
|
+
atomic_json_write(path, _ovcli_data_from_connection_values(values), mode=0o600)
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def _validate_openviking_reachability(endpoint: str) -> tuple[bool, str]:
|
|
900
|
+
endpoint = _normalize_openviking_url(endpoint)
|
|
901
|
+
try:
|
|
902
|
+
client = _VikingClient(endpoint)
|
|
903
|
+
if hasattr(client, "health_payload"):
|
|
904
|
+
payload = client.health_payload()
|
|
905
|
+
if payload.get("healthy") is False:
|
|
906
|
+
return False, "OpenViking server responded but reported unhealthy status."
|
|
907
|
+
if payload:
|
|
908
|
+
return True, ""
|
|
909
|
+
elif client.health():
|
|
910
|
+
return True, ""
|
|
911
|
+
except Exception as e:
|
|
912
|
+
if _status_code_from_error(e) is not None:
|
|
913
|
+
return False, f"OpenViking server responded with {_format_openviking_exception(e)}."
|
|
914
|
+
return False, f"OpenViking server is not reachable at {endpoint}: {_format_openviking_exception(e)}"
|
|
915
|
+
return False, f"OpenViking server is not reachable at {endpoint}."
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def _validate_openviking_auth(values: dict) -> tuple[bool, str]:
|
|
919
|
+
endpoint = _normalize_openviking_url(values.get("endpoint"))
|
|
920
|
+
try:
|
|
921
|
+
client = _VikingClient(
|
|
922
|
+
endpoint,
|
|
923
|
+
_clean_config_value(values.get("api_key")),
|
|
924
|
+
account=_clean_config_value(values.get("account")),
|
|
925
|
+
user=_clean_config_value(values.get("user")),
|
|
926
|
+
agent=_clean_config_value(values.get("agent")) or _DEFAULT_AGENT,
|
|
927
|
+
)
|
|
928
|
+
client.validate_auth()
|
|
929
|
+
except Exception as e:
|
|
930
|
+
return False, f"OpenViking authentication validation failed: {_format_openviking_exception(e)}"
|
|
931
|
+
return True, ""
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def _validate_openviking_root_access(values: dict) -> tuple[bool, str]:
|
|
935
|
+
endpoint = _normalize_openviking_url(values.get("endpoint"))
|
|
936
|
+
try:
|
|
937
|
+
client = _VikingClient(
|
|
938
|
+
endpoint,
|
|
939
|
+
_clean_config_value(values.get("api_key")),
|
|
940
|
+
agent=_clean_config_value(values.get("agent")) or _DEFAULT_AGENT,
|
|
941
|
+
)
|
|
942
|
+
client.validate_root_access()
|
|
943
|
+
except Exception as e:
|
|
944
|
+
return False, f"OpenViking root API key validation failed: {_format_openviking_exception(e)}"
|
|
945
|
+
return True, ""
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def _validate_openviking_user_key_scope(values: dict) -> tuple[bool, str]:
|
|
949
|
+
root_ok, _message = _validate_openviking_root_access(values)
|
|
950
|
+
if not root_ok:
|
|
951
|
+
return True, ""
|
|
952
|
+
return (
|
|
953
|
+
False,
|
|
954
|
+
"That key has ROOT access. Choose Root API key and provide account/user, "
|
|
955
|
+
"or enter a user API key.",
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
def _status_code_from_error(error: Exception) -> Optional[int]:
|
|
960
|
+
if isinstance(error, _OpenVikingHTTPError):
|
|
961
|
+
return error.status_code
|
|
962
|
+
response = getattr(error, "response", None)
|
|
963
|
+
return getattr(response, "status_code", None)
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def _admin_probe_means_regular_key(error: Exception) -> bool:
|
|
967
|
+
return _status_code_from_error(error) in {401, 403, 404}
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def _should_probe_openviking_auth(health: dict, *, require_api_key: bool, has_api_key: bool) -> bool:
|
|
971
|
+
if require_api_key or has_api_key:
|
|
972
|
+
return True
|
|
973
|
+
auth_mode = health.get("auth_mode")
|
|
974
|
+
if auth_mode == "dev":
|
|
975
|
+
return False
|
|
976
|
+
if auth_mode in {"api_key", "trusted", None}:
|
|
977
|
+
return True
|
|
978
|
+
return False
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def _validate_openviking_setup_values(
|
|
982
|
+
values: dict,
|
|
983
|
+
*,
|
|
984
|
+
require_api_key: bool = False,
|
|
985
|
+
) -> tuple[bool, str, Optional[str]]:
|
|
986
|
+
endpoint = _normalize_openviking_url(values.get("endpoint"))
|
|
987
|
+
api_key = _clean_config_value(values.get("api_key"))
|
|
988
|
+
if require_api_key and not api_key:
|
|
989
|
+
return False, "Remote OpenViking configs require an API key.", None
|
|
990
|
+
|
|
991
|
+
try:
|
|
992
|
+
client = _VikingClient(
|
|
993
|
+
endpoint,
|
|
994
|
+
api_key,
|
|
995
|
+
account=_clean_config_value(values.get("account")),
|
|
996
|
+
user=_clean_config_value(values.get("user")),
|
|
997
|
+
agent=_clean_config_value(values.get("agent")) or _DEFAULT_AGENT,
|
|
998
|
+
)
|
|
999
|
+
health = client.health_payload()
|
|
1000
|
+
if health.get("healthy") is False:
|
|
1001
|
+
return False, "OpenViking server responded but reported unhealthy status.", None
|
|
1002
|
+
if _should_probe_openviking_auth(
|
|
1003
|
+
health,
|
|
1004
|
+
require_api_key=require_api_key,
|
|
1005
|
+
has_api_key=bool(api_key),
|
|
1006
|
+
):
|
|
1007
|
+
client.validate_auth()
|
|
1008
|
+
if not api_key:
|
|
1009
|
+
return True, "", None
|
|
1010
|
+
try:
|
|
1011
|
+
client.validate_root_access()
|
|
1012
|
+
return True, "", "root"
|
|
1013
|
+
except Exception as e:
|
|
1014
|
+
if _admin_probe_means_regular_key(e):
|
|
1015
|
+
return True, "", "user"
|
|
1016
|
+
raise
|
|
1017
|
+
except Exception as e:
|
|
1018
|
+
return False, f"OpenViking validation failed: {_format_openviking_exception(e)}", None
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def _retry_or_cancel_manual_setup(select, title: str, message: str, cancelled):
|
|
1022
|
+
print(f" {message}")
|
|
1023
|
+
choice = select(
|
|
1024
|
+
title,
|
|
1025
|
+
[
|
|
1026
|
+
("Retry", "try this step again"),
|
|
1027
|
+
("Cancel setup", "no changes saved"),
|
|
1028
|
+
],
|
|
1029
|
+
default=0,
|
|
1030
|
+
cancel_returns=cancelled,
|
|
1031
|
+
)
|
|
1032
|
+
if choice == 0:
|
|
1033
|
+
return True
|
|
1034
|
+
return _SETUP_CANCELLED
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def _print_validation_progress(message: str) -> None:
|
|
1038
|
+
print(f" {message}", flush=True)
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def _local_openviking_bind(endpoint: str) -> tuple[str, int]:
|
|
1042
|
+
normalized = _normalize_openviking_url(endpoint)
|
|
1043
|
+
parsed = urlparse(normalized)
|
|
1044
|
+
host = parsed.hostname or "127.0.0.1"
|
|
1045
|
+
port = parsed.port or 1933
|
|
1046
|
+
return host, port
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def _openviking_server_log_path() -> Path:
|
|
1050
|
+
try:
|
|
1051
|
+
from hermes_constants import get_hermes_home
|
|
1052
|
+
home = get_hermes_home()
|
|
1053
|
+
except Exception:
|
|
1054
|
+
home = Path(os.environ.get("HERMES_HOME", "")).expanduser() if os.environ.get("HERMES_HOME") else Path.home() / ".hermes"
|
|
1055
|
+
return home / _OPENVIKING_SERVER_LOG_RELATIVE_PATH
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
def _start_local_openviking_server(endpoint: str) -> tuple[bool, str]:
|
|
1059
|
+
server_cmd = shutil.which("openviking-server")
|
|
1060
|
+
if not server_cmd:
|
|
1061
|
+
return False, "openviking-server was not found on PATH. Start it manually, then retry."
|
|
1062
|
+
try:
|
|
1063
|
+
host, port = _local_openviking_bind(endpoint)
|
|
1064
|
+
except ValueError as e:
|
|
1065
|
+
return False, f"Could not parse local OpenViking URL: {e}"
|
|
1066
|
+
log_path = _openviking_server_log_path()
|
|
1067
|
+
try:
|
|
1068
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1069
|
+
with log_path.open("ab") as log_file:
|
|
1070
|
+
subprocess.Popen(
|
|
1071
|
+
[server_cmd, "--host", host, "--port", str(port)],
|
|
1072
|
+
stdout=log_file,
|
|
1073
|
+
stderr=log_file,
|
|
1074
|
+
stdin=subprocess.DEVNULL,
|
|
1075
|
+
start_new_session=True,
|
|
1076
|
+
)
|
|
1077
|
+
except Exception as e:
|
|
1078
|
+
return False, f"Could not start openviking-server: {e}"
|
|
1079
|
+
return True, f"Started openviking-server on {host}:{port} in the background. Logs: {log_path}"
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def _wait_for_openviking_health(endpoint: str, *, timeout_seconds: float = 15.0) -> bool:
|
|
1083
|
+
deadline = time.monotonic() + timeout_seconds
|
|
1084
|
+
while time.monotonic() < deadline:
|
|
1085
|
+
ok, _message = _validate_openviking_reachability(endpoint)
|
|
1086
|
+
if ok:
|
|
1087
|
+
return True
|
|
1088
|
+
time.sleep(0.5)
|
|
1089
|
+
return False
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
def _reachability_failure_allows_local_autostart(message: str) -> bool:
|
|
1093
|
+
return not (message or "").startswith(_OPENVIKING_RESPONDED_FAILURE_PREFIX)
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def _handle_unreachable_endpoint(
|
|
1097
|
+
endpoint: str,
|
|
1098
|
+
message: str,
|
|
1099
|
+
select,
|
|
1100
|
+
cancelled,
|
|
1101
|
+
*,
|
|
1102
|
+
allow_local_autostart: bool = True,
|
|
1103
|
+
):
|
|
1104
|
+
if _is_local_openviking_url(endpoint) and allow_local_autostart:
|
|
1105
|
+
print(f" {message}")
|
|
1106
|
+
choice = select(
|
|
1107
|
+
" Local OpenViking server is down",
|
|
1108
|
+
[
|
|
1109
|
+
("Start local OpenViking", "run openviking-server and retry"),
|
|
1110
|
+
("Retry URL", "enter the server URL again"),
|
|
1111
|
+
("Cancel setup", "no changes saved"),
|
|
1112
|
+
],
|
|
1113
|
+
default=0,
|
|
1114
|
+
cancel_returns=cancelled,
|
|
1115
|
+
)
|
|
1116
|
+
if choice == 0:
|
|
1117
|
+
started, start_message = _start_local_openviking_server(endpoint)
|
|
1118
|
+
print(f" {start_message}")
|
|
1119
|
+
if not started:
|
|
1120
|
+
return False
|
|
1121
|
+
print(" Waiting for OpenViking server to become reachable...", flush=True)
|
|
1122
|
+
if _wait_for_openviking_health(
|
|
1123
|
+
endpoint,
|
|
1124
|
+
timeout_seconds=_LOCAL_OPENVIKING_AUTOSTART_TIMEOUT,
|
|
1125
|
+
):
|
|
1126
|
+
print(" OpenViking server is reachable.")
|
|
1127
|
+
return True
|
|
1128
|
+
print(" OpenViking server did not become reachable.")
|
|
1129
|
+
return False
|
|
1130
|
+
if choice == 1:
|
|
1131
|
+
return False
|
|
1132
|
+
return _SETUP_CANCELLED
|
|
1133
|
+
|
|
1134
|
+
return _retry_or_cancel_manual_setup(
|
|
1135
|
+
select,
|
|
1136
|
+
" OpenViking server unhealthy" if _is_local_openviking_url(endpoint) else " OpenViking server unreachable",
|
|
1137
|
+
message,
|
|
1138
|
+
cancelled,
|
|
1139
|
+
)
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
def _emit_runtime_warning(message: str, warning_callback=None) -> None:
|
|
1143
|
+
logger.warning("%s", message)
|
|
1144
|
+
if warning_callback:
|
|
1145
|
+
try:
|
|
1146
|
+
warning_callback(message)
|
|
1147
|
+
except Exception:
|
|
1148
|
+
logger.debug("OpenViking runtime warning callback failed", exc_info=True)
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
def _emit_runtime_status(message: str, status_callback=None) -> None:
|
|
1152
|
+
logger.info("%s", message)
|
|
1153
|
+
if status_callback:
|
|
1154
|
+
try:
|
|
1155
|
+
status_callback(message)
|
|
1156
|
+
except Exception:
|
|
1157
|
+
logger.debug("OpenViking runtime status callback failed", exc_info=True)
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
def _runtime_openviking_timeout_message(endpoint: str) -> str:
|
|
1161
|
+
return (
|
|
1162
|
+
f"Local OpenViking server at {endpoint} is not reachable. "
|
|
1163
|
+
"Tried to start openviking-server, but it did not become reachable "
|
|
1164
|
+
f"within {_LOCAL_OPENVIKING_AUTOSTART_TIMEOUT:.0f} seconds. "
|
|
1165
|
+
"OpenViking memory disabled for this Hermes run."
|
|
1166
|
+
)
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
def _classify_runtime_openviking_health(client: _VikingClient, endpoint: str) -> tuple[str, str]:
|
|
1170
|
+
"""Classify runtime health without treating every false result as server absence."""
|
|
1171
|
+
try:
|
|
1172
|
+
if hasattr(client, "health_payload"):
|
|
1173
|
+
payload = client.health_payload()
|
|
1174
|
+
if payload.get("healthy") is False:
|
|
1175
|
+
return (
|
|
1176
|
+
"responded",
|
|
1177
|
+
f"OpenViking server at {endpoint} responded but reported unhealthy status.",
|
|
1178
|
+
)
|
|
1179
|
+
return "healthy", ""
|
|
1180
|
+
if client.health():
|
|
1181
|
+
return "healthy", ""
|
|
1182
|
+
except _OpenVikingHTTPError as e:
|
|
1183
|
+
return (
|
|
1184
|
+
"responded",
|
|
1185
|
+
f"OpenViking server at {endpoint} responded with {_format_openviking_exception(e)}.",
|
|
1186
|
+
)
|
|
1187
|
+
except Exception:
|
|
1188
|
+
return "unreachable", ""
|
|
1189
|
+
return "unreachable", ""
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
def _prompt_profile_name(prompt, select, cancelled) -> str | object:
|
|
1193
|
+
while True:
|
|
1194
|
+
name = _clean_config_value(prompt("OpenViking profile name"))
|
|
1195
|
+
if _is_valid_ovcli_profile_name(name):
|
|
1196
|
+
return name
|
|
1197
|
+
retry = _retry_or_cancel_manual_setup(
|
|
1198
|
+
select,
|
|
1199
|
+
" Invalid OpenViking profile name",
|
|
1200
|
+
"Profile names can only contain letters, numbers, '-' and '_'.",
|
|
1201
|
+
cancelled,
|
|
1202
|
+
)
|
|
1203
|
+
if retry is _SETUP_CANCELLED:
|
|
1204
|
+
return _SETUP_CANCELLED
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
def _confirm_replace_existing_profile(path: Path, values: dict, select, cancelled):
|
|
1208
|
+
if not path.exists():
|
|
1209
|
+
return True
|
|
1210
|
+
try:
|
|
1211
|
+
existing_data = _load_ovcli_config(path)
|
|
1212
|
+
except Exception:
|
|
1213
|
+
existing_data = {}
|
|
1214
|
+
if existing_data == _ovcli_data_from_connection_values(values):
|
|
1215
|
+
return True
|
|
1216
|
+
choice = select(
|
|
1217
|
+
" OpenViking profile already exists",
|
|
1218
|
+
[
|
|
1219
|
+
("Choose another name", "leave the existing profile unchanged"),
|
|
1220
|
+
("Replace profile", "overwrite this saved OpenViking profile"),
|
|
1221
|
+
("Cancel setup", "no changes saved"),
|
|
1222
|
+
],
|
|
1223
|
+
default=0,
|
|
1224
|
+
cancel_returns=cancelled,
|
|
1225
|
+
)
|
|
1226
|
+
if choice == 1:
|
|
1227
|
+
return True
|
|
1228
|
+
if choice == 0:
|
|
1229
|
+
return False
|
|
1230
|
+
return _SETUP_CANCELLED
|
|
1231
|
+
|
|
1232
|
+
|
|
1233
|
+
def _prompt_manual_connection_values(prompt, select, cancelled, *, service: bool = False):
|
|
1234
|
+
if service:
|
|
1235
|
+
endpoint = _OPENVIKING_SERVICE_ENDPOINT
|
|
1236
|
+
print(f" OpenViking Service endpoint: {endpoint}")
|
|
1237
|
+
else:
|
|
1238
|
+
while True:
|
|
1239
|
+
endpoint = _normalize_openviking_url(prompt("OpenViking server URL", default=_DEFAULT_ENDPOINT))
|
|
1240
|
+
_print_validation_progress("Checking OpenViking server...")
|
|
1241
|
+
reachable, message = _validate_openviking_reachability(endpoint)
|
|
1242
|
+
if reachable:
|
|
1243
|
+
print(" OpenViking server is reachable.")
|
|
1244
|
+
break
|
|
1245
|
+
retry = _handle_unreachable_endpoint(
|
|
1246
|
+
endpoint,
|
|
1247
|
+
message,
|
|
1248
|
+
select,
|
|
1249
|
+
cancelled,
|
|
1250
|
+
allow_local_autostart=_reachability_failure_allows_local_autostart(message),
|
|
1251
|
+
)
|
|
1252
|
+
if retry is True:
|
|
1253
|
+
break
|
|
1254
|
+
if retry is _SETUP_CANCELLED:
|
|
1255
|
+
return _SETUP_CANCELLED
|
|
1256
|
+
|
|
1257
|
+
is_local = _is_local_openviking_url(endpoint)
|
|
1258
|
+
api_key_type = "user" if service else ""
|
|
1259
|
+
prefilled_api_key = ""
|
|
1260
|
+
prefilled_agent = ""
|
|
1261
|
+
while True:
|
|
1262
|
+
values = {
|
|
1263
|
+
"endpoint": endpoint,
|
|
1264
|
+
"api_key": "",
|
|
1265
|
+
"root_api_key": "",
|
|
1266
|
+
"account": "",
|
|
1267
|
+
"user": "",
|
|
1268
|
+
"agent": "",
|
|
1269
|
+
}
|
|
1270
|
+
if not api_key_type and is_local:
|
|
1271
|
+
credential_choice = select(
|
|
1272
|
+
" OpenViking credential",
|
|
1273
|
+
[
|
|
1274
|
+
("No API key", "local dev mode"),
|
|
1275
|
+
("User API key", "server derives account/user automatically"),
|
|
1276
|
+
("Root API key", "requires account and user IDs"),
|
|
1277
|
+
],
|
|
1278
|
+
default=0,
|
|
1279
|
+
cancel_returns=cancelled,
|
|
1280
|
+
)
|
|
1281
|
+
if credential_choice == cancelled:
|
|
1282
|
+
return _SETUP_CANCELLED
|
|
1283
|
+
if credential_choice == 0:
|
|
1284
|
+
values["agent"] = _clean_config_value(
|
|
1285
|
+
prompt(_AGENT_PROMPT_LABEL, default=_DEFAULT_AGENT)
|
|
1286
|
+
) or _DEFAULT_AGENT
|
|
1287
|
+
_print_validation_progress("Validating OpenViking local dev access...")
|
|
1288
|
+
valid, message, _role = _validate_openviking_setup_values(values)
|
|
1289
|
+
if valid:
|
|
1290
|
+
print(" OpenViking local dev access validated.")
|
|
1291
|
+
return values
|
|
1292
|
+
retry = _retry_or_cancel_manual_setup(
|
|
1293
|
+
select,
|
|
1294
|
+
" OpenViking credential failed",
|
|
1295
|
+
message,
|
|
1296
|
+
cancelled,
|
|
1297
|
+
)
|
|
1298
|
+
if retry is _SETUP_CANCELLED:
|
|
1299
|
+
return _SETUP_CANCELLED
|
|
1300
|
+
continue
|
|
1301
|
+
api_key_type = "root" if credential_choice == 2 else "user"
|
|
1302
|
+
elif not api_key_type:
|
|
1303
|
+
credential_choice = select(
|
|
1304
|
+
" OpenViking API key type",
|
|
1305
|
+
[
|
|
1306
|
+
("User API key", "server derives account/user automatically"),
|
|
1307
|
+
("Root API key", "requires account and user IDs"),
|
|
1308
|
+
],
|
|
1309
|
+
default=0,
|
|
1310
|
+
cancel_returns=cancelled,
|
|
1311
|
+
)
|
|
1312
|
+
if credential_choice == cancelled:
|
|
1313
|
+
return _SETUP_CANCELLED
|
|
1314
|
+
api_key_type = "root" if credential_choice == 1 else "user"
|
|
1315
|
+
|
|
1316
|
+
values["api_key_type"] = api_key_type
|
|
1317
|
+
if service:
|
|
1318
|
+
api_key_label = "OpenViking API key"
|
|
1319
|
+
else:
|
|
1320
|
+
api_key_label = (
|
|
1321
|
+
"OpenViking root API key"
|
|
1322
|
+
if api_key_type == "root"
|
|
1323
|
+
else "OpenViking user API key"
|
|
1324
|
+
)
|
|
1325
|
+
if prefilled_api_key:
|
|
1326
|
+
values["api_key"] = prefilled_api_key
|
|
1327
|
+
prefilled_api_key = ""
|
|
1328
|
+
else:
|
|
1329
|
+
values["api_key"] = _clean_config_value(prompt(api_key_label, secret=True))
|
|
1330
|
+
if not values["api_key"]:
|
|
1331
|
+
retry = _retry_or_cancel_manual_setup(
|
|
1332
|
+
select,
|
|
1333
|
+
" OpenViking API key required",
|
|
1334
|
+
f"{api_key_label} is required.",
|
|
1335
|
+
cancelled,
|
|
1336
|
+
)
|
|
1337
|
+
if retry is _SETUP_CANCELLED:
|
|
1338
|
+
return _SETUP_CANCELLED
|
|
1339
|
+
continue
|
|
1340
|
+
|
|
1341
|
+
if api_key_type == "root":
|
|
1342
|
+
_print_validation_progress("Validating OpenViking root API key...")
|
|
1343
|
+
valid, message, role = _validate_openviking_setup_values(values, require_api_key=True)
|
|
1344
|
+
root_ok = valid and role == "root"
|
|
1345
|
+
if not root_ok:
|
|
1346
|
+
if valid and role == "user":
|
|
1347
|
+
print(" That key is valid, but it is a user API key.")
|
|
1348
|
+
route_choice = select(
|
|
1349
|
+
" OpenViking key is a user key",
|
|
1350
|
+
[
|
|
1351
|
+
("Use as User API key", "server derives account/user automatically"),
|
|
1352
|
+
("Re-enter Root API key", "try another root key"),
|
|
1353
|
+
("Cancel setup", "no changes saved"),
|
|
1354
|
+
],
|
|
1355
|
+
default=0,
|
|
1356
|
+
cancel_returns=cancelled,
|
|
1357
|
+
)
|
|
1358
|
+
if route_choice == 0:
|
|
1359
|
+
prefilled_api_key = values["api_key"]
|
|
1360
|
+
api_key_type = "user"
|
|
1361
|
+
continue
|
|
1362
|
+
if route_choice == 1:
|
|
1363
|
+
api_key_type = "root"
|
|
1364
|
+
continue
|
|
1365
|
+
return _SETUP_CANCELLED
|
|
1366
|
+
retry = _retry_or_cancel_manual_setup(
|
|
1367
|
+
select,
|
|
1368
|
+
" OpenViking root API key failed",
|
|
1369
|
+
message,
|
|
1370
|
+
cancelled,
|
|
1371
|
+
)
|
|
1372
|
+
if retry is _SETUP_CANCELLED:
|
|
1373
|
+
return _SETUP_CANCELLED
|
|
1374
|
+
continue
|
|
1375
|
+
print(" OpenViking root API key validated.")
|
|
1376
|
+
values["root_api_key"] = values["api_key"]
|
|
1377
|
+
account_ok, account_message, account = _validate_openviking_identity_value(
|
|
1378
|
+
prompt("OpenViking account"),
|
|
1379
|
+
field="account",
|
|
1380
|
+
)
|
|
1381
|
+
user_ok, user_message, user = _validate_openviking_identity_value(
|
|
1382
|
+
prompt("OpenViking user"),
|
|
1383
|
+
field="user",
|
|
1384
|
+
)
|
|
1385
|
+
values["account"] = account
|
|
1386
|
+
values["user"] = user
|
|
1387
|
+
if not account_ok or not user_ok:
|
|
1388
|
+
message = account_message if not account_ok else user_message
|
|
1389
|
+
retry = _retry_or_cancel_manual_setup(
|
|
1390
|
+
select,
|
|
1391
|
+
" OpenViking tenant identity required",
|
|
1392
|
+
message,
|
|
1393
|
+
cancelled,
|
|
1394
|
+
)
|
|
1395
|
+
if retry is _SETUP_CANCELLED:
|
|
1396
|
+
return _SETUP_CANCELLED
|
|
1397
|
+
prefilled_api_key = values["api_key"]
|
|
1398
|
+
continue
|
|
1399
|
+
|
|
1400
|
+
if prefilled_agent:
|
|
1401
|
+
values["agent"] = prefilled_agent
|
|
1402
|
+
prefilled_agent = ""
|
|
1403
|
+
else:
|
|
1404
|
+
values["agent"] = _clean_config_value(
|
|
1405
|
+
prompt(_AGENT_PROMPT_LABEL, default=_DEFAULT_AGENT)
|
|
1406
|
+
) or _DEFAULT_AGENT
|
|
1407
|
+
_print_validation_progress("Validating OpenViking API access...")
|
|
1408
|
+
valid, message, role = _validate_openviking_setup_values(
|
|
1409
|
+
values,
|
|
1410
|
+
require_api_key=service or not is_local,
|
|
1411
|
+
)
|
|
1412
|
+
if valid:
|
|
1413
|
+
if api_key_type == "user":
|
|
1414
|
+
if role == "root":
|
|
1415
|
+
print(" That key is valid, but it has root access.")
|
|
1416
|
+
route_choice = select(
|
|
1417
|
+
" OpenViking user API key is root key",
|
|
1418
|
+
[
|
|
1419
|
+
("Configure as Root API key", "provide account and user IDs"),
|
|
1420
|
+
("Re-enter User API key", "try another user key"),
|
|
1421
|
+
("Cancel setup", "no changes saved"),
|
|
1422
|
+
],
|
|
1423
|
+
default=0,
|
|
1424
|
+
cancel_returns=cancelled,
|
|
1425
|
+
)
|
|
1426
|
+
if route_choice == 0:
|
|
1427
|
+
prefilled_api_key = values["api_key"]
|
|
1428
|
+
prefilled_agent = values["agent"]
|
|
1429
|
+
api_key_type = "root"
|
|
1430
|
+
continue
|
|
1431
|
+
if route_choice == 1:
|
|
1432
|
+
api_key_type = "user"
|
|
1433
|
+
continue
|
|
1434
|
+
return _SETUP_CANCELLED
|
|
1435
|
+
if api_key_type == "root" and role != "root":
|
|
1436
|
+
retry = _retry_or_cancel_manual_setup(
|
|
1437
|
+
select,
|
|
1438
|
+
" OpenViking root API key failed",
|
|
1439
|
+
"The supplied key was not accepted as a root API key.",
|
|
1440
|
+
cancelled,
|
|
1441
|
+
)
|
|
1442
|
+
if retry is _SETUP_CANCELLED:
|
|
1443
|
+
return _SETUP_CANCELLED
|
|
1444
|
+
continue
|
|
1445
|
+
print(" OpenViking API access validated.")
|
|
1446
|
+
return values
|
|
1447
|
+
retry = _retry_or_cancel_manual_setup(
|
|
1448
|
+
select,
|
|
1449
|
+
" OpenViking API access failed",
|
|
1450
|
+
message,
|
|
1451
|
+
cancelled,
|
|
1452
|
+
)
|
|
1453
|
+
if retry is _SETUP_CANCELLED:
|
|
1454
|
+
return _SETUP_CANCELLED
|
|
1455
|
+
|
|
1456
|
+
|
|
1457
|
+
def _set_openviking_provider(config: dict, provider_config: dict) -> None:
|
|
1458
|
+
config["memory"]["provider"] = "openviking"
|
|
1459
|
+
config["memory"]["openviking"] = provider_config
|
|
1460
|
+
|
|
1461
|
+
|
|
1462
|
+
def _link_ovcli_profile(
|
|
1463
|
+
*,
|
|
1464
|
+
config: dict,
|
|
1465
|
+
provider_config: dict,
|
|
1466
|
+
env_path: Path,
|
|
1467
|
+
ovcli_path: Path,
|
|
1468
|
+
) -> None:
|
|
1469
|
+
for key in ("endpoint", "api_key", "root_api_key", "account", "user", "agent", "api_key_type"):
|
|
1470
|
+
provider_config.pop(key, None)
|
|
1471
|
+
provider_config["use_ovcli_config"] = True
|
|
1472
|
+
_remember_ovcli_path(provider_config, ovcli_path)
|
|
1473
|
+
_set_openviking_provider(config, provider_config)
|
|
1474
|
+
_write_env_vars(env_path, {}, remove_keys=_OPENVIKING_ENV_KEYS)
|
|
1475
|
+
for key in _OPENVIKING_ENV_KEYS:
|
|
1476
|
+
os.environ.pop(key, None)
|
|
1477
|
+
|
|
1478
|
+
|
|
1479
|
+
def _save_hermes_only_config(
|
|
1480
|
+
*,
|
|
1481
|
+
config: dict,
|
|
1482
|
+
provider_config: dict,
|
|
1483
|
+
env_path: Path,
|
|
1484
|
+
values: dict,
|
|
1485
|
+
) -> None:
|
|
1486
|
+
provider_config["use_ovcli_config"] = False
|
|
1487
|
+
provider_config.pop("ovcli_config_path", None)
|
|
1488
|
+
_set_openviking_provider(config, provider_config)
|
|
1489
|
+
_write_env_vars(
|
|
1490
|
+
env_path,
|
|
1491
|
+
_env_writes_from_connection_values(values),
|
|
1492
|
+
remove_keys=_OPENVIKING_ENV_KEYS,
|
|
1493
|
+
)
|
|
1494
|
+
|
|
1495
|
+
|
|
1496
|
+
def _profile_display_name(profile: _OvcliProfile) -> str:
|
|
1497
|
+
if profile.source == "env":
|
|
1498
|
+
return _OVCLI_CONFIG_ENV
|
|
1499
|
+
if profile.source == "active":
|
|
1500
|
+
return "ovcli.conf"
|
|
1501
|
+
return profile.name
|
|
1502
|
+
|
|
1503
|
+
|
|
1504
|
+
def _profile_description(profile: _OvcliProfile) -> str:
|
|
1505
|
+
endpoint = _clean_config_value(profile.values.get("endpoint")) or _DEFAULT_ENDPOINT
|
|
1506
|
+
return f"{endpoint} ({profile.path})"
|
|
1507
|
+
|
|
1508
|
+
|
|
1509
|
+
def _validate_profile_for_setup(profile: _OvcliProfile) -> tuple[bool, str, Optional[str]]:
|
|
1510
|
+
require_api_key = not _is_local_openviking_url(profile.values.get("endpoint", ""))
|
|
1511
|
+
return _validate_openviking_setup_values(profile.values, require_api_key=require_api_key)
|
|
1512
|
+
|
|
1513
|
+
|
|
1514
|
+
def _print_openviking_ready(message: str, path: Optional[Path] = None) -> None:
|
|
1515
|
+
print("\n OpenViking memory is ready")
|
|
1516
|
+
print(f" {message}")
|
|
1517
|
+
if path is not None:
|
|
1518
|
+
print(f" Config file: {path}")
|
|
1519
|
+
print(" Start a new Hermes session to activate.\n")
|
|
1520
|
+
|
|
1521
|
+
|
|
1522
|
+
def _run_existing_profile_setup(
|
|
1523
|
+
*,
|
|
1524
|
+
profiles: list[_OvcliProfile],
|
|
1525
|
+
select,
|
|
1526
|
+
cancelled,
|
|
1527
|
+
config: dict,
|
|
1528
|
+
provider_config: dict,
|
|
1529
|
+
env_path: Path,
|
|
1530
|
+
) -> bool | object:
|
|
1531
|
+
while True:
|
|
1532
|
+
choice = select(
|
|
1533
|
+
" OpenViking profile",
|
|
1534
|
+
[(_profile_display_name(profile), _profile_description(profile)) for profile in profiles],
|
|
1535
|
+
default=0,
|
|
1536
|
+
cancel_returns=cancelled,
|
|
1537
|
+
)
|
|
1538
|
+
if choice == cancelled:
|
|
1539
|
+
return _SETUP_CANCELLED
|
|
1540
|
+
if choice < 0 or choice >= len(profiles):
|
|
1541
|
+
return _SETUP_CANCELLED
|
|
1542
|
+
|
|
1543
|
+
profile = profiles[choice]
|
|
1544
|
+
_print_validation_progress("Validating OpenViking profile...")
|
|
1545
|
+
ok, message, _role = _validate_profile_for_setup(profile)
|
|
1546
|
+
if ok:
|
|
1547
|
+
_link_ovcli_profile(
|
|
1548
|
+
config=config,
|
|
1549
|
+
provider_config=provider_config,
|
|
1550
|
+
env_path=env_path,
|
|
1551
|
+
ovcli_path=profile.path,
|
|
1552
|
+
)
|
|
1553
|
+
_print_openviking_ready(f"Linked profile: {_profile_display_name(profile)}", profile.path)
|
|
1554
|
+
return True
|
|
1555
|
+
|
|
1556
|
+
print(f" {message}")
|
|
1557
|
+
retry = select(
|
|
1558
|
+
" OpenViking profile validation failed",
|
|
1559
|
+
[
|
|
1560
|
+
("Choose another profile", "select a different OpenViking profile"),
|
|
1561
|
+
("Retry validation", "try this profile again"),
|
|
1562
|
+
("Cancel setup", "no changes saved"),
|
|
1563
|
+
],
|
|
1564
|
+
default=0,
|
|
1565
|
+
cancel_returns=cancelled,
|
|
1566
|
+
)
|
|
1567
|
+
if retry == 0:
|
|
1568
|
+
continue
|
|
1569
|
+
if retry == 1:
|
|
1570
|
+
_print_validation_progress("Validating OpenViking profile...")
|
|
1571
|
+
ok, message, _role = _validate_profile_for_setup(profile)
|
|
1572
|
+
if ok:
|
|
1573
|
+
_link_ovcli_profile(
|
|
1574
|
+
config=config,
|
|
1575
|
+
provider_config=provider_config,
|
|
1576
|
+
env_path=env_path,
|
|
1577
|
+
ovcli_path=profile.path,
|
|
1578
|
+
)
|
|
1579
|
+
_print_openviking_ready(f"Linked profile: {_profile_display_name(profile)}", profile.path)
|
|
1580
|
+
return True
|
|
1581
|
+
print(f" {message}")
|
|
1582
|
+
continue
|
|
1583
|
+
return _SETUP_CANCELLED
|
|
1584
|
+
|
|
1585
|
+
|
|
1586
|
+
def _mirror_manual_config_to_openviking_store(
|
|
1587
|
+
*,
|
|
1588
|
+
prompt,
|
|
1589
|
+
select,
|
|
1590
|
+
cancelled,
|
|
1591
|
+
values: dict,
|
|
1592
|
+
) -> Path | object:
|
|
1593
|
+
while True:
|
|
1594
|
+
name = _prompt_profile_name(prompt, select, cancelled)
|
|
1595
|
+
if name is _SETUP_CANCELLED:
|
|
1596
|
+
return _SETUP_CANCELLED
|
|
1597
|
+
path = _ovcli_config_dir() / f"{_OVCLI_SAVED_PREFIX}{name}"
|
|
1598
|
+
replace = _confirm_replace_existing_profile(path, values, select, cancelled)
|
|
1599
|
+
if replace is _SETUP_CANCELLED:
|
|
1600
|
+
return _SETUP_CANCELLED
|
|
1601
|
+
if replace is False:
|
|
1602
|
+
continue
|
|
1603
|
+
_write_ovcli_config(path, values)
|
|
1604
|
+
return path
|
|
1605
|
+
|
|
1606
|
+
|
|
1607
|
+
def _run_create_profile_setup(
|
|
1608
|
+
*,
|
|
1609
|
+
prompt,
|
|
1610
|
+
select,
|
|
1611
|
+
cancelled,
|
|
1612
|
+
config: dict,
|
|
1613
|
+
provider_config: dict,
|
|
1614
|
+
env_path: Path,
|
|
1615
|
+
) -> bool | object:
|
|
1616
|
+
source_choice = select(
|
|
1617
|
+
" OpenViking connection",
|
|
1618
|
+
[
|
|
1619
|
+
("OpenViking Service (VolcEngine Cloud)", "use the managed OpenViking endpoint"),
|
|
1620
|
+
("Custom", "use a local, VPS, or self-hosted OpenViking server"),
|
|
1621
|
+
],
|
|
1622
|
+
default=0,
|
|
1623
|
+
cancel_returns=cancelled,
|
|
1624
|
+
)
|
|
1625
|
+
if source_choice == cancelled:
|
|
1626
|
+
return _SETUP_CANCELLED
|
|
1627
|
+
|
|
1628
|
+
values = _prompt_manual_connection_values(prompt, select, cancelled, service=(source_choice == 0))
|
|
1629
|
+
if values is _SETUP_CANCELLED:
|
|
1630
|
+
return _SETUP_CANCELLED
|
|
1631
|
+
if values is None:
|
|
1632
|
+
return False
|
|
1633
|
+
|
|
1634
|
+
save_choice = select(
|
|
1635
|
+
" Save OpenViking config",
|
|
1636
|
+
[
|
|
1637
|
+
("Keep in Hermes only", "write values only to Hermes .env"),
|
|
1638
|
+
("Mirror to OpenViking store", "write ~/.openviking/ovcli.conf.<name> and link it"),
|
|
1639
|
+
],
|
|
1640
|
+
default=1,
|
|
1641
|
+
cancel_returns=cancelled,
|
|
1642
|
+
)
|
|
1643
|
+
if save_choice == cancelled:
|
|
1644
|
+
return _SETUP_CANCELLED
|
|
1645
|
+
|
|
1646
|
+
if save_choice == 1:
|
|
1647
|
+
ovcli_path = _mirror_manual_config_to_openviking_store(
|
|
1648
|
+
prompt=prompt,
|
|
1649
|
+
select=select,
|
|
1650
|
+
cancelled=cancelled,
|
|
1651
|
+
values=values,
|
|
1652
|
+
)
|
|
1653
|
+
if ovcli_path is _SETUP_CANCELLED:
|
|
1654
|
+
return _SETUP_CANCELLED
|
|
1655
|
+
_link_ovcli_profile(
|
|
1656
|
+
config=config,
|
|
1657
|
+
provider_config=provider_config,
|
|
1658
|
+
env_path=env_path,
|
|
1659
|
+
ovcli_path=ovcli_path,
|
|
1660
|
+
)
|
|
1661
|
+
_print_openviking_ready("Created and linked OpenViking profile.", ovcli_path)
|
|
1662
|
+
return True
|
|
1663
|
+
|
|
1664
|
+
_save_hermes_only_config(
|
|
1665
|
+
config=config,
|
|
1666
|
+
provider_config=provider_config,
|
|
1667
|
+
env_path=env_path,
|
|
1668
|
+
values=values,
|
|
1669
|
+
)
|
|
1670
|
+
_print_openviking_ready("Connection saved to Hermes .env.")
|
|
1671
|
+
return True
|
|
1672
|
+
|
|
1673
|
+
|
|
408
1674
|
# ---------------------------------------------------------------------------
|
|
409
1675
|
# MemoryProvider implementation
|
|
410
1676
|
# ---------------------------------------------------------------------------
|
|
@@ -418,10 +1684,37 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
|
|
418
1684
|
self._api_key = ""
|
|
419
1685
|
self._session_id = ""
|
|
420
1686
|
self._turn_count = 0
|
|
421
|
-
|
|
1687
|
+
# Guards the (_session_id, _turn_count) pair. sync_turn runs on the
|
|
1688
|
+
# MemoryManager's background sync executor while on_session_end /
|
|
1689
|
+
# on_session_switch run on the caller's thread, so the snapshot+reset
|
|
1690
|
+
# of the turn counter and the session-id rotation must be atomic
|
|
1691
|
+
# against a concurrent increment. See hermes-agent#28296 review.
|
|
1692
|
+
self._session_state_lock = threading.Lock()
|
|
1693
|
+
# Commit only after session writes drain. The set is keyed by the sid
|
|
1694
|
+
# the writer is POSTing under (snapshotted at spawn), so on_session_end
|
|
1695
|
+
# / on_session_switch see every still-alive writer for that sid even
|
|
1696
|
+
# if later writes have replaced the latest-tracked thread.
|
|
1697
|
+
self._inflight_writers: Dict[str, Set[threading.Thread]] = {}
|
|
1698
|
+
self._inflight_lock = threading.Lock()
|
|
1699
|
+
self._deferred_commit_sids: Set[str] = set()
|
|
1700
|
+
self._deferred_commit_threads: Set[threading.Thread] = set()
|
|
1701
|
+
self._deferred_commit_lock = threading.Lock()
|
|
1702
|
+
self._committed_session_ids: Set[str] = set()
|
|
1703
|
+
self._committed_session_lock = threading.Lock()
|
|
422
1704
|
self._prefetch_result = ""
|
|
423
1705
|
self._prefetch_lock = threading.Lock()
|
|
424
1706
|
self._prefetch_thread: Optional[threading.Thread] = None
|
|
1707
|
+
self._runtime_start_lock = threading.Lock()
|
|
1708
|
+
self._runtime_start_thread: Optional[threading.Thread] = None
|
|
1709
|
+
# All prefetch threads ever spawned (daemon, short-lived). Tracked so
|
|
1710
|
+
# shutdown() can drain them and rapid re-queues don't orphan a still-
|
|
1711
|
+
# running thread by overwriting the single _prefetch_thread slot.
|
|
1712
|
+
self._prefetch_threads: Set[threading.Thread] = set()
|
|
1713
|
+
# Set on shutdown so deferred-commit / writer finalizers stop issuing
|
|
1714
|
+
# network writes against a torn-down provider.
|
|
1715
|
+
self._shutting_down = False
|
|
1716
|
+
# Drop prefetch results from older switch generations.
|
|
1717
|
+
self._prefetch_generation = 0
|
|
425
1718
|
|
|
426
1719
|
@property
|
|
427
1720
|
def name(self) -> str:
|
|
@@ -429,7 +1722,16 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
|
|
429
1722
|
|
|
430
1723
|
def is_available(self) -> bool:
|
|
431
1724
|
"""Check if OpenViking endpoint is configured. No network calls."""
|
|
432
|
-
|
|
1725
|
+
if os.environ.get("OPENVIKING_ENDPOINT"):
|
|
1726
|
+
return True
|
|
1727
|
+
provider_config = _load_hermes_openviking_config()
|
|
1728
|
+
if not provider_config.get("use_ovcli_config"):
|
|
1729
|
+
return False
|
|
1730
|
+
try:
|
|
1731
|
+
ovcli_path = _resolve_ovcli_config_path(str(provider_config.get("ovcli_config_path") or ""))
|
|
1732
|
+
return bool(_connection_values_from_ovcli(_load_ovcli_config(ovcli_path)).get("endpoint"))
|
|
1733
|
+
except Exception:
|
|
1734
|
+
return False
|
|
433
1735
|
|
|
434
1736
|
def get_config_schema(self):
|
|
435
1737
|
return [
|
|
@@ -448,40 +1750,265 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
|
|
448
1750
|
},
|
|
449
1751
|
{
|
|
450
1752
|
"key": "account",
|
|
451
|
-
"description": "OpenViking tenant account ID (
|
|
452
|
-
"default": "default",
|
|
1753
|
+
"description": "OpenViking tenant account ID (blank for user API keys)",
|
|
453
1754
|
"env_var": "OPENVIKING_ACCOUNT",
|
|
454
1755
|
},
|
|
455
1756
|
{
|
|
456
1757
|
"key": "user",
|
|
457
|
-
"description": "OpenViking user ID within the account (
|
|
458
|
-
"default": "default",
|
|
1758
|
+
"description": "OpenViking user ID within the account (blank for user API keys)",
|
|
459
1759
|
"env_var": "OPENVIKING_USER",
|
|
460
1760
|
},
|
|
461
1761
|
{
|
|
462
1762
|
"key": "agent",
|
|
463
|
-
"description":
|
|
1763
|
+
"description": (
|
|
1764
|
+
"Hermes peer ID in OpenViking, sent as the actor peer and "
|
|
1765
|
+
"used for peer-scoped memories"
|
|
1766
|
+
),
|
|
464
1767
|
"default": "hermes",
|
|
465
1768
|
"env_var": "OPENVIKING_AGENT",
|
|
466
1769
|
},
|
|
467
1770
|
]
|
|
468
1771
|
|
|
1772
|
+
def get_status_config(self, provider_config: dict) -> dict:
|
|
1773
|
+
provider_config = dict(provider_config or {})
|
|
1774
|
+
if provider_config.get("use_ovcli_config"):
|
|
1775
|
+
ovcli_path = _resolve_ovcli_config_path(str(provider_config.get("ovcli_config_path") or ""))
|
|
1776
|
+
try:
|
|
1777
|
+
settings = _resolve_connection_settings(provider_config)
|
|
1778
|
+
except Exception as e:
|
|
1779
|
+
return {
|
|
1780
|
+
"use_ovcli_config": True,
|
|
1781
|
+
"ovcli_config_path": str(ovcli_path),
|
|
1782
|
+
"error": _format_openviking_exception(e),
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
display = {
|
|
1786
|
+
"use_ovcli_config": True,
|
|
1787
|
+
"ovcli_config_path": str(ovcli_path),
|
|
1788
|
+
"endpoint": settings.get("endpoint") or _DEFAULT_ENDPOINT,
|
|
1789
|
+
"agent": settings.get("agent") or _DEFAULT_AGENT,
|
|
1790
|
+
}
|
|
1791
|
+
if settings.get("account"):
|
|
1792
|
+
display["account"] = settings["account"]
|
|
1793
|
+
if settings.get("user"):
|
|
1794
|
+
display["user"] = settings["user"]
|
|
1795
|
+
env_overrides = [key for key in _OPENVIKING_ENV_KEYS if _env_value(key) is not None]
|
|
1796
|
+
if env_overrides:
|
|
1797
|
+
display["env_overrides"] = ", ".join(env_overrides)
|
|
1798
|
+
return display
|
|
1799
|
+
|
|
1800
|
+
display = dict(provider_config)
|
|
1801
|
+
for key in ("api_key", "root_api_key"):
|
|
1802
|
+
if key in display:
|
|
1803
|
+
display[key] = "(set)"
|
|
1804
|
+
return display
|
|
1805
|
+
|
|
1806
|
+
def post_setup(self, hermes_home: str, config: dict) -> None:
|
|
1807
|
+
"""Custom setup that can reuse OpenViking's shared CLI config."""
|
|
1808
|
+
from hermes_cli.config import save_config
|
|
1809
|
+
from hermes_cli.memory_setup import _CANCELLED, _curses_select, _print_cancelled_setup, _prompt
|
|
1810
|
+
|
|
1811
|
+
hermes_home_path = Path(hermes_home)
|
|
1812
|
+
env_path = hermes_home_path / ".env"
|
|
1813
|
+
if not isinstance(config.get("memory"), dict):
|
|
1814
|
+
config["memory"] = {}
|
|
1815
|
+
provider_config = config["memory"].get("openviking", {})
|
|
1816
|
+
if not isinstance(provider_config, dict):
|
|
1817
|
+
provider_config = {}
|
|
1818
|
+
|
|
1819
|
+
print("\n OpenViking memory setup\n")
|
|
1820
|
+
|
|
1821
|
+
profiles = _discover_ovcli_profiles()
|
|
1822
|
+
if profiles:
|
|
1823
|
+
setup_options = [
|
|
1824
|
+
("Use existing OpenViking profile", "choose from detected ovcli.conf profiles"),
|
|
1825
|
+
("Create new OpenViking profile", "enter a new URL/API key"),
|
|
1826
|
+
]
|
|
1827
|
+
choice = _curses_select(
|
|
1828
|
+
" OpenViking config source",
|
|
1829
|
+
setup_options,
|
|
1830
|
+
default=0,
|
|
1831
|
+
cancel_returns=_CANCELLED,
|
|
1832
|
+
)
|
|
1833
|
+
if choice == _CANCELLED:
|
|
1834
|
+
_print_cancelled_setup()
|
|
1835
|
+
return
|
|
1836
|
+
|
|
1837
|
+
if choice == 0:
|
|
1838
|
+
result = _run_existing_profile_setup(
|
|
1839
|
+
profiles=profiles,
|
|
1840
|
+
select=_curses_select,
|
|
1841
|
+
cancelled=_CANCELLED,
|
|
1842
|
+
config=config,
|
|
1843
|
+
provider_config=provider_config,
|
|
1844
|
+
env_path=env_path,
|
|
1845
|
+
)
|
|
1846
|
+
if result is _SETUP_CANCELLED:
|
|
1847
|
+
_print_cancelled_setup()
|
|
1848
|
+
return
|
|
1849
|
+
if result:
|
|
1850
|
+
save_config(config)
|
|
1851
|
+
return
|
|
1852
|
+
|
|
1853
|
+
else:
|
|
1854
|
+
print(" No existing OpenViking CLI profiles found. Creating a new config.")
|
|
1855
|
+
|
|
1856
|
+
result = _run_create_profile_setup(
|
|
1857
|
+
prompt=_prompt,
|
|
1858
|
+
select=_curses_select,
|
|
1859
|
+
cancelled=_CANCELLED,
|
|
1860
|
+
config=config,
|
|
1861
|
+
provider_config=provider_config,
|
|
1862
|
+
env_path=env_path,
|
|
1863
|
+
)
|
|
1864
|
+
if result is _SETUP_CANCELLED:
|
|
1865
|
+
_print_cancelled_setup()
|
|
1866
|
+
return
|
|
1867
|
+
if result:
|
|
1868
|
+
save_config(config)
|
|
1869
|
+
|
|
1870
|
+
def _start_runtime_openviking_waiter(
|
|
1871
|
+
self,
|
|
1872
|
+
*,
|
|
1873
|
+
status_callback=None,
|
|
1874
|
+
warning_callback=None,
|
|
1875
|
+
) -> None:
|
|
1876
|
+
with self._runtime_start_lock:
|
|
1877
|
+
if self._runtime_start_thread and self._runtime_start_thread.is_alive():
|
|
1878
|
+
return
|
|
1879
|
+
self._runtime_start_thread = threading.Thread(
|
|
1880
|
+
target=self._finish_runtime_openviking_start,
|
|
1881
|
+
kwargs={
|
|
1882
|
+
"status_callback": status_callback,
|
|
1883
|
+
"warning_callback": warning_callback,
|
|
1884
|
+
},
|
|
1885
|
+
daemon=True,
|
|
1886
|
+
name="openviking-runtime-start",
|
|
1887
|
+
)
|
|
1888
|
+
self._runtime_start_thread.start()
|
|
1889
|
+
|
|
1890
|
+
def _finish_runtime_openviking_start(
|
|
1891
|
+
self,
|
|
1892
|
+
*,
|
|
1893
|
+
status_callback=None,
|
|
1894
|
+
warning_callback=None,
|
|
1895
|
+
) -> None:
|
|
1896
|
+
endpoint = self._endpoint
|
|
1897
|
+
if not _wait_for_openviking_health(
|
|
1898
|
+
endpoint,
|
|
1899
|
+
timeout_seconds=_LOCAL_OPENVIKING_AUTOSTART_TIMEOUT,
|
|
1900
|
+
):
|
|
1901
|
+
_emit_runtime_warning(
|
|
1902
|
+
_runtime_openviking_timeout_message(endpoint),
|
|
1903
|
+
warning_callback,
|
|
1904
|
+
)
|
|
1905
|
+
return
|
|
1906
|
+
|
|
1907
|
+
try:
|
|
1908
|
+
client = _VikingClient(
|
|
1909
|
+
endpoint,
|
|
1910
|
+
self._api_key,
|
|
1911
|
+
account=self._account,
|
|
1912
|
+
user=self._user,
|
|
1913
|
+
agent=self._agent,
|
|
1914
|
+
)
|
|
1915
|
+
if not client.health():
|
|
1916
|
+
_emit_runtime_warning(
|
|
1917
|
+
f"OpenViking server at {endpoint} is still not reachable after auto-start; "
|
|
1918
|
+
"OpenViking memory disabled for this Hermes run.",
|
|
1919
|
+
warning_callback,
|
|
1920
|
+
)
|
|
1921
|
+
return
|
|
1922
|
+
except ImportError:
|
|
1923
|
+
logger.warning("httpx not installed — OpenViking plugin disabled")
|
|
1924
|
+
return
|
|
1925
|
+
except Exception as e:
|
|
1926
|
+
_emit_runtime_warning(
|
|
1927
|
+
f"OpenViking server at {endpoint} could not be attached after auto-start: {e}. "
|
|
1928
|
+
"OpenViking memory disabled for this Hermes run.",
|
|
1929
|
+
warning_callback,
|
|
1930
|
+
)
|
|
1931
|
+
return
|
|
1932
|
+
|
|
1933
|
+
self._client = client
|
|
1934
|
+
_emit_runtime_status(
|
|
1935
|
+
f"Local OpenViking server at {endpoint} is reachable; OpenViking memory is active for later turns.",
|
|
1936
|
+
status_callback,
|
|
1937
|
+
)
|
|
1938
|
+
|
|
1939
|
+
def _handle_runtime_openviking_unreachable(
|
|
1940
|
+
self,
|
|
1941
|
+
*,
|
|
1942
|
+
status_callback=None,
|
|
1943
|
+
warning_callback=None,
|
|
1944
|
+
) -> None:
|
|
1945
|
+
endpoint = self._endpoint
|
|
1946
|
+
if not _is_local_openviking_url(endpoint):
|
|
1947
|
+
_emit_runtime_warning(
|
|
1948
|
+
f"Remote OpenViking server at {endpoint} is not reachable; "
|
|
1949
|
+
"OpenViking memory disabled for this Hermes run. "
|
|
1950
|
+
"Check the configured endpoint and network connectivity.",
|
|
1951
|
+
warning_callback,
|
|
1952
|
+
)
|
|
1953
|
+
self._client = None
|
|
1954
|
+
return
|
|
1955
|
+
|
|
1956
|
+
started, start_message = _start_local_openviking_server(endpoint)
|
|
1957
|
+
if not started:
|
|
1958
|
+
_emit_runtime_warning(
|
|
1959
|
+
f"Local OpenViking server at {endpoint} is not reachable. {start_message} "
|
|
1960
|
+
"OpenViking memory disabled for this Hermes run.",
|
|
1961
|
+
warning_callback,
|
|
1962
|
+
)
|
|
1963
|
+
self._client = None
|
|
1964
|
+
return
|
|
1965
|
+
|
|
1966
|
+
self._client = None
|
|
1967
|
+
_emit_runtime_status(
|
|
1968
|
+
f"{start_message} OpenViking memory is starting in the background and will attach when ready.",
|
|
1969
|
+
status_callback,
|
|
1970
|
+
)
|
|
1971
|
+
self._start_runtime_openviking_waiter(
|
|
1972
|
+
status_callback=status_callback,
|
|
1973
|
+
warning_callback=warning_callback,
|
|
1974
|
+
)
|
|
1975
|
+
|
|
469
1976
|
def initialize(self, session_id: str, **kwargs) -> None:
|
|
470
|
-
|
|
471
|
-
self.
|
|
472
|
-
self.
|
|
473
|
-
self.
|
|
474
|
-
self.
|
|
1977
|
+
settings = _resolve_connection_settings(_load_hermes_openviking_config())
|
|
1978
|
+
self._endpoint = settings["endpoint"]
|
|
1979
|
+
self._api_key = settings["api_key"]
|
|
1980
|
+
self._account = settings["account"]
|
|
1981
|
+
self._user = settings["user"]
|
|
1982
|
+
self._agent = settings["agent"]
|
|
475
1983
|
self._session_id = session_id
|
|
476
1984
|
self._turn_count = 0
|
|
1985
|
+
warning_callback = (
|
|
1986
|
+
kwargs.get("warning_callback")
|
|
1987
|
+
if kwargs.get("platform") == "cli"
|
|
1988
|
+
else None
|
|
1989
|
+
)
|
|
1990
|
+
status_callback = (
|
|
1991
|
+
kwargs.get("status_callback")
|
|
1992
|
+
if kwargs.get("platform") == "cli"
|
|
1993
|
+
else None
|
|
1994
|
+
)
|
|
477
1995
|
|
|
478
1996
|
try:
|
|
479
1997
|
self._client = _VikingClient(
|
|
480
1998
|
self._endpoint, self._api_key,
|
|
481
1999
|
account=self._account, user=self._user, agent=self._agent,
|
|
482
2000
|
)
|
|
483
|
-
|
|
484
|
-
|
|
2001
|
+
health_state, health_message = _classify_runtime_openviking_health(self._client, self._endpoint)
|
|
2002
|
+
if health_state == "unreachable":
|
|
2003
|
+
self._handle_runtime_openviking_unreachable(
|
|
2004
|
+
status_callback=status_callback,
|
|
2005
|
+
warning_callback=warning_callback,
|
|
2006
|
+
)
|
|
2007
|
+
elif health_state != "healthy":
|
|
2008
|
+
_emit_runtime_warning(
|
|
2009
|
+
f"{health_message} OpenViking memory disabled for this Hermes run.",
|
|
2010
|
+
warning_callback,
|
|
2011
|
+
)
|
|
485
2012
|
self._client = None
|
|
486
2013
|
except ImportError:
|
|
487
2014
|
logger.warning("httpx not installed — OpenViking plugin disabled")
|
|
@@ -531,9 +2058,16 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
|
|
531
2058
|
|
|
532
2059
|
def queue_prefetch(self, query: str, *, session_id: str = "") -> None:
|
|
533
2060
|
"""Fire a background search to pre-load relevant context."""
|
|
2061
|
+
query = _derive_openviking_user_text(query)
|
|
534
2062
|
if not self._client or not query:
|
|
535
2063
|
return
|
|
536
2064
|
|
|
2065
|
+
# Drop prefetch results from older switch generations.
|
|
2066
|
+
with self._prefetch_lock:
|
|
2067
|
+
gen = self._prefetch_generation
|
|
2068
|
+
|
|
2069
|
+
holder: List[threading.Thread] = []
|
|
2070
|
+
|
|
537
2071
|
def _run():
|
|
538
2072
|
try:
|
|
539
2073
|
client = _VikingClient(
|
|
@@ -542,7 +2076,7 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
|
|
542
2076
|
)
|
|
543
2077
|
resp = client.post("/api/v1/search/find", {
|
|
544
2078
|
"query": query,
|
|
545
|
-
"
|
|
2079
|
+
"limit": 5,
|
|
546
2080
|
})
|
|
547
2081
|
result = resp.get("result", {})
|
|
548
2082
|
parts = []
|
|
@@ -556,51 +2090,594 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
|
|
556
2090
|
parts.append(f"- [{score:.2f}] {abstract} ({uri})")
|
|
557
2091
|
if parts:
|
|
558
2092
|
with self._prefetch_lock:
|
|
2093
|
+
if gen != self._prefetch_generation:
|
|
2094
|
+
return
|
|
559
2095
|
self._prefetch_result = "\n".join(parts)
|
|
560
2096
|
except Exception as e:
|
|
561
2097
|
logger.debug("OpenViking prefetch failed: %s", e)
|
|
2098
|
+
finally:
|
|
2099
|
+
with self._prefetch_lock:
|
|
2100
|
+
if holder:
|
|
2101
|
+
self._prefetch_threads.discard(holder[0])
|
|
562
2102
|
|
|
563
|
-
|
|
2103
|
+
thread = threading.Thread(
|
|
564
2104
|
target=_run, daemon=True, name="openviking-prefetch"
|
|
565
2105
|
)
|
|
566
|
-
|
|
2106
|
+
holder.append(thread)
|
|
2107
|
+
with self._prefetch_lock:
|
|
2108
|
+
self._prefetch_thread = thread
|
|
2109
|
+
self._prefetch_threads.add(thread)
|
|
2110
|
+
thread.start()
|
|
2111
|
+
|
|
2112
|
+
def _spawn_writer(self, sid: str, target: Callable[[], None], name: str) -> None:
|
|
2113
|
+
"""Spawn a daemon writer tracked in _inflight_writers[sid].
|
|
2114
|
+
|
|
2115
|
+
Tracking is keyed by sid (not by a single latest-thread slot) so that
|
|
2116
|
+
on_session_end / on_session_switch can drain every still-alive writer
|
|
2117
|
+
for the session being committed.
|
|
2118
|
+
"""
|
|
2119
|
+
holder: List[threading.Thread] = []
|
|
2120
|
+
|
|
2121
|
+
def _wrapped():
|
|
2122
|
+
try:
|
|
2123
|
+
target()
|
|
2124
|
+
finally:
|
|
2125
|
+
with self._inflight_lock:
|
|
2126
|
+
workers = self._inflight_writers.get(sid)
|
|
2127
|
+
if workers is not None:
|
|
2128
|
+
workers.discard(holder[0])
|
|
2129
|
+
if not workers:
|
|
2130
|
+
self._inflight_writers.pop(sid, None)
|
|
2131
|
+
|
|
2132
|
+
thread = threading.Thread(target=_wrapped, daemon=True, name=name)
|
|
2133
|
+
holder.append(thread)
|
|
2134
|
+
with self._inflight_lock:
|
|
2135
|
+
self._inflight_writers.setdefault(sid, set()).add(thread)
|
|
2136
|
+
thread.start()
|
|
2137
|
+
|
|
2138
|
+
def _drain_finalizers(self, timeout: float) -> bool:
|
|
2139
|
+
"""Join every in-flight async session finalizer within a timeout.
|
|
2140
|
+
|
|
2141
|
+
The switch-path commit runs on a daemon finalizer thread so it never
|
|
2142
|
+
blocks the caller's command thread; this lets shutdown and tests wait
|
|
2143
|
+
for those commits deterministically. Returns True if all drained.
|
|
2144
|
+
"""
|
|
2145
|
+
deadline = time.monotonic() + timeout
|
|
2146
|
+
while True:
|
|
2147
|
+
with self._deferred_commit_lock:
|
|
2148
|
+
workers = [t for t in self._deferred_commit_threads if t.is_alive()]
|
|
2149
|
+
if not workers:
|
|
2150
|
+
return True
|
|
2151
|
+
remaining = deadline - time.monotonic()
|
|
2152
|
+
if remaining <= 0:
|
|
2153
|
+
return False
|
|
2154
|
+
for t in workers:
|
|
2155
|
+
slice_left = deadline - time.monotonic()
|
|
2156
|
+
if slice_left <= 0:
|
|
2157
|
+
break
|
|
2158
|
+
# Floor the per-join wait so a thread whose join() returns
|
|
2159
|
+
# instantly while still reporting alive can't hot-spin this loop.
|
|
2160
|
+
t.join(timeout=min(slice_left, 0.05))
|
|
2161
|
+
|
|
2162
|
+
def _drain_writers(self, sid: str, timeout: float) -> bool:
|
|
2163
|
+
"""Join every in-flight writer for sid within a shared timeout budget.
|
|
2164
|
+
|
|
2165
|
+
Returns True if all writers drained, False if any are still alive when
|
|
2166
|
+
the budget runs out. Callers use the False return to skip the commit.
|
|
2167
|
+
"""
|
|
2168
|
+
if not sid:
|
|
2169
|
+
return True
|
|
2170
|
+
deadline = time.monotonic() + timeout
|
|
2171
|
+
while True:
|
|
2172
|
+
with self._inflight_lock:
|
|
2173
|
+
workers = [t for t in self._inflight_writers.get(sid, ()) if t.is_alive()]
|
|
2174
|
+
if not workers:
|
|
2175
|
+
return True
|
|
2176
|
+
remaining = deadline - time.monotonic()
|
|
2177
|
+
if remaining <= 0:
|
|
2178
|
+
return False
|
|
2179
|
+
for t in workers:
|
|
2180
|
+
slice_left = deadline - time.monotonic()
|
|
2181
|
+
if slice_left <= 0:
|
|
2182
|
+
break
|
|
2183
|
+
t.join(timeout=slice_left)
|
|
2184
|
+
|
|
2185
|
+
def _new_client(self) -> _VikingClient:
|
|
2186
|
+
return _VikingClient(
|
|
2187
|
+
self._endpoint,
|
|
2188
|
+
self._api_key,
|
|
2189
|
+
account=self._account,
|
|
2190
|
+
user=self._user,
|
|
2191
|
+
agent=self._agent,
|
|
2192
|
+
)
|
|
2193
|
+
|
|
2194
|
+
@staticmethod
|
|
2195
|
+
def _text_part(content: str) -> Dict[str, str]:
|
|
2196
|
+
return {"type": "text", "text": content}
|
|
2197
|
+
|
|
2198
|
+
def _turn_batch_payload(self, user_content: str, assistant_content: str) -> Dict[str, Any]:
|
|
2199
|
+
assistant_message: Dict[str, Any] = {
|
|
2200
|
+
"role": "assistant",
|
|
2201
|
+
"parts": [self._text_part(assistant_content)],
|
|
2202
|
+
}
|
|
2203
|
+
if self._agent:
|
|
2204
|
+
assistant_message["peer_id"] = self._agent
|
|
2205
|
+
return {
|
|
2206
|
+
"messages": [
|
|
2207
|
+
{"role": "user", "parts": [self._text_part(user_content)]},
|
|
2208
|
+
assistant_message,
|
|
2209
|
+
]
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
def _post_session_turn(
|
|
2213
|
+
self,
|
|
2214
|
+
client: _VikingClient,
|
|
2215
|
+
sid: str,
|
|
2216
|
+
user_content: str,
|
|
2217
|
+
assistant_content: str,
|
|
2218
|
+
) -> None:
|
|
2219
|
+
client.post(
|
|
2220
|
+
f"/api/v1/sessions/{sid}/messages/batch",
|
|
2221
|
+
self._turn_batch_payload(user_content, assistant_content),
|
|
2222
|
+
)
|
|
2223
|
+
|
|
2224
|
+
def _session_has_pending_tokens(self, sid: str) -> bool:
|
|
2225
|
+
try:
|
|
2226
|
+
response = self._client.get(f"/api/v1/sessions/{sid}")
|
|
2227
|
+
except Exception:
|
|
2228
|
+
return False
|
|
2229
|
+
session = self._unwrap_result(response)
|
|
2230
|
+
if not isinstance(session, dict):
|
|
2231
|
+
return False
|
|
2232
|
+
try:
|
|
2233
|
+
return int(session.get("pending_tokens") or 0) > 0
|
|
2234
|
+
except (TypeError, ValueError):
|
|
2235
|
+
return False
|
|
2236
|
+
|
|
2237
|
+
def _has_committed_session(self, sid: str) -> bool:
|
|
2238
|
+
with self._committed_session_lock:
|
|
2239
|
+
return sid in self._committed_session_ids
|
|
2240
|
+
|
|
2241
|
+
def _mark_session_committed(self, sid: str) -> None:
|
|
2242
|
+
with self._committed_session_lock:
|
|
2243
|
+
self._committed_session_ids.add(sid)
|
|
2244
|
+
|
|
2245
|
+
def _session_needs_commit(self, sid: str, turn_count: int) -> bool:
|
|
2246
|
+
# Already-committed sessions never need a second commit, regardless of
|
|
2247
|
+
# the turn counter — a racing sync_turn can re-increment _turn_count
|
|
2248
|
+
# after a commit+reset, so the committed-guard must win over turn_count.
|
|
2249
|
+
if self._has_committed_session(sid):
|
|
2250
|
+
return False
|
|
2251
|
+
if turn_count > 0:
|
|
2252
|
+
return True
|
|
2253
|
+
return self._session_has_pending_tokens(sid)
|
|
2254
|
+
|
|
2255
|
+
def _commit_session(self, sid: str, turn_count: int, *, context: str) -> bool:
|
|
2256
|
+
try:
|
|
2257
|
+
self._client.post(
|
|
2258
|
+
f"/api/v1/sessions/{sid}/commit",
|
|
2259
|
+
{"keep_recent_count": 0},
|
|
2260
|
+
)
|
|
2261
|
+
self._mark_session_committed(sid)
|
|
2262
|
+
logger.info("OpenViking session %s committed %s (%d turns)", sid, context, turn_count)
|
|
2263
|
+
return True
|
|
2264
|
+
except Exception as e:
|
|
2265
|
+
logger.warning("OpenViking session commit failed for %s: %s", sid, e)
|
|
2266
|
+
return False
|
|
2267
|
+
|
|
2268
|
+
def _finalize_session_async(self, sid: str, turn_count: int, *, context: str) -> None:
|
|
2269
|
+
"""Drain the old session's writers and commit it on a daemon thread.
|
|
2270
|
+
|
|
2271
|
+
Used by on_session_switch (and the deferred-commit fallback) so the
|
|
2272
|
+
potentially-multi-second drain + pending-token GET + commit POST never
|
|
2273
|
+
runs on the caller's command thread. Deduped by sid so a rapid second
|
|
2274
|
+
switch can't stack two finalizers for the same session, and a no-op
|
|
2275
|
+
once shutdown has begun so we don't POST against a torn-down client.
|
|
2276
|
+
"""
|
|
2277
|
+
if not sid:
|
|
2278
|
+
return
|
|
2279
|
+
with self._deferred_commit_lock:
|
|
2280
|
+
if self._shutting_down or sid in self._deferred_commit_sids:
|
|
2281
|
+
return
|
|
2282
|
+
self._deferred_commit_sids.add(sid)
|
|
2283
|
+
|
|
2284
|
+
holder: List[threading.Thread] = []
|
|
2285
|
+
|
|
2286
|
+
def _finalize() -> None:
|
|
2287
|
+
try:
|
|
2288
|
+
if self._shutting_down:
|
|
2289
|
+
return
|
|
2290
|
+
if not self._drain_writers(sid, timeout=_DEFERRED_COMMIT_TIMEOUT):
|
|
2291
|
+
logger.warning(
|
|
2292
|
+
"OpenViking writer for %s still alive after drain — "
|
|
2293
|
+
"leaving session uncommitted",
|
|
2294
|
+
sid,
|
|
2295
|
+
)
|
|
2296
|
+
return
|
|
2297
|
+
if self._shutting_down:
|
|
2298
|
+
return
|
|
2299
|
+
if self._session_needs_commit(sid, turn_count):
|
|
2300
|
+
self._commit_session(sid, turn_count, context=context)
|
|
2301
|
+
finally:
|
|
2302
|
+
with self._deferred_commit_lock:
|
|
2303
|
+
self._deferred_commit_sids.discard(sid)
|
|
2304
|
+
if holder:
|
|
2305
|
+
self._deferred_commit_threads.discard(holder[0])
|
|
2306
|
+
|
|
2307
|
+
thread = threading.Thread(
|
|
2308
|
+
target=_finalize,
|
|
2309
|
+
daemon=True,
|
|
2310
|
+
name=f"openviking-finalize-{sid}",
|
|
2311
|
+
)
|
|
2312
|
+
holder.append(thread)
|
|
2313
|
+
with self._deferred_commit_lock:
|
|
2314
|
+
self._deferred_commit_threads.add(thread)
|
|
2315
|
+
thread.start()
|
|
2316
|
+
|
|
2317
|
+
def _invalidate_prefetch_state(self) -> None:
|
|
2318
|
+
# Bump the generation under the same lock used by prefetch workers so
|
|
2319
|
+
# late results from an older session are discarded deterministically.
|
|
2320
|
+
with self._prefetch_lock:
|
|
2321
|
+
self._prefetch_generation += 1
|
|
2322
|
+
self._prefetch_result = ""
|
|
2323
|
+
# Join EVERY tracked prefetch thread, not just the latest slot — a
|
|
2324
|
+
# rapid re-queue can leave an older thread for the abandoned session
|
|
2325
|
+
# still running (consistent with shutdown()).
|
|
2326
|
+
workers = [t for t in self._prefetch_threads if t.is_alive()]
|
|
2327
|
+
for t in workers:
|
|
2328
|
+
t.join(timeout=3.0)
|
|
2329
|
+
with self._prefetch_lock:
|
|
2330
|
+
self._prefetch_result = ""
|
|
2331
|
+
|
|
2332
|
+
@staticmethod
|
|
2333
|
+
def _message_text(content: Any) -> str:
|
|
2334
|
+
"""Extract text from OpenAI-style string/list content."""
|
|
2335
|
+
return flatten_message_text(content)
|
|
2336
|
+
|
|
2337
|
+
@classmethod
|
|
2338
|
+
def _message_matches_text(cls, message: Dict[str, Any], expected: Any) -> bool:
|
|
2339
|
+
expected_text = cls._message_text(expected).strip()
|
|
2340
|
+
if not expected_text:
|
|
2341
|
+
return False
|
|
2342
|
+
actual_text = cls._message_text(message.get("content")).strip()
|
|
2343
|
+
return actual_text == expected_text
|
|
2344
|
+
|
|
2345
|
+
@classmethod
|
|
2346
|
+
def _extract_current_turn_messages(
|
|
2347
|
+
cls,
|
|
2348
|
+
messages: Optional[List[Dict[str, Any]]],
|
|
2349
|
+
user_content: str,
|
|
2350
|
+
assistant_content: str,
|
|
2351
|
+
) -> List[Dict[str, Any]]:
|
|
2352
|
+
"""Slice the completed turn out of Hermes' full canonical transcript."""
|
|
2353
|
+
if not messages:
|
|
2354
|
+
return []
|
|
2355
|
+
|
|
2356
|
+
end_idx: Optional[int] = None
|
|
2357
|
+
if cls._message_text(assistant_content).strip():
|
|
2358
|
+
for idx in range(len(messages) - 1, -1, -1):
|
|
2359
|
+
message = messages[idx]
|
|
2360
|
+
if (
|
|
2361
|
+
isinstance(message, dict)
|
|
2362
|
+
and message.get("role") == "assistant"
|
|
2363
|
+
and cls._message_matches_text(message, assistant_content)
|
|
2364
|
+
):
|
|
2365
|
+
end_idx = idx
|
|
2366
|
+
break
|
|
2367
|
+
if end_idx is None:
|
|
2368
|
+
for idx in range(len(messages) - 1, -1, -1):
|
|
2369
|
+
message = messages[idx]
|
|
2370
|
+
if isinstance(message, dict) and message.get("role") == "assistant":
|
|
2371
|
+
end_idx = idx
|
|
2372
|
+
break
|
|
2373
|
+
if end_idx is None:
|
|
2374
|
+
end_idx = len(messages) - 1
|
|
2375
|
+
|
|
2376
|
+
start_idx: Optional[int] = None
|
|
2377
|
+
if cls._message_text(user_content).strip():
|
|
2378
|
+
for idx in range(end_idx, -1, -1):
|
|
2379
|
+
message = messages[idx]
|
|
2380
|
+
if (
|
|
2381
|
+
isinstance(message, dict)
|
|
2382
|
+
and message.get("role") == "user"
|
|
2383
|
+
and cls._message_matches_text(message, user_content)
|
|
2384
|
+
):
|
|
2385
|
+
start_idx = idx
|
|
2386
|
+
break
|
|
2387
|
+
if start_idx is None:
|
|
2388
|
+
for idx in range(end_idx, -1, -1):
|
|
2389
|
+
message = messages[idx]
|
|
2390
|
+
if isinstance(message, dict) and message.get("role") == "user":
|
|
2391
|
+
start_idx = idx
|
|
2392
|
+
break
|
|
2393
|
+
if start_idx is None:
|
|
2394
|
+
return []
|
|
2395
|
+
|
|
2396
|
+
return [message for message in messages[start_idx : end_idx + 1] if isinstance(message, dict)]
|
|
2397
|
+
|
|
2398
|
+
@staticmethod
|
|
2399
|
+
def _tool_call_id(tool_call: Dict[str, Any]) -> str:
|
|
2400
|
+
return str(tool_call.get("id") or tool_call.get("tool_call_id") or "")
|
|
2401
|
+
|
|
2402
|
+
@staticmethod
|
|
2403
|
+
def _tool_call_name(tool_call: Dict[str, Any]) -> str:
|
|
2404
|
+
function = tool_call.get("function")
|
|
2405
|
+
if isinstance(function, dict):
|
|
2406
|
+
return str(function.get("name") or "")
|
|
2407
|
+
return str(tool_call.get("name") or "")
|
|
2408
|
+
|
|
2409
|
+
@staticmethod
|
|
2410
|
+
def _is_openviking_recall_tool_name(tool_name: Any) -> bool:
|
|
2411
|
+
return str(tool_name or "").strip().lower() in _OPENVIKING_RECALL_TOOL_NAMES
|
|
2412
|
+
|
|
2413
|
+
@staticmethod
|
|
2414
|
+
def _tool_call_input(tool_call: Dict[str, Any]) -> Dict[str, Any]:
|
|
2415
|
+
function = tool_call.get("function")
|
|
2416
|
+
raw_args: Any = None
|
|
2417
|
+
if isinstance(function, dict):
|
|
2418
|
+
raw_args = function.get("arguments")
|
|
2419
|
+
if raw_args is None:
|
|
2420
|
+
raw_args = tool_call.get("args")
|
|
2421
|
+
if raw_args is None:
|
|
2422
|
+
return {}
|
|
2423
|
+
if isinstance(raw_args, dict):
|
|
2424
|
+
return raw_args
|
|
2425
|
+
if isinstance(raw_args, str):
|
|
2426
|
+
if not raw_args.strip():
|
|
2427
|
+
return {}
|
|
2428
|
+
try:
|
|
2429
|
+
parsed = json.loads(raw_args)
|
|
2430
|
+
except Exception:
|
|
2431
|
+
return {"value": raw_args}
|
|
2432
|
+
if isinstance(parsed, dict):
|
|
2433
|
+
return parsed
|
|
2434
|
+
return {"value": parsed}
|
|
2435
|
+
return {"value": raw_args}
|
|
2436
|
+
|
|
2437
|
+
@classmethod
|
|
2438
|
+
def _tool_result_status(cls, message: Dict[str, Any]) -> str:
|
|
2439
|
+
raw_status = str(message.get("status") or message.get("tool_status") or "").lower()
|
|
2440
|
+
if raw_status in _TOOL_STATUS_ERROR_ALIASES:
|
|
2441
|
+
return _TOOL_STATUS_ERROR
|
|
2442
|
+
if raw_status in _TOOL_STATUS_COMPLETED_ALIASES:
|
|
2443
|
+
return _TOOL_STATUS_COMPLETED
|
|
2444
|
+
|
|
2445
|
+
text = cls._message_text(message.get("content")).strip()
|
|
2446
|
+
if text:
|
|
2447
|
+
try:
|
|
2448
|
+
parsed = json.loads(text)
|
|
2449
|
+
except Exception:
|
|
2450
|
+
parsed = None
|
|
2451
|
+
if isinstance(parsed, dict):
|
|
2452
|
+
status = str(parsed.get("status") or "").lower()
|
|
2453
|
+
exit_code = parsed.get("exit_code")
|
|
2454
|
+
if (
|
|
2455
|
+
status in _TOOL_STATUS_ERROR_ALIASES
|
|
2456
|
+
or parsed.get("success") is False
|
|
2457
|
+
or bool(parsed.get("error"))
|
|
2458
|
+
or (isinstance(exit_code, int) and exit_code != 0)
|
|
2459
|
+
):
|
|
2460
|
+
return _TOOL_STATUS_ERROR
|
|
2461
|
+
|
|
2462
|
+
return _TOOL_STATUS_COMPLETED
|
|
2463
|
+
|
|
2464
|
+
@classmethod
|
|
2465
|
+
def _messages_to_openviking_batch(
|
|
2466
|
+
cls,
|
|
2467
|
+
messages: List[Dict[str, Any]],
|
|
2468
|
+
*,
|
|
2469
|
+
assistant_peer_id: str = "",
|
|
2470
|
+
) -> List[Dict[str, Any]]:
|
|
2471
|
+
"""Convert Hermes canonical messages into OpenViking batch payloads."""
|
|
2472
|
+
assistant_peer_id = str(assistant_peer_id or "").strip()
|
|
2473
|
+
tool_calls_by_id: Dict[str, Dict[str, Any]] = {}
|
|
2474
|
+
completed_tool_ids: set[str] = set()
|
|
2475
|
+
skipped_tool_ids: set[str] = set()
|
|
2476
|
+
for message in messages:
|
|
2477
|
+
if not isinstance(message, dict):
|
|
2478
|
+
continue
|
|
2479
|
+
if message.get("role") == "tool":
|
|
2480
|
+
tool_id = str(message.get("tool_call_id") or message.get("id") or "")
|
|
2481
|
+
if tool_id:
|
|
2482
|
+
completed_tool_ids.add(tool_id)
|
|
2483
|
+
if cls._is_openviking_recall_tool_name(message.get("name")):
|
|
2484
|
+
skipped_tool_ids.add(tool_id)
|
|
2485
|
+
continue
|
|
2486
|
+
if message.get("role") != "assistant":
|
|
2487
|
+
continue
|
|
2488
|
+
for tool_call in message.get("tool_calls") or []:
|
|
2489
|
+
if not isinstance(tool_call, dict):
|
|
2490
|
+
continue
|
|
2491
|
+
tool_id = cls._tool_call_id(tool_call)
|
|
2492
|
+
tool_name = cls._tool_call_name(tool_call)
|
|
2493
|
+
if tool_id:
|
|
2494
|
+
tool_calls_by_id[tool_id] = {
|
|
2495
|
+
"tool_name": tool_name,
|
|
2496
|
+
"tool_input": cls._tool_call_input(tool_call),
|
|
2497
|
+
}
|
|
2498
|
+
if cls._is_openviking_recall_tool_name(tool_name):
|
|
2499
|
+
skipped_tool_ids.add(tool_id)
|
|
2500
|
+
|
|
2501
|
+
payload_messages: List[Dict[str, Any]] = []
|
|
2502
|
+
pending_tool_parts: List[Dict[str, Any]] = []
|
|
2503
|
+
|
|
2504
|
+
def payload_message(role: str, parts: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
2505
|
+
payload: Dict[str, Any] = {"role": role, "parts": parts}
|
|
2506
|
+
if role == "assistant" and assistant_peer_id:
|
|
2507
|
+
payload["peer_id"] = assistant_peer_id
|
|
2508
|
+
return payload
|
|
2509
|
+
|
|
2510
|
+
def flush_tool_parts() -> None:
|
|
2511
|
+
nonlocal pending_tool_parts
|
|
2512
|
+
if pending_tool_parts:
|
|
2513
|
+
payload_messages.append(payload_message("assistant", pending_tool_parts))
|
|
2514
|
+
pending_tool_parts = []
|
|
2515
|
+
|
|
2516
|
+
for message in messages:
|
|
2517
|
+
if not isinstance(message, dict):
|
|
2518
|
+
continue
|
|
2519
|
+
|
|
2520
|
+
role = str(message.get("role") or "")
|
|
2521
|
+
if role in {"system", "developer"}:
|
|
2522
|
+
continue
|
|
2523
|
+
|
|
2524
|
+
if role == "tool":
|
|
2525
|
+
tool_id = str(message.get("tool_call_id") or message.get("id") or "")
|
|
2526
|
+
prior_call = tool_calls_by_id.get(tool_id, {})
|
|
2527
|
+
tool_name = str(message.get("name") or prior_call.get("tool_name") or "")
|
|
2528
|
+
if tool_id in skipped_tool_ids or cls._is_openviking_recall_tool_name(tool_name):
|
|
2529
|
+
continue
|
|
2530
|
+
tool_part = {
|
|
2531
|
+
"type": "tool",
|
|
2532
|
+
"tool_id": tool_id,
|
|
2533
|
+
"tool_name": tool_name,
|
|
2534
|
+
"tool_input": prior_call.get("tool_input", {}),
|
|
2535
|
+
"tool_output": cls._message_text(message.get("content")),
|
|
2536
|
+
"tool_status": cls._tool_result_status(message),
|
|
2537
|
+
}
|
|
2538
|
+
pending_tool_parts.append(tool_part)
|
|
2539
|
+
continue
|
|
2540
|
+
|
|
2541
|
+
if role not in {"user", "assistant"}:
|
|
2542
|
+
continue
|
|
2543
|
+
|
|
2544
|
+
flush_tool_parts()
|
|
2545
|
+
parts: List[Dict[str, Any]] = []
|
|
2546
|
+
text = cls._message_text(message.get("content"))
|
|
2547
|
+
if text:
|
|
2548
|
+
parts.append({"type": "text", "text": text})
|
|
2549
|
+
|
|
2550
|
+
if role == "assistant":
|
|
2551
|
+
for tool_call in message.get("tool_calls") or []:
|
|
2552
|
+
if not isinstance(tool_call, dict):
|
|
2553
|
+
continue
|
|
2554
|
+
tool_id = cls._tool_call_id(tool_call)
|
|
2555
|
+
tool_name = cls._tool_call_name(tool_call)
|
|
2556
|
+
if tool_id in skipped_tool_ids or cls._is_openviking_recall_tool_name(tool_name):
|
|
2557
|
+
continue
|
|
2558
|
+
if tool_id in completed_tool_ids:
|
|
2559
|
+
continue
|
|
2560
|
+
# Reuse the tool_input parsed in the pre-scan when available
|
|
2561
|
+
# (non-empty ids are cached); fall back to parsing for the
|
|
2562
|
+
# uncached empty-id case so we never drop arguments.
|
|
2563
|
+
prior_call = tool_calls_by_id.get(tool_id) if tool_id else None
|
|
2564
|
+
tool_input = (
|
|
2565
|
+
prior_call["tool_input"]
|
|
2566
|
+
if prior_call is not None
|
|
2567
|
+
else cls._tool_call_input(tool_call)
|
|
2568
|
+
)
|
|
2569
|
+
parts.append({
|
|
2570
|
+
"type": "tool",
|
|
2571
|
+
"tool_id": tool_id,
|
|
2572
|
+
"tool_name": tool_name,
|
|
2573
|
+
"tool_input": tool_input,
|
|
2574
|
+
"tool_status": _TOOL_STATUS_PENDING,
|
|
2575
|
+
})
|
|
2576
|
+
|
|
2577
|
+
if parts:
|
|
2578
|
+
payload_messages.append(payload_message(role, parts))
|
|
2579
|
+
|
|
2580
|
+
flush_tool_parts()
|
|
2581
|
+
return payload_messages
|
|
567
2582
|
|
|
568
|
-
def sync_turn(
|
|
2583
|
+
def sync_turn(
|
|
2584
|
+
self,
|
|
2585
|
+
user_content: str,
|
|
2586
|
+
assistant_content: str,
|
|
2587
|
+
*,
|
|
2588
|
+
session_id: str = "",
|
|
2589
|
+
messages: Optional[List[Dict[str, Any]]] = None,
|
|
2590
|
+
) -> None:
|
|
569
2591
|
"""Record the conversation turn in OpenViking's session (non-blocking)."""
|
|
570
2592
|
if not self._client:
|
|
571
2593
|
return
|
|
572
2594
|
|
|
573
|
-
|
|
2595
|
+
user_content = _derive_openviking_user_text(user_content)
|
|
2596
|
+
if not user_content:
|
|
2597
|
+
return
|
|
2598
|
+
|
|
2599
|
+
turn_messages = (
|
|
2600
|
+
self._extract_current_turn_messages(messages, user_content, assistant_content)
|
|
2601
|
+
if messages is not None
|
|
2602
|
+
else []
|
|
2603
|
+
)
|
|
2604
|
+
if turn_messages:
|
|
2605
|
+
turn_messages = [dict(message) for message in turn_messages]
|
|
2606
|
+
for message in turn_messages:
|
|
2607
|
+
if message.get("role") == "user":
|
|
2608
|
+
message["content"] = user_content
|
|
2609
|
+
break
|
|
2610
|
+
batch_messages = self._messages_to_openviking_batch(
|
|
2611
|
+
turn_messages,
|
|
2612
|
+
assistant_peer_id=getattr(self, "_agent", _DEFAULT_AGENT),
|
|
2613
|
+
)
|
|
2614
|
+
|
|
2615
|
+
if _sync_trace_enabled():
|
|
2616
|
+
logger.info(
|
|
2617
|
+
"OpenViking sync_turn trace: session_arg=%r cached_session=%r "
|
|
2618
|
+
"messages_param_supported=true messages_present=%s message_count=%s "
|
|
2619
|
+
"turn_message_count=%d batch_message_count=%d user_len=%d assistant_len=%d "
|
|
2620
|
+
"user_preview=%r assistant_preview=%r",
|
|
2621
|
+
session_id,
|
|
2622
|
+
self._session_id,
|
|
2623
|
+
messages is not None,
|
|
2624
|
+
len(messages) if messages is not None else None,
|
|
2625
|
+
len(turn_messages),
|
|
2626
|
+
len(batch_messages),
|
|
2627
|
+
len(str(user_content or "")),
|
|
2628
|
+
len(str(assistant_content or "")),
|
|
2629
|
+
_preview(user_content),
|
|
2630
|
+
_preview(assistant_content),
|
|
2631
|
+
)
|
|
2632
|
+
|
|
2633
|
+
# Snapshot the sid and bump the turn counter atomically so a
|
|
2634
|
+
# concurrent on_session_switch/on_session_end can't interleave its
|
|
2635
|
+
# snapshot+reset between the read and the increment (lost turn) and so
|
|
2636
|
+
# the turn is unambiguously attributed to the session it targets.
|
|
2637
|
+
with self._session_state_lock:
|
|
2638
|
+
sid = str(session_id or self._session_id).strip()
|
|
2639
|
+
if not sid:
|
|
2640
|
+
return
|
|
2641
|
+
self._turn_count += 1
|
|
574
2642
|
|
|
575
2643
|
def _sync():
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
2644
|
+
def _post_turn(client: _VikingClient) -> None:
|
|
2645
|
+
if batch_messages:
|
|
2646
|
+
payload = {"messages": batch_messages}
|
|
2647
|
+
if _sync_trace_enabled():
|
|
2648
|
+
logger.info(
|
|
2649
|
+
"OpenViking sync_turn trace: POST /api/v1/sessions/%s/messages/batch payload=%s",
|
|
2650
|
+
sid,
|
|
2651
|
+
json.dumps(payload, ensure_ascii=False),
|
|
2652
|
+
)
|
|
2653
|
+
try:
|
|
2654
|
+
client.post(f"/api/v1/sessions/{sid}/messages/batch", payload)
|
|
2655
|
+
return
|
|
2656
|
+
except Exception as batch_error:
|
|
2657
|
+
logger.warning(
|
|
2658
|
+
"OpenViking structured sync failed; falling back to text sync: %s",
|
|
2659
|
+
batch_error,
|
|
2660
|
+
)
|
|
2661
|
+
|
|
2662
|
+
self._post_session_turn(
|
|
2663
|
+
client,
|
|
2664
|
+
sid,
|
|
2665
|
+
user_content[:4000],
|
|
2666
|
+
self._message_text(assistant_content)[:4000],
|
|
580
2667
|
)
|
|
581
|
-
sid = self._session_id
|
|
582
2668
|
|
|
583
|
-
|
|
584
|
-
client.
|
|
585
|
-
|
|
586
|
-
"content": user_content[:4000], # trim very long messages
|
|
587
|
-
})
|
|
588
|
-
# Add assistant message
|
|
589
|
-
client.post(f"/api/v1/sessions/{sid}/messages", {
|
|
590
|
-
"role": "assistant",
|
|
591
|
-
"content": assistant_content[:4000],
|
|
592
|
-
})
|
|
2669
|
+
try:
|
|
2670
|
+
client = self._new_client()
|
|
2671
|
+
_post_turn(client)
|
|
593
2672
|
except Exception as e:
|
|
594
|
-
logger.debug("OpenViking sync_turn failed: %s", e)
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
2673
|
+
logger.debug("OpenViking sync_turn failed, reconnecting: %s", e)
|
|
2674
|
+
try:
|
|
2675
|
+
client = self._new_client()
|
|
2676
|
+
_post_turn(client)
|
|
2677
|
+
except Exception as retry_error:
|
|
2678
|
+
logger.warning("OpenViking sync_turn failed: %s", retry_error)
|
|
599
2679
|
|
|
600
|
-
self.
|
|
601
|
-
target=_sync, daemon=True, name="openviking-sync"
|
|
602
|
-
)
|
|
603
|
-
self._sync_thread.start()
|
|
2680
|
+
self._spawn_writer(sid, _sync, name="openviking-sync")
|
|
604
2681
|
|
|
605
2682
|
def on_session_end(self, messages: List[Dict[str, Any]]) -> None:
|
|
606
2683
|
"""Commit the session to trigger memory extraction.
|
|
@@ -611,25 +2688,103 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
|
|
611
2688
|
if not self._client:
|
|
612
2689
|
return
|
|
613
2690
|
|
|
614
|
-
#
|
|
615
|
-
#
|
|
616
|
-
#
|
|
617
|
-
|
|
618
|
-
|
|
2691
|
+
# Snapshot sid + turn count atomically against a concurrent sync_turn
|
|
2692
|
+
# increment. on_session_end runs at teardown so the drain+commit stays
|
|
2693
|
+
# synchronous here (we want it to land before the process exits), but
|
|
2694
|
+
# the counter read must still be consistent.
|
|
2695
|
+
with self._session_state_lock:
|
|
2696
|
+
sid = self._session_id
|
|
2697
|
+
turn_count = self._turn_count
|
|
2698
|
+
|
|
2699
|
+
# Commit only after session writes drain.
|
|
2700
|
+
if not self._drain_writers(sid, timeout=_SESSION_DRAIN_TIMEOUT):
|
|
2701
|
+
logger.warning(
|
|
2702
|
+
"OpenViking writer for %s still alive after drain — skipping commit",
|
|
2703
|
+
sid,
|
|
2704
|
+
)
|
|
2705
|
+
return
|
|
619
2706
|
|
|
620
|
-
if self.
|
|
2707
|
+
if not self._session_needs_commit(sid, turn_count):
|
|
621
2708
|
return
|
|
622
2709
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
2710
|
+
if self._commit_session(sid, turn_count, context="on session end"):
|
|
2711
|
+
# Mark clean so a follow-up on_session_switch skips its own commit.
|
|
2712
|
+
with self._session_state_lock:
|
|
2713
|
+
if self._session_id == sid:
|
|
2714
|
+
self._turn_count = 0
|
|
2715
|
+
|
|
2716
|
+
def on_session_switch(
|
|
2717
|
+
self,
|
|
2718
|
+
new_session_id: str,
|
|
2719
|
+
*,
|
|
2720
|
+
parent_session_id: str = "",
|
|
2721
|
+
reset: bool = False,
|
|
2722
|
+
**kwargs,
|
|
2723
|
+
) -> None:
|
|
2724
|
+
"""Commit the old session and rotate cached state to the new session_id.
|
|
2725
|
+
|
|
2726
|
+
Fires on /resume, /branch, /reset, /new, and context compression.
|
|
2727
|
+
Without this hook, ``_session_id`` stays stuck at the value
|
|
2728
|
+
``initialize()`` cached, so subsequent ``sync_turn()`` writes land in
|
|
2729
|
+
the already-closed old session and ``on_session_end()`` tries to
|
|
2730
|
+
commit it a second time. The new session never accumulates messages,
|
|
2731
|
+
and memory extraction never fires for it. See hermes-agent#28296.
|
|
2732
|
+
|
|
2733
|
+
Flushes any in-flight sync under the old session_id, commits the old
|
|
2734
|
+
session if it has pending turns (same extraction semantics as
|
|
2735
|
+
``on_session_end``), drains and clears any stale prefetch result,
|
|
2736
|
+
then rotates ``_session_id`` and resets ``_turn_count``.
|
|
2737
|
+
"""
|
|
2738
|
+
new_id = str(new_session_id or "").strip()
|
|
2739
|
+
if not new_id or not self._client:
|
|
2740
|
+
return
|
|
2741
|
+
|
|
2742
|
+
rewound = bool(kwargs.get("rewound"))
|
|
2743
|
+
|
|
2744
|
+
# Rotate cached session state synchronously (cheap, in-memory) and
|
|
2745
|
+
# snapshot the old session under the lock so a concurrent sync_turn
|
|
2746
|
+
# either lands fully before the rotation (counted under old) or fully
|
|
2747
|
+
# after (counted under new) — never split. The OLD session's commit
|
|
2748
|
+
# (drain + pending-token GET + commit POST, potentially many seconds)
|
|
2749
|
+
# is then offloaded so /new, /branch, /resume, /undo never block the
|
|
2750
|
+
# caller's command thread (cf. the end-of-turn-sync offload in #41945).
|
|
2751
|
+
with self._session_state_lock:
|
|
2752
|
+
old_session_id = self._session_id
|
|
2753
|
+
old_turn_count = self._turn_count
|
|
2754
|
+
rotate = not (rewound or new_id == old_session_id)
|
|
2755
|
+
if rotate:
|
|
2756
|
+
self._session_id = new_id
|
|
2757
|
+
self._turn_count = 0
|
|
2758
|
+
|
|
2759
|
+
# Invalidate stale prefetch OUTSIDE the session lock — it takes its own
|
|
2760
|
+
# _prefetch_lock and may join a prefetch thread for up to 3s, which we
|
|
2761
|
+
# must not do while holding the session lock (would block sync_turn and
|
|
2762
|
+
# risk lock-ordering coupling).
|
|
2763
|
+
self._invalidate_prefetch_state()
|
|
2764
|
+
|
|
2765
|
+
if not rotate:
|
|
2766
|
+
# Same-session rewind (/undo) or no-op rotation: no commit, no
|
|
2767
|
+
# counter reset — just the prefetch invalidation above.
|
|
2768
|
+
logger.debug(
|
|
2769
|
+
"OpenViking on_session_switch invalidated state without rotation: "
|
|
2770
|
+
"session=%s rewound=%s",
|
|
2771
|
+
old_session_id, rewound,
|
|
2772
|
+
)
|
|
2773
|
+
return
|
|
2774
|
+
|
|
2775
|
+
# Drain + commit the OLD session off the command thread.
|
|
2776
|
+
if old_session_id:
|
|
2777
|
+
self._finalize_session_async(old_session_id, old_turn_count, context="on switch")
|
|
2778
|
+
|
|
2779
|
+
logger.debug(
|
|
2780
|
+
"OpenViking on_session_switch: old=%s new=%s parent=%s reset=%s",
|
|
2781
|
+
old_session_id, new_id, parent_session_id, reset,
|
|
2782
|
+
)
|
|
628
2783
|
|
|
629
2784
|
def _build_memory_uri(self, subdir: str) -> str:
|
|
630
|
-
"""Build a viking:// memory URI under the configured
|
|
2785
|
+
"""Build a viking:// memory URI under the configured peer namespace."""
|
|
631
2786
|
slug = uuid.uuid4().hex[:12]
|
|
632
|
-
return f"viking://user/{self.
|
|
2787
|
+
return f"viking://user/peers/{self._agent}/memories/{subdir}/mem_{slug}.md"
|
|
633
2788
|
|
|
634
2789
|
def on_memory_write(
|
|
635
2790
|
self,
|
|
@@ -685,11 +2840,28 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
|
|
685
2840
|
return tool_error(str(e))
|
|
686
2841
|
|
|
687
2842
|
def shutdown(self) -> None:
|
|
688
|
-
#
|
|
689
|
-
|
|
690
|
-
|
|
2843
|
+
# Stop deferred finalizers from issuing new commits against a
|
|
2844
|
+
# torn-down client, then drain everything still in flight.
|
|
2845
|
+
self._shutting_down = True
|
|
2846
|
+
# Wait for every in-flight writer across all tracked sessions.
|
|
2847
|
+
with self._inflight_lock:
|
|
2848
|
+
all_workers = [
|
|
2849
|
+
t for workers in self._inflight_writers.values() for t in workers
|
|
2850
|
+
]
|
|
2851
|
+
with self._deferred_commit_lock:
|
|
2852
|
+
deferred_workers = list(self._deferred_commit_threads)
|
|
2853
|
+
with self._prefetch_lock:
|
|
2854
|
+
prefetch_workers = list(self._prefetch_threads)
|
|
2855
|
+
for t in all_workers:
|
|
2856
|
+
if t.is_alive():
|
|
2857
|
+
t.join(timeout=5.0)
|
|
2858
|
+
for t in deferred_workers:
|
|
2859
|
+
if t.is_alive():
|
|
691
2860
|
t.join(timeout=5.0)
|
|
692
|
-
|
|
2861
|
+
for t in prefetch_workers:
|
|
2862
|
+
if t.is_alive():
|
|
2863
|
+
t.join(timeout=5.0)
|
|
2864
|
+
# Clear atexit reference so it doesn't double-commit.
|
|
693
2865
|
global _last_active_provider
|
|
694
2866
|
if _last_active_provider is self:
|
|
695
2867
|
_last_active_provider = None
|
|
@@ -743,14 +2915,16 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
|
|
743
2915
|
|
|
744
2916
|
payload: Dict[str, Any] = {"query": query}
|
|
745
2917
|
mode = args.get("mode", "auto")
|
|
746
|
-
if mode != "auto":
|
|
747
|
-
payload["mode"] = mode
|
|
748
2918
|
if args.get("scope"):
|
|
749
2919
|
payload["target_uri"] = args["scope"]
|
|
750
2920
|
if args.get("limit"):
|
|
751
|
-
payload["
|
|
2921
|
+
payload["limit"] = args["limit"]
|
|
2922
|
+
|
|
2923
|
+
endpoint = "/api/v1/search/search" if mode == "deep" else "/api/v1/search/find"
|
|
2924
|
+
if endpoint == "/api/v1/search/search" and self._session_id:
|
|
2925
|
+
payload["session_id"] = self._session_id
|
|
752
2926
|
|
|
753
|
-
resp = self._client.post(
|
|
2927
|
+
resp = self._client.post(endpoint, payload)
|
|
754
2928
|
result = resp.get("result", {})
|
|
755
2929
|
|
|
756
2930
|
# Format results for the model — keep it concise
|