@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
package/agent/hermes_state.py
CHANGED
|
@@ -29,11 +29,85 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar
|
|
|
29
29
|
|
|
30
30
|
logger = logging.getLogger(__name__)
|
|
31
31
|
|
|
32
|
+
def _delegate_from_json(col: str = "model_config") -> str:
|
|
33
|
+
return f"json_extract(COALESCE({col}, '{{}}'), '$._delegate_from')"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# A child session counts as a /branch (kept visible, never cascade-deleted) if
|
|
37
|
+
# it carries the stable marker OR the legacy end_reason heuristic holds.
|
|
38
|
+
_BRANCH_CHILD_SQL = (
|
|
39
|
+
"json_extract(COALESCE({a}.model_config, '{{}}'), '$._branched_from') IS NOT NULL"
|
|
40
|
+
" OR EXISTS (SELECT 1 FROM sessions p"
|
|
41
|
+
" WHERE p.id = {a}.parent_session_id"
|
|
42
|
+
" AND p.end_reason = 'branched'"
|
|
43
|
+
" AND {a}.started_at >= p.ended_at)"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
_COMPRESSION_CHILD_SQL = (
|
|
47
|
+
"EXISTS (SELECT 1 FROM sessions p"
|
|
48
|
+
" WHERE p.id = {a}.parent_session_id"
|
|
49
|
+
" AND p.end_reason = 'compression'"
|
|
50
|
+
" AND {a}.started_at >= p.ended_at)"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Rows that surface in pickers: roots + branch children (subagent runs and
|
|
54
|
+
# compression continuations stay hidden).
|
|
55
|
+
_LISTABLE_CHILD_SQL = f"(s.parent_session_id IS NULL OR {_BRANCH_CHILD_SQL.format(a='s')})"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _ephemeral_child_sql(alias: str = "s") -> str:
|
|
59
|
+
"""Subagent runs (cascade-delete targets), not branches or compression tips."""
|
|
60
|
+
branch = _BRANCH_CHILD_SQL.format(a=alias)
|
|
61
|
+
compression = _COMPRESSION_CHILD_SQL.format(a=alias)
|
|
62
|
+
return (
|
|
63
|
+
f"({alias}.parent_session_id IS NOT NULL"
|
|
64
|
+
f" AND NOT ({branch})"
|
|
65
|
+
f" AND NOT ({compression}))"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _collect_delegate_child_ids(conn, parent_ids: List[str]) -> List[str]:
|
|
70
|
+
"""Delegate-subagent ids to cascade-delete with *parent_ids*.
|
|
71
|
+
|
|
72
|
+
Only rows carrying the ``_delegate_from`` marker (set at creation, and
|
|
73
|
+
backfilled by the v16 migration) — generic untagged children keep the
|
|
74
|
+
orphan-don't-delete contract. Walks marker chains recursively so an
|
|
75
|
+
orchestrator subagent's own delegate children go too (FK safety).
|
|
76
|
+
"""
|
|
77
|
+
df = _delegate_from_json()
|
|
78
|
+
found: set[str] = set()
|
|
79
|
+
frontier = [sid for sid in parent_ids if sid]
|
|
80
|
+
while frontier:
|
|
81
|
+
ph = ",".join("?" * len(frontier))
|
|
82
|
+
cursor = conn.execute(
|
|
83
|
+
f"SELECT id FROM sessions WHERE {df} IN ({ph}) "
|
|
84
|
+
f"OR (parent_session_id IN ({ph}) AND {df} IS NOT NULL)",
|
|
85
|
+
frontier + frontier,
|
|
86
|
+
)
|
|
87
|
+
frontier = [row["id"] for row in cursor.fetchall() if row["id"] not in found]
|
|
88
|
+
found.update(frontier)
|
|
89
|
+
return list(found)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _delete_delegate_children(conn, parent_ids: List[str]) -> List[str]:
|
|
93
|
+
ids = _collect_delegate_child_ids(conn, parent_ids)
|
|
94
|
+
if ids:
|
|
95
|
+
ph = ",".join("?" * len(ids))
|
|
96
|
+
conn.execute(f"DELETE FROM messages WHERE session_id IN ({ph})", ids)
|
|
97
|
+
# FK safety: orphan any untagged stragglers pointing at a doomed row.
|
|
98
|
+
conn.execute(
|
|
99
|
+
f"UPDATE sessions SET parent_session_id = NULL "
|
|
100
|
+
f"WHERE parent_session_id IN ({ph})",
|
|
101
|
+
ids,
|
|
102
|
+
)
|
|
103
|
+
conn.execute(f"DELETE FROM sessions WHERE id IN ({ph})", ids)
|
|
104
|
+
return ids
|
|
105
|
+
|
|
32
106
|
T = TypeVar("T")
|
|
33
107
|
|
|
34
108
|
DEFAULT_DB_PATH = get_hermes_home() / "state.db"
|
|
35
109
|
|
|
36
|
-
SCHEMA_VERSION =
|
|
110
|
+
SCHEMA_VERSION = 16
|
|
37
111
|
|
|
38
112
|
# ---------------------------------------------------------------------------
|
|
39
113
|
# WAL-compatibility fallback
|
|
@@ -226,6 +300,212 @@ def _log_wal_fallback_once(db_label: str, exc: Exception) -> None:
|
|
|
226
300
|
exc,
|
|
227
301
|
)
|
|
228
302
|
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
# Malformed-schema recovery
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
# A distinct, nastier failure class than a malformed FTS *inverted index*:
|
|
307
|
+
# the ``sqlite_master`` schema table itself becomes inconsistent — most
|
|
308
|
+
# commonly a DUPLICATE object definition, e.g. two ``CREATE VIRTUAL TABLE
|
|
309
|
+
# messages_fts`` rows. SQLite parses the entire schema while preparing the
|
|
310
|
+
# FIRST statement on a connection, so on this class *every* statement raises
|
|
311
|
+
# before it runs — including ``PRAGMA journal_mode`` (which is why this trips
|
|
312
|
+
# in ``apply_wal_with_fallback`` during ``SessionDB.__init__``, long before
|
|
313
|
+
# ``_init_schema`` is reached) and even ``PRAGMA integrity_check`` and a plain
|
|
314
|
+
# ``DROP TABLE``. The only operations that still work are
|
|
315
|
+
# ``PRAGMA writable_schema=ON`` plus direct ``sqlite_master`` surgery.
|
|
316
|
+
#
|
|
317
|
+
# Symptom users hit (Desktop/Dashboard show "no sessions" while 200+ JSON
|
|
318
|
+
# files sit on disk):
|
|
319
|
+
# sqlite3.DatabaseError: malformed database schema (messages_fts) -
|
|
320
|
+
# table messages_fts already exists
|
|
321
|
+
#
|
|
322
|
+
# The canonical ``sessions`` / ``messages`` data is intact in these cases —
|
|
323
|
+
# only the derived schema is broken — so recovery preserves all transcripts
|
|
324
|
+
# and merely rebuilds the FTS layer.
|
|
325
|
+
_MALFORMED_SCHEMA_MARKERS = (
|
|
326
|
+
"malformed database schema",
|
|
327
|
+
"database disk image is malformed",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Process-global guard so auto-repair is attempted at most once per DB path
|
|
331
|
+
# per process (prevents repair loops and serialises concurrent web_server /
|
|
332
|
+
# gateway opens against the same malformed file).
|
|
333
|
+
_repair_attempted_paths: set[str] = set()
|
|
334
|
+
_repair_attempt_lock = threading.Lock()
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def is_malformed_db_error(exc: BaseException) -> bool:
|
|
338
|
+
"""True if *exc* is a SQLite 'malformed schema / disk image' error.
|
|
339
|
+
|
|
340
|
+
These are the corruption classes where the schema fails to parse, so
|
|
341
|
+
targeted ``sqlite_master`` surgery (not an ordinary FTS rebuild) is the
|
|
342
|
+
only recovery path.
|
|
343
|
+
"""
|
|
344
|
+
if not isinstance(exc, sqlite3.DatabaseError):
|
|
345
|
+
return False
|
|
346
|
+
return any(marker in str(exc).lower() for marker in _MALFORMED_SCHEMA_MARKERS)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _claim_repair_attempt(db_path: Path) -> bool:
|
|
350
|
+
"""Claim the one-shot repair attempt for *db_path* in this process.
|
|
351
|
+
|
|
352
|
+
Returns True for the first caller, False afterwards. Keeps a malformed
|
|
353
|
+
DB from triggering an unbounded repair/reopen loop and stops concurrent
|
|
354
|
+
callers from racing surgery on the same file.
|
|
355
|
+
"""
|
|
356
|
+
key = str(db_path)
|
|
357
|
+
with _repair_attempt_lock:
|
|
358
|
+
if key in _repair_attempted_paths:
|
|
359
|
+
return False
|
|
360
|
+
_repair_attempted_paths.add(key)
|
|
361
|
+
return True
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _backup_db_file(db_path: Path) -> Optional[Path]:
|
|
365
|
+
"""Copy a (possibly malformed) DB file to a timestamped backup beside it.
|
|
366
|
+
|
|
367
|
+
Raw file copy on purpose: the DB won't open cleanly, so we preserve the
|
|
368
|
+
bytes exactly for forensics / manual restore. WAL and SHM sidecars are
|
|
369
|
+
copied too when present. Returns the backup path, or None on failure.
|
|
370
|
+
"""
|
|
371
|
+
import datetime
|
|
372
|
+
import shutil
|
|
373
|
+
|
|
374
|
+
stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
375
|
+
backup_path = db_path.with_name(f"{db_path.name}.malformed-backup-{stamp}")
|
|
376
|
+
try:
|
|
377
|
+
shutil.copy2(db_path, backup_path)
|
|
378
|
+
for suffix in ("-wal", "-shm"):
|
|
379
|
+
sidecar = db_path.with_name(db_path.name + suffix)
|
|
380
|
+
if sidecar.exists():
|
|
381
|
+
shutil.copy2(sidecar, backup_path.with_name(backup_path.name + suffix))
|
|
382
|
+
return backup_path
|
|
383
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
384
|
+
logger.warning("Could not back up malformed DB %s: %s", db_path, exc)
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _db_opens_cleanly(db_path: Path) -> Optional[str]:
|
|
389
|
+
"""Probe a DB on a fresh connection. Returns None if healthy, else a reason.
|
|
390
|
+
|
|
391
|
+
Runs the same first-statement (``PRAGMA journal_mode``) that trips the
|
|
392
|
+
malformed-schema parse, then ``PRAGMA integrity_check`` and a canonical
|
|
393
|
+
``sessions`` read.
|
|
394
|
+
"""
|
|
395
|
+
conn = sqlite3.connect(str(db_path), isolation_level=None)
|
|
396
|
+
try:
|
|
397
|
+
conn.execute("PRAGMA journal_mode").fetchone()
|
|
398
|
+
rows = conn.execute("PRAGMA integrity_check").fetchall()
|
|
399
|
+
problems = [str(r[0]) for r in rows if r and str(r[0]).lower() != "ok"]
|
|
400
|
+
if problems:
|
|
401
|
+
return "; ".join(problems[:3])
|
|
402
|
+
conn.execute("SELECT COUNT(*) FROM sessions").fetchone()
|
|
403
|
+
return None
|
|
404
|
+
except sqlite3.DatabaseError as exc:
|
|
405
|
+
return str(exc)
|
|
406
|
+
finally:
|
|
407
|
+
conn.close()
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def repair_state_db_schema(db_path: Path, *, backup: bool = True) -> Dict[str, Any]:
|
|
411
|
+
"""Repair a state.db whose ``sqlite_master`` schema is malformed.
|
|
412
|
+
|
|
413
|
+
Handles the "duplicate object definition" / malformed-schema class where
|
|
414
|
+
even ``PRAGMA`` statements fail. Tries least-destructive recovery first
|
|
415
|
+
and escalates:
|
|
416
|
+
|
|
417
|
+
1. **De-duplicate** ``sqlite_master`` (keep the lowest rowid per
|
|
418
|
+
``type``/``name``). Fixes the canonical "table X already exists"
|
|
419
|
+
case and PRESERVES the existing FTS index intact.
|
|
420
|
+
2. **Drop the FTS schema** (every ``messages_fts*`` object) + ``VACUUM``.
|
|
421
|
+
The next ``SessionDB()`` open rebuilds the FTS indexes from the
|
|
422
|
+
canonical ``messages`` table.
|
|
423
|
+
|
|
424
|
+
Canonical ``sessions`` / ``messages`` rows are never modified. A
|
|
425
|
+
timestamped raw backup is taken first unless ``backup=False``.
|
|
426
|
+
|
|
427
|
+
Returns a report dict: ``{repaired: bool, strategy: str|None,
|
|
428
|
+
backup_path: str|None, error: str|None}``.
|
|
429
|
+
"""
|
|
430
|
+
report: Dict[str, Any] = {
|
|
431
|
+
"repaired": False,
|
|
432
|
+
"strategy": None,
|
|
433
|
+
"backup_path": None,
|
|
434
|
+
"error": None,
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
db_path = Path(db_path)
|
|
438
|
+
if not db_path.exists():
|
|
439
|
+
report["error"] = f"{db_path} does not exist"
|
|
440
|
+
return report
|
|
441
|
+
|
|
442
|
+
if backup:
|
|
443
|
+
bpath = _backup_db_file(db_path)
|
|
444
|
+
report["backup_path"] = str(bpath) if bpath else None
|
|
445
|
+
|
|
446
|
+
# ── Strategy 1: de-duplicate sqlite_master (keeps FTS index) ──
|
|
447
|
+
try:
|
|
448
|
+
conn = sqlite3.connect(str(db_path), isolation_level=None)
|
|
449
|
+
try:
|
|
450
|
+
conn.execute("PRAGMA writable_schema=ON")
|
|
451
|
+
dupes = conn.execute(
|
|
452
|
+
"SELECT type, name, COUNT(*) AS c, MIN(rowid) AS keep "
|
|
453
|
+
"FROM sqlite_master GROUP BY type, name HAVING c > 1"
|
|
454
|
+
).fetchall()
|
|
455
|
+
for type_, name, _count, keep in dupes:
|
|
456
|
+
conn.execute(
|
|
457
|
+
"DELETE FROM sqlite_master "
|
|
458
|
+
"WHERE type IS ? AND name IS ? AND rowid <> ?",
|
|
459
|
+
(type_, name, keep),
|
|
460
|
+
)
|
|
461
|
+
conn.execute("PRAGMA writable_schema=OFF")
|
|
462
|
+
conn.commit()
|
|
463
|
+
finally:
|
|
464
|
+
conn.close()
|
|
465
|
+
if _db_opens_cleanly(db_path) is None:
|
|
466
|
+
report["repaired"] = True
|
|
467
|
+
report["strategy"] = "dedup_schema"
|
|
468
|
+
logger.warning(
|
|
469
|
+
"state.db schema repaired by de-duplicating sqlite_master "
|
|
470
|
+
"(FTS index preserved): %s", db_path
|
|
471
|
+
)
|
|
472
|
+
return report
|
|
473
|
+
except sqlite3.DatabaseError as exc:
|
|
474
|
+
logger.warning("state.db dedup repair pass failed: %s", exc)
|
|
475
|
+
|
|
476
|
+
# ── Strategy 2: drop all FTS schema, VACUUM, rebuild on next open ──
|
|
477
|
+
try:
|
|
478
|
+
conn = sqlite3.connect(str(db_path), isolation_level=None)
|
|
479
|
+
try:
|
|
480
|
+
conn.execute("PRAGMA writable_schema=ON")
|
|
481
|
+
conn.execute("DELETE FROM sqlite_master WHERE name LIKE 'messages_fts%'")
|
|
482
|
+
conn.execute("PRAGMA writable_schema=OFF")
|
|
483
|
+
conn.commit()
|
|
484
|
+
conn.execute("VACUUM")
|
|
485
|
+
finally:
|
|
486
|
+
conn.close()
|
|
487
|
+
reason = _db_opens_cleanly(db_path)
|
|
488
|
+
if reason is None:
|
|
489
|
+
report["repaired"] = True
|
|
490
|
+
report["strategy"] = "drop_fts_rebuild"
|
|
491
|
+
logger.warning(
|
|
492
|
+
"state.db schema repaired by dropping FTS schema; indexes "
|
|
493
|
+
"will rebuild from messages on next open: %s", db_path
|
|
494
|
+
)
|
|
495
|
+
return report
|
|
496
|
+
report["error"] = reason
|
|
497
|
+
except sqlite3.DatabaseError as exc:
|
|
498
|
+
report["error"] = str(exc)
|
|
499
|
+
|
|
500
|
+
if not report["repaired"]:
|
|
501
|
+
logger.error(
|
|
502
|
+
"state.db schema repair could not recover %s automatically "
|
|
503
|
+
"(backup: %s); manual restore from backup may be required.",
|
|
504
|
+
db_path, report["backup_path"],
|
|
505
|
+
)
|
|
506
|
+
return report
|
|
507
|
+
|
|
508
|
+
|
|
229
509
|
SCHEMA_SQL = """
|
|
230
510
|
CREATE TABLE IF NOT EXISTS schema_version (
|
|
231
511
|
version INTEGER NOT NULL
|
|
@@ -302,6 +582,7 @@ CREATE TABLE IF NOT EXISTS compression_locks (
|
|
|
302
582
|
);
|
|
303
583
|
|
|
304
584
|
CREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source);
|
|
585
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_source_id ON sessions(source, id);
|
|
305
586
|
CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id);
|
|
306
587
|
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
|
|
307
588
|
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
|
|
@@ -396,32 +677,81 @@ class SessionDB:
|
|
|
396
677
|
# Attempt a PASSIVE WAL checkpoint every N successful writes.
|
|
397
678
|
_CHECKPOINT_EVERY_N_WRITES = 50
|
|
398
679
|
|
|
399
|
-
def __init__(self, db_path: Path = None):
|
|
680
|
+
def __init__(self, db_path: Path = None, read_only: bool = False):
|
|
400
681
|
self.db_path = db_path or DEFAULT_DB_PATH
|
|
401
|
-
self.
|
|
682
|
+
self.read_only = read_only
|
|
402
683
|
|
|
403
684
|
self._lock = threading.Lock()
|
|
404
685
|
self._write_count = 0
|
|
405
686
|
self._fts_enabled = False
|
|
687
|
+
self._trigram_available = False
|
|
406
688
|
self._fts_unavailable_warned = False
|
|
689
|
+
self._conn = None
|
|
407
690
|
try:
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
#
|
|
412
|
-
#
|
|
413
|
-
#
|
|
414
|
-
|
|
415
|
-
#
|
|
416
|
-
#
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
691
|
+
if read_only:
|
|
692
|
+
# Read-only attach for cross-profile aggregation: SELECT-only,
|
|
693
|
+
# so we skip schema init entirely (no DDL, no FTS probe, no
|
|
694
|
+
# column reconcile). Crucially this takes NO write lock, so
|
|
695
|
+
# polling another profile's live DB on every sidebar refresh
|
|
696
|
+
# never contends with that profile's running backend. The DB
|
|
697
|
+
# must already exist + be initialised (callers guard on
|
|
698
|
+
# db_path.exists()); a SELECT against an empty file raises and
|
|
699
|
+
# the caller degrades per-profile.
|
|
700
|
+
self._conn = sqlite3.connect(
|
|
701
|
+
f"file:{self.db_path}?mode=ro",
|
|
702
|
+
uri=True,
|
|
703
|
+
check_same_thread=False,
|
|
704
|
+
timeout=1.0,
|
|
705
|
+
isolation_level=None,
|
|
706
|
+
)
|
|
707
|
+
self._conn.row_factory = sqlite3.Row
|
|
708
|
+
return
|
|
709
|
+
|
|
710
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
711
|
+
|
|
712
|
+
def _connect_and_init():
|
|
713
|
+
self._conn = sqlite3.connect(
|
|
714
|
+
str(self.db_path),
|
|
715
|
+
check_same_thread=False,
|
|
716
|
+
# Short timeout — application-level retry with random
|
|
717
|
+
# jitter handles contention instead of sitting in
|
|
718
|
+
# SQLite's internal busy handler for up to 30s.
|
|
719
|
+
timeout=1.0,
|
|
720
|
+
# auto-starts transactions on DML, which conflicts with
|
|
721
|
+
# our explicit BEGIN IMMEDIATE. None = we manage
|
|
722
|
+
# transactions ourselves.
|
|
723
|
+
isolation_level=None,
|
|
724
|
+
)
|
|
725
|
+
self._conn.row_factory = sqlite3.Row
|
|
726
|
+
apply_wal_with_fallback(self._conn, db_label="state.db")
|
|
727
|
+
self._conn.execute("PRAGMA foreign_keys=ON")
|
|
728
|
+
self._init_schema()
|
|
423
729
|
|
|
424
|
-
|
|
730
|
+
try:
|
|
731
|
+
_connect_and_init()
|
|
732
|
+
except sqlite3.DatabaseError as exc:
|
|
733
|
+
# The malformed-schema class (e.g. a duplicate sqlite_master
|
|
734
|
+
# row for messages_fts) fails on the very first statement —
|
|
735
|
+
# before _init_schema can run — so it can't be caught at the
|
|
736
|
+
# FTS-rebuild layer. Recover by repairing sqlite_master in
|
|
737
|
+
# place (backup first; canonical sessions/messages preserved),
|
|
738
|
+
# then reopen once. This is what lets Desktop/Dashboard
|
|
739
|
+
# self-heal instead of silently showing "no sessions".
|
|
740
|
+
if not is_malformed_db_error(exc) or not _claim_repair_attempt(self.db_path):
|
|
741
|
+
raise
|
|
742
|
+
logger.error(
|
|
743
|
+
"state.db schema is malformed (%s) — attempting automatic "
|
|
744
|
+
"repair (a backup copy is made first).", exc,
|
|
745
|
+
)
|
|
746
|
+
try:
|
|
747
|
+
if self._conn is not None:
|
|
748
|
+
self._conn.close()
|
|
749
|
+
except Exception:
|
|
750
|
+
pass
|
|
751
|
+
report = repair_state_db_schema(self.db_path)
|
|
752
|
+
if not report.get("repaired"):
|
|
753
|
+
raise
|
|
754
|
+
_connect_and_init()
|
|
425
755
|
except Exception as exc:
|
|
426
756
|
# Capture the cause so /resume and friends can surface WHY the
|
|
427
757
|
# session DB is unavailable instead of a bare "Session database
|
|
@@ -443,7 +773,33 @@ class SessionDB:
|
|
|
443
773
|
@staticmethod
|
|
444
774
|
def _is_fts5_unavailable_error(exc: sqlite3.OperationalError) -> bool:
|
|
445
775
|
err = str(exc).lower()
|
|
446
|
-
|
|
776
|
+
if "no such module" in err and "fts5" in err:
|
|
777
|
+
return True
|
|
778
|
+
# SQLite builds that have FTS5 but lack the optional trigram tokenizer
|
|
779
|
+
# raise "no such tokenizer: trigram" instead of "no such module".
|
|
780
|
+
# Scope to trigram specifically to avoid masking unrelated tokenizer errors.
|
|
781
|
+
if "no such tokenizer: trigram" in err:
|
|
782
|
+
return True
|
|
783
|
+
return False
|
|
784
|
+
|
|
785
|
+
@staticmethod
|
|
786
|
+
def _is_trigram_unavailable_error(exc: sqlite3.OperationalError) -> bool:
|
|
787
|
+
"""True when only the trigram tokenizer is missing (FTS5 itself works)."""
|
|
788
|
+
return "no such tokenizer: trigram" in str(exc).lower()
|
|
789
|
+
|
|
790
|
+
def _warn_trigram_unavailable(self, exc: sqlite3.OperationalError) -> None:
|
|
791
|
+
"""Log once that the trigram tokenizer is missing; base FTS5 stays enabled."""
|
|
792
|
+
if getattr(self, "_trigram_unavailable_warned", False):
|
|
793
|
+
return
|
|
794
|
+
self._trigram_unavailable_warned = True
|
|
795
|
+
logger.info(
|
|
796
|
+
"SQLite trigram tokenizer unavailable for %s "
|
|
797
|
+
"(requires SQLite >= 3.34, this build is %s); "
|
|
798
|
+
"CJK/substring search will fall back to LIKE: %s",
|
|
799
|
+
self.db_path,
|
|
800
|
+
sqlite3.sqlite_version,
|
|
801
|
+
exc,
|
|
802
|
+
)
|
|
447
803
|
|
|
448
804
|
def _warn_fts5_unavailable(self, exc: sqlite3.OperationalError) -> None:
|
|
449
805
|
self._fts_enabled = False
|
|
@@ -452,12 +808,9 @@ class SessionDB:
|
|
|
452
808
|
self._fts_unavailable_warned = True
|
|
453
809
|
logger.warning(
|
|
454
810
|
"SQLite FTS5 unavailable for %s; full-text session search "
|
|
455
|
-
"disabled.
|
|
456
|
-
"
|
|
457
|
-
"
|
|
458
|
-
"mainline install. Some features may be missing or behave "
|
|
459
|
-
"differently. Install the supported way: "
|
|
460
|
-
"https://hermes-agent.nousresearch.com (underlying error: %s)",
|
|
811
|
+
"disabled. Run `hermes update` to rebuild the venv with a "
|
|
812
|
+
"current Python (managed uv guarantees FTS5). "
|
|
813
|
+
"(underlying error: %s)",
|
|
461
814
|
self.db_path,
|
|
462
815
|
exc,
|
|
463
816
|
)
|
|
@@ -492,9 +845,12 @@ class SessionDB:
|
|
|
492
845
|
return int(row[0] if not isinstance(row, sqlite3.Row) else row[0])
|
|
493
846
|
|
|
494
847
|
@staticmethod
|
|
495
|
-
def _rebuild_fts_indexes(
|
|
496
|
-
|
|
497
|
-
|
|
848
|
+
def _rebuild_fts_indexes(
|
|
849
|
+
cursor: sqlite3.Cursor,
|
|
850
|
+
*,
|
|
851
|
+
include_trigram: bool = True,
|
|
852
|
+
) -> None:
|
|
853
|
+
cursor.execute("DELETE FROM messages_fts")
|
|
498
854
|
cursor.execute(
|
|
499
855
|
"INSERT INTO messages_fts(rowid, content) "
|
|
500
856
|
"SELECT id, "
|
|
@@ -503,6 +859,9 @@ class SessionDB:
|
|
|
503
859
|
"COALESCE(tool_calls, '') "
|
|
504
860
|
"FROM messages"
|
|
505
861
|
)
|
|
862
|
+
if not include_trigram:
|
|
863
|
+
return
|
|
864
|
+
cursor.execute("DELETE FROM messages_fts_trigram")
|
|
506
865
|
cursor.execute(
|
|
507
866
|
"INSERT INTO messages_fts_trigram(rowid, content) "
|
|
508
867
|
"SELECT id, "
|
|
@@ -518,7 +877,12 @@ class SessionDB:
|
|
|
518
877
|
return True
|
|
519
878
|
except sqlite3.OperationalError as exc:
|
|
520
879
|
if self._is_fts5_unavailable_error(exc):
|
|
521
|
-
|
|
880
|
+
# Only disable FTS entirely when the whole module is missing.
|
|
881
|
+
# A missing trigram tokenizer only affects trigram searches.
|
|
882
|
+
if self._is_trigram_unavailable_error(exc):
|
|
883
|
+
self._warn_trigram_unavailable(exc)
|
|
884
|
+
else:
|
|
885
|
+
self._warn_fts5_unavailable(exc)
|
|
522
886
|
return None
|
|
523
887
|
if "no such table" in str(exc).lower():
|
|
524
888
|
return False
|
|
@@ -542,7 +906,13 @@ class SessionDB:
|
|
|
542
906
|
except sqlite3.OperationalError as exc:
|
|
543
907
|
if not self._is_fts5_unavailable_error(exc):
|
|
544
908
|
raise
|
|
545
|
-
|
|
909
|
+
# Only disable FTS entirely when the whole FTS5 module is missing.
|
|
910
|
+
# A missing specific tokenizer (e.g. trigram) means only that
|
|
911
|
+
# particular table cannot be created — the base FTS5 table is fine.
|
|
912
|
+
if self._is_trigram_unavailable_error(exc):
|
|
913
|
+
self._warn_trigram_unavailable(exc)
|
|
914
|
+
else:
|
|
915
|
+
self._warn_fts5_unavailable(exc)
|
|
546
916
|
return False
|
|
547
917
|
|
|
548
918
|
def _execute_write(self, fn: Callable[[sqlite3.Connection], T]) -> T:
|
|
@@ -598,17 +968,27 @@ class SessionDB:
|
|
|
598
968
|
)
|
|
599
969
|
|
|
600
970
|
def _try_wal_checkpoint(self) -> None:
|
|
601
|
-
"""Best-effort
|
|
971
|
+
"""Best-effort TRUNCATE WAL checkpoint. Never raises.
|
|
602
972
|
|
|
603
|
-
Flushes committed WAL frames back into the main DB file
|
|
604
|
-
|
|
605
|
-
|
|
973
|
+
Flushes committed WAL frames back into the main DB file and
|
|
974
|
+
truncates the WAL file to zero bytes. Keeps the WAL from
|
|
975
|
+
growing unbounded when many processes hold persistent
|
|
606
976
|
connections.
|
|
977
|
+
|
|
978
|
+
PASSIVE checkpoint was previously used here, but it never
|
|
979
|
+
truncates the WAL file — the file stays at its high-water
|
|
980
|
+
mark until an explicit TRUNCATE is called (which only
|
|
981
|
+
happened inside the infrequent vacuum()).
|
|
982
|
+
|
|
983
|
+
TRUNCATE may block writers briefly while checkpointing, but
|
|
984
|
+
_try_wal_checkpoint is called off the hot path (every 50
|
|
985
|
+
writes) and already runs under ``self._lock``, so the
|
|
986
|
+
additional hold time is negligible.
|
|
607
987
|
"""
|
|
608
988
|
try:
|
|
609
989
|
with self._lock:
|
|
610
990
|
result = self._conn.execute(
|
|
611
|
-
"PRAGMA wal_checkpoint(
|
|
991
|
+
"PRAGMA wal_checkpoint(TRUNCATE)"
|
|
612
992
|
).fetchone()
|
|
613
993
|
if result and result[1] > 0:
|
|
614
994
|
logger.debug(
|
|
@@ -621,13 +1001,13 @@ class SessionDB:
|
|
|
621
1001
|
def close(self):
|
|
622
1002
|
"""Close the database connection.
|
|
623
1003
|
|
|
624
|
-
Attempts a
|
|
625
|
-
help
|
|
1004
|
+
Attempts a TRUNCATE WAL checkpoint first so that exiting processes
|
|
1005
|
+
help shrink the WAL file.
|
|
626
1006
|
"""
|
|
627
1007
|
with self._lock:
|
|
628
1008
|
if self._conn:
|
|
629
1009
|
try:
|
|
630
|
-
self._conn.execute("PRAGMA wal_checkpoint(
|
|
1010
|
+
self._conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
|
631
1011
|
except Exception:
|
|
632
1012
|
pass
|
|
633
1013
|
self._conn.close()
|
|
@@ -787,11 +1167,16 @@ class SessionDB:
|
|
|
787
1167
|
# backfills, index changes tied to a specific version step) stay
|
|
788
1168
|
# in a version-gated chain. Column additions are handled by
|
|
789
1169
|
# _reconcile_columns() above and no longer need entries here.
|
|
790
|
-
if current_version < 10:
|
|
1170
|
+
if current_version < 10 and SCHEMA_VERSION == 10:
|
|
791
1171
|
# v10: trigram FTS5 table for CJK/substring search. The
|
|
792
1172
|
# virtual table + triggers are created unconditionally via
|
|
793
1173
|
# FTS_TRIGRAM_SQL below, but existing rows need a one-time
|
|
794
1174
|
# backfill into the FTS index.
|
|
1175
|
+
#
|
|
1176
|
+
# Only run this when v10 itself is the target schema. Current
|
|
1177
|
+
# v11+ code drops and rebuilds both FTS tables below, so doing
|
|
1178
|
+
# the v10-only trigram backfill first only burns startup time
|
|
1179
|
+
# and WAL space before v11 throws the work away.
|
|
795
1180
|
if fts5_available:
|
|
796
1181
|
_fts_trigram_exists = self._fts_table_probe(
|
|
797
1182
|
cursor, "messages_fts_trigram"
|
|
@@ -825,21 +1210,23 @@ class SessionDB:
|
|
|
825
1210
|
except sqlite3.OperationalError as exc:
|
|
826
1211
|
if not self._is_fts5_unavailable_error(exc):
|
|
827
1212
|
raise
|
|
828
|
-
self.
|
|
829
|
-
|
|
830
|
-
|
|
1213
|
+
if self._is_trigram_unavailable_error(exc):
|
|
1214
|
+
self._warn_trigram_unavailable(exc)
|
|
1215
|
+
else:
|
|
1216
|
+
self._warn_fts5_unavailable(exc)
|
|
1217
|
+
fts5_available = False
|
|
1218
|
+
fts_migrations_complete = False
|
|
831
1219
|
break
|
|
832
1220
|
|
|
833
1221
|
if fts5_available:
|
|
834
1222
|
# Recreate virtual tables + triggers with the new inline-mode
|
|
835
1223
|
# schema that indexes content || tool_name || tool_calls.
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
# Backfill both indexes from every existing messages row.
|
|
1224
|
+
# Handle base and trigram independently — a missing
|
|
1225
|
+
# trigram tokenizer should not prevent base FTS backfill.
|
|
1226
|
+
base_fts_ok = self._ensure_fts_schema(
|
|
1227
|
+
cursor, "messages_fts", FTS_SQL
|
|
1228
|
+
)
|
|
1229
|
+
if base_fts_ok:
|
|
843
1230
|
cursor.execute(
|
|
844
1231
|
"INSERT INTO messages_fts(rowid, content) "
|
|
845
1232
|
"SELECT id, "
|
|
@@ -848,6 +1235,10 @@ class SessionDB:
|
|
|
848
1235
|
"COALESCE(tool_calls, '') "
|
|
849
1236
|
"FROM messages"
|
|
850
1237
|
)
|
|
1238
|
+
trigram_ok = self._ensure_fts_schema(
|
|
1239
|
+
cursor, "messages_fts_trigram", FTS_TRIGRAM_SQL
|
|
1240
|
+
)
|
|
1241
|
+
if trigram_ok:
|
|
851
1242
|
cursor.execute(
|
|
852
1243
|
"INSERT INTO messages_fts_trigram(rowid, content) "
|
|
853
1244
|
"SELECT id, "
|
|
@@ -856,8 +1247,12 @@ class SessionDB:
|
|
|
856
1247
|
"COALESCE(tool_calls, '') "
|
|
857
1248
|
"FROM messages"
|
|
858
1249
|
)
|
|
859
|
-
|
|
1250
|
+
if not base_fts_ok:
|
|
860
1251
|
fts_migrations_complete = False
|
|
1252
|
+
# Track trigram availability for CJK LIKE fallback.
|
|
1253
|
+
self._trigram_available = trigram_ok
|
|
1254
|
+
else:
|
|
1255
|
+
fts_migrations_complete = False
|
|
861
1256
|
else:
|
|
862
1257
|
fts_migrations_complete = False
|
|
863
1258
|
if current_version < 12:
|
|
@@ -872,6 +1267,32 @@ class SessionDB:
|
|
|
872
1267
|
)
|
|
873
1268
|
except sqlite3.OperationalError:
|
|
874
1269
|
pass
|
|
1270
|
+
if current_version < 16:
|
|
1271
|
+
# v16: tag delegate subagent rows so pickers stay clean after
|
|
1272
|
+
# parent deletes that used to orphan them (parent_session_id → NULL).
|
|
1273
|
+
try:
|
|
1274
|
+
cursor.execute(
|
|
1275
|
+
"UPDATE sessions SET model_config = json_set("
|
|
1276
|
+
"COALESCE(model_config, '{}'), '$._delegate_from', parent_session_id) "
|
|
1277
|
+
f"WHERE parent_session_id IS NOT NULL "
|
|
1278
|
+
"AND json_extract(COALESCE(model_config, '{}'), '$._delegate_from') IS NULL "
|
|
1279
|
+
f"AND {_ephemeral_child_sql('sessions')}"
|
|
1280
|
+
)
|
|
1281
|
+
cursor.execute(
|
|
1282
|
+
"UPDATE sessions SET model_config = json_set("
|
|
1283
|
+
"COALESCE(model_config, '{}'), '$._delegate_from', '__orphaned__') "
|
|
1284
|
+
"WHERE parent_session_id IS NULL "
|
|
1285
|
+
"AND json_extract(COALESCE(model_config, '{}'), '$._delegate_from') IS NULL "
|
|
1286
|
+
"AND json_extract(COALESCE(model_config, '{}'), '$._branched_from') IS NULL "
|
|
1287
|
+
"AND title IS NULL "
|
|
1288
|
+
"AND message_count <= 25 "
|
|
1289
|
+
"AND EXISTS (SELECT 1 FROM messages m "
|
|
1290
|
+
" WHERE m.session_id = sessions.id AND m.role = 'tool') "
|
|
1291
|
+
"AND NOT EXISTS (SELECT 1 FROM sessions ch "
|
|
1292
|
+
" WHERE ch.parent_session_id = sessions.id)"
|
|
1293
|
+
)
|
|
1294
|
+
except sqlite3.OperationalError:
|
|
1295
|
+
pass
|
|
875
1296
|
if current_version < SCHEMA_VERSION and fts_migrations_complete:
|
|
876
1297
|
cursor.execute(
|
|
877
1298
|
"UPDATE schema_version SET version = ?",
|
|
@@ -901,8 +1322,12 @@ class SessionDB:
|
|
|
901
1322
|
trigram_enabled = self._ensure_fts_schema(
|
|
902
1323
|
cursor, "messages_fts_trigram", FTS_TRIGRAM_SQL
|
|
903
1324
|
)
|
|
904
|
-
|
|
905
|
-
|
|
1325
|
+
self._trigram_available = trigram_enabled
|
|
1326
|
+
if triggers_need_repair:
|
|
1327
|
+
self._rebuild_fts_indexes(
|
|
1328
|
+
cursor,
|
|
1329
|
+
include_trigram=trigram_enabled,
|
|
1330
|
+
)
|
|
906
1331
|
|
|
907
1332
|
self._conn.commit()
|
|
908
1333
|
|
|
@@ -1107,6 +1532,24 @@ class SessionDB:
|
|
|
1107
1532
|
return None
|
|
1108
1533
|
return row["holder"] if isinstance(row, sqlite3.Row) else row[0]
|
|
1109
1534
|
|
|
1535
|
+
def update_session_meta(
|
|
1536
|
+
self,
|
|
1537
|
+
session_id: str,
|
|
1538
|
+
model_config_json: str,
|
|
1539
|
+
model: Optional[str] = None,
|
|
1540
|
+
) -> None:
|
|
1541
|
+
"""Update model_config and optionally model for an existing session.
|
|
1542
|
+
|
|
1543
|
+
Uses COALESCE so that passing model=None leaves the stored model
|
|
1544
|
+
column unchanged. Routes through _execute_write for the standard
|
|
1545
|
+
BEGIN IMMEDIATE + jitter-retry + lock guarantee.
|
|
1546
|
+
"""
|
|
1547
|
+
def _do(conn):
|
|
1548
|
+
conn.execute(
|
|
1549
|
+
"UPDATE sessions SET model_config = ?, model = COALESCE(?, model) WHERE id = ?",
|
|
1550
|
+
(model_config_json, model, session_id),
|
|
1551
|
+
)
|
|
1552
|
+
self._execute_write(_do)
|
|
1110
1553
|
|
|
1111
1554
|
def update_system_prompt(self, session_id: str, system_prompt: str) -> None:
|
|
1112
1555
|
"""Store the full assembled system prompt snapshot."""
|
|
@@ -1393,6 +1836,43 @@ class SessionDB:
|
|
|
1393
1836
|
|
|
1394
1837
|
return cleaned
|
|
1395
1838
|
|
|
1839
|
+
def _is_compression_ancestor(
|
|
1840
|
+
self, conn, *, ancestor_id: str, descendant_id: str
|
|
1841
|
+
) -> bool:
|
|
1842
|
+
"""Return True if *ancestor_id* is a compression predecessor of
|
|
1843
|
+
*descendant_id* (walking parent links up the continuation chain).
|
|
1844
|
+
|
|
1845
|
+
The continuation edge is the canonical one shared with
|
|
1846
|
+
:func:`_ephemeral_child_sql` / :meth:`set_session_archived`
|
|
1847
|
+
(``_COMPRESSION_CHILD_SQL``): a parent → child edge counts only when the
|
|
1848
|
+
parent ended with ``end_reason = 'compression'`` and the child started
|
|
1849
|
+
at or after the parent's ``ended_at``, which distinguishes continuations
|
|
1850
|
+
from delegate subagents / branch children that also carry a
|
|
1851
|
+
``parent_session_id``. Expressed as a single recursive CTE rather than a
|
|
1852
|
+
per-hop Python walk so the edge definition lives in exactly one place.
|
|
1853
|
+
"""
|
|
1854
|
+
if not ancestor_id or not descendant_id or ancestor_id == descendant_id:
|
|
1855
|
+
return False
|
|
1856
|
+
# Walk parent links up from the descendant, following only compression
|
|
1857
|
+
# continuation edges, and check whether ancestor_id is reached.
|
|
1858
|
+
edge = _COMPRESSION_CHILD_SQL.format(a="child")
|
|
1859
|
+
row = conn.execute(
|
|
1860
|
+
f"""
|
|
1861
|
+
WITH RECURSIVE ancestors(id) AS (
|
|
1862
|
+
SELECT ?
|
|
1863
|
+
UNION
|
|
1864
|
+
SELECT parent.id
|
|
1865
|
+
FROM ancestors a
|
|
1866
|
+
JOIN sessions child ON child.id = a.id
|
|
1867
|
+
JOIN sessions parent ON parent.id = child.parent_session_id
|
|
1868
|
+
WHERE {edge}
|
|
1869
|
+
)
|
|
1870
|
+
SELECT 1 FROM ancestors WHERE id = ? AND id != ? LIMIT 1
|
|
1871
|
+
""",
|
|
1872
|
+
(descendant_id, ancestor_id, descendant_id),
|
|
1873
|
+
).fetchone()
|
|
1874
|
+
return row is not None
|
|
1875
|
+
|
|
1396
1876
|
def set_session_title(self, session_id: str, title: str) -> bool:
|
|
1397
1877
|
"""Set or update a session's title.
|
|
1398
1878
|
|
|
@@ -1411,9 +1891,29 @@ class SessionDB:
|
|
|
1411
1891
|
)
|
|
1412
1892
|
conflict = cursor.fetchone()
|
|
1413
1893
|
if conflict:
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1894
|
+
conflict_id = conflict["id"]
|
|
1895
|
+
# A compression continuation is the live, projected-forward
|
|
1896
|
+
# head of its conversation; its compressed predecessors are
|
|
1897
|
+
# ended and hidden from the session list (list_sessions_rich
|
|
1898
|
+
# projects roots → tip). When the title that "conflicts" is
|
|
1899
|
+
# held by such a hidden ancestor, the user has no way to free
|
|
1900
|
+
# it — renaming the visible tip back to the base name would
|
|
1901
|
+
# dead-end with "already in use by <session they can't see>".
|
|
1902
|
+
# Treat this as a transfer: move the title off the ancestor
|
|
1903
|
+
# onto the continuation. Uniqueness is preserved (still only
|
|
1904
|
+
# one session carries the exact title) and the parent-link
|
|
1905
|
+
# lineage is untouched.
|
|
1906
|
+
if self._is_compression_ancestor(
|
|
1907
|
+
conn, ancestor_id=conflict_id, descendant_id=session_id
|
|
1908
|
+
):
|
|
1909
|
+
conn.execute(
|
|
1910
|
+
"UPDATE sessions SET title = NULL WHERE id = ?",
|
|
1911
|
+
(conflict_id,),
|
|
1912
|
+
)
|
|
1913
|
+
else:
|
|
1914
|
+
raise ValueError(
|
|
1915
|
+
f"Title '{title}' is already in use by session {conflict_id}"
|
|
1916
|
+
)
|
|
1417
1917
|
cursor = conn.execute(
|
|
1418
1918
|
"UPDATE sessions SET title = ? WHERE id = ?",
|
|
1419
1919
|
(title, session_id),
|
|
@@ -1435,15 +1935,51 @@ class SessionDB:
|
|
|
1435
1935
|
"""Archive or unarchive a session.
|
|
1436
1936
|
|
|
1437
1937
|
Archived sessions are hidden from the default session list but keep all
|
|
1438
|
-
their messages — this is a soft hide, not a delete.
|
|
1439
|
-
|
|
1938
|
+
their messages — this is a soft hide, not a delete. For compression
|
|
1939
|
+
chains, archive the whole logical conversation. Desktop lists compression
|
|
1940
|
+
roots projected forward to their latest continuation; updating only the
|
|
1941
|
+
displayed tip lets the still-unarchived root resurrect it on refresh.
|
|
1942
|
+
Returns True when at least one row was updated.
|
|
1440
1943
|
"""
|
|
1441
1944
|
def _do(conn):
|
|
1442
1945
|
cursor = conn.execute(
|
|
1443
|
-
"
|
|
1444
|
-
|
|
1946
|
+
"""
|
|
1947
|
+
WITH RECURSIVE
|
|
1948
|
+
ancestors(id) AS (
|
|
1949
|
+
SELECT ?
|
|
1950
|
+
UNION
|
|
1951
|
+
SELECT parent.id
|
|
1952
|
+
FROM ancestors a
|
|
1953
|
+
JOIN sessions child ON child.id = a.id
|
|
1954
|
+
JOIN sessions parent ON parent.id = child.parent_session_id
|
|
1955
|
+
WHERE parent.end_reason = 'compression'
|
|
1956
|
+
AND child.started_at >= parent.ended_at
|
|
1957
|
+
),
|
|
1958
|
+
descendants(id) AS (
|
|
1959
|
+
SELECT ?
|
|
1960
|
+
UNION
|
|
1961
|
+
SELECT child.id
|
|
1962
|
+
FROM descendants d
|
|
1963
|
+
JOIN sessions parent ON parent.id = d.id
|
|
1964
|
+
JOIN sessions child ON child.parent_session_id = parent.id
|
|
1965
|
+
WHERE parent.end_reason = 'compression'
|
|
1966
|
+
AND child.started_at >= parent.ended_at
|
|
1967
|
+
),
|
|
1968
|
+
lineage(id) AS (
|
|
1969
|
+
SELECT id FROM ancestors
|
|
1970
|
+
UNION
|
|
1971
|
+
SELECT id FROM descendants
|
|
1972
|
+
)
|
|
1973
|
+
UPDATE sessions
|
|
1974
|
+
SET archived = ?
|
|
1975
|
+
WHERE id IN (SELECT id FROM lineage)
|
|
1976
|
+
""",
|
|
1977
|
+
(session_id, session_id, 1 if archived else 0),
|
|
1445
1978
|
)
|
|
1446
|
-
|
|
1979
|
+
rowcount = cursor.rowcount
|
|
1980
|
+
if rowcount is None or rowcount < 0:
|
|
1981
|
+
rowcount = conn.execute("SELECT changes()").fetchone()[0]
|
|
1982
|
+
return rowcount
|
|
1447
1983
|
rowcount = self._execute_write(_do)
|
|
1448
1984
|
return rowcount > 0
|
|
1449
1985
|
|
|
@@ -1568,6 +2104,7 @@ class SessionDB:
|
|
|
1568
2104
|
order_by_last_active: bool = False,
|
|
1569
2105
|
include_archived: bool = False,
|
|
1570
2106
|
archived_only: bool = False,
|
|
2107
|
+
id_query: str = None,
|
|
1571
2108
|
) -> List[Dict[str, Any]]:
|
|
1572
2109
|
"""List sessions with preview (first user message) and last active timestamp.
|
|
1573
2110
|
|
|
@@ -1600,18 +2137,22 @@ class SessionDB:
|
|
|
1600
2137
|
params = []
|
|
1601
2138
|
|
|
1602
2139
|
if not include_children:
|
|
1603
|
-
# Show root sessions and branch sessions
|
|
1604
|
-
#
|
|
1605
|
-
#
|
|
1606
|
-
#
|
|
1607
|
-
#
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
2140
|
+
# Show root sessions and branch sessions, while still hiding
|
|
2141
|
+
# sub-agent runs and compression continuations (which also carry a
|
|
2142
|
+
# parent_session_id but were spawned while the parent was still
|
|
2143
|
+
# live — i.e., started_at < parent.ended_at).
|
|
2144
|
+
#
|
|
2145
|
+
# Branch sessions are identified two ways, OR'd for robustness:
|
|
2146
|
+
# 1. A stable ``_branched_from`` marker in model_config, written
|
|
2147
|
+
# by /branch at creation time. This survives the parent being
|
|
2148
|
+
# reopened and re-ended with a different end_reason (e.g.
|
|
2149
|
+
# tui_shutdown overwriting 'branched'), which otherwise hides
|
|
2150
|
+
# the branch — see issue #20856.
|
|
2151
|
+
# 2. The legacy heuristic (parent ended with 'branched' before the
|
|
2152
|
+
# child started), covering branch sessions created before the
|
|
2153
|
+
# marker existed.
|
|
2154
|
+
where_clauses.append(_LISTABLE_CHILD_SQL)
|
|
2155
|
+
where_clauses.append(f"{_delegate_from_json('s.model_config')} IS NULL")
|
|
1615
2156
|
|
|
1616
2157
|
if source:
|
|
1617
2158
|
where_clauses.append("s.source = ?")
|
|
@@ -1629,6 +2170,16 @@ class SessionDB:
|
|
|
1629
2170
|
where_clauses.append("s.archived = 0")
|
|
1630
2171
|
|
|
1631
2172
|
where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
|
|
2173
|
+
|
|
2174
|
+
# Optional session-id filter, pushed into SQL so callers (Desktop
|
|
2175
|
+
# session-id search) don't have to fetch every row and filter in
|
|
2176
|
+
# Python. ``id_query`` is matched as a case-insensitive substring
|
|
2177
|
+
# against each surfaced row's id AND every id in its forward
|
|
2178
|
+
# compression chain — so searching a compression *root* id or a *tip*
|
|
2179
|
+
# id both resolve to the same projected conversation. Only used in the
|
|
2180
|
+
# order_by_last_active path (which builds the chain CTE); other callers
|
|
2181
|
+
# pass id_query=None.
|
|
2182
|
+
id_needle = (id_query or "").strip().lower()
|
|
1632
2183
|
if order_by_last_active:
|
|
1633
2184
|
# Compute effective_last_active by walking each surfaced session's
|
|
1634
2185
|
# compression-continuation chain forward in SQL and taking the MAX
|
|
@@ -1641,6 +2192,28 @@ class SessionDB:
|
|
|
1641
2192
|
# compression-continuation edges using the same criteria as
|
|
1642
2193
|
# get_compression_tip (parent.end_reason='compression' AND
|
|
1643
2194
|
# child.started_at >= parent.ended_at).
|
|
2195
|
+
outer_where = where_sql
|
|
2196
|
+
id_params: List[Any] = []
|
|
2197
|
+
if id_needle:
|
|
2198
|
+
# Admit a surfaced row if its own id or any id in its forward
|
|
2199
|
+
# compression chain matches the needle. LIKE with a leading
|
|
2200
|
+
# wildcard can't use an index, but the chain membership and
|
|
2201
|
+
# the small result set keep this bounded — far cheaper than
|
|
2202
|
+
# fetching every session and scanning in Python.
|
|
2203
|
+
id_clause = (
|
|
2204
|
+
"EXISTS (SELECT 1 FROM chain cq"
|
|
2205
|
+
" WHERE cq.root_id = s.id"
|
|
2206
|
+
" AND LOWER(cq.cur_id) LIKE ? ESCAPE '\\')"
|
|
2207
|
+
)
|
|
2208
|
+
like_pattern = (
|
|
2209
|
+
"%"
|
|
2210
|
+
+ id_needle.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
|
2211
|
+
+ "%"
|
|
2212
|
+
)
|
|
2213
|
+
id_params = [like_pattern]
|
|
2214
|
+
outer_where = (
|
|
2215
|
+
f"{where_sql} AND {id_clause}" if where_sql else f"WHERE {id_clause}"
|
|
2216
|
+
)
|
|
1644
2217
|
query = f"""
|
|
1645
2218
|
WITH RECURSIVE chain(root_id, cur_id) AS (
|
|
1646
2219
|
SELECT s.id, s.id FROM sessions s {where_sql}
|
|
@@ -1677,12 +2250,13 @@ class SessionDB:
|
|
|
1677
2250
|
COALESCE(cm.effective_last_active, s.started_at) AS _effective_last_active
|
|
1678
2251
|
FROM sessions s
|
|
1679
2252
|
LEFT JOIN chain_max cm ON cm.root_id = s.id
|
|
1680
|
-
{
|
|
2253
|
+
{outer_where}
|
|
1681
2254
|
ORDER BY _effective_last_active DESC, s.started_at DESC, s.id DESC
|
|
1682
2255
|
LIMIT ? OFFSET ?
|
|
1683
2256
|
"""
|
|
1684
|
-
# WHERE params apply twice (CTE seed + outer select)
|
|
1685
|
-
|
|
2257
|
+
# WHERE params apply twice (CTE seed + outer select); the id filter
|
|
2258
|
+
# only applies to the outer select.
|
|
2259
|
+
params = params + params + id_params + [limit, offset]
|
|
1686
2260
|
else:
|
|
1687
2261
|
query = f"""
|
|
1688
2262
|
SELECT s.*,
|
|
@@ -1756,6 +2330,72 @@ class SessionDB:
|
|
|
1756
2330
|
|
|
1757
2331
|
return sessions
|
|
1758
2332
|
|
|
2333
|
+
def list_cron_job_runs(
|
|
2334
|
+
self,
|
|
2335
|
+
job_id: str,
|
|
2336
|
+
limit: int = 20,
|
|
2337
|
+
offset: int = 0,
|
|
2338
|
+
) -> List[Dict[str, Any]]:
|
|
2339
|
+
"""List the run sessions produced by a single cron job, newest first.
|
|
2340
|
+
|
|
2341
|
+
Cron runs are flat, independent sessions whose id is
|
|
2342
|
+
``cron_{job_id}_{timestamp}`` (see ``cron/scheduler.run_job``). They are
|
|
2343
|
+
never compression roots and never branch, so this deliberately skips the
|
|
2344
|
+
``list_sessions_rich`` recursive compression-chain CTE / leading-wildcard
|
|
2345
|
+
``id_query`` path — that path seeds from *every* ``source='cron'`` row in
|
|
2346
|
+
the DB and only filters to one job's runs after the scan, so it scales
|
|
2347
|
+
with the whole cron pile (a heavy history makes the desktop run-history
|
|
2348
|
+
endpoint time out before it eventually populates).
|
|
2349
|
+
|
|
2350
|
+
Instead this binds to one job with a ``[prefix, prefix_hi)`` range over
|
|
2351
|
+
the id (an index range scan, not a ``%...%`` substring), filters
|
|
2352
|
+
``source='cron'``, and orders by ``started_at DESC``. Work scales with
|
|
2353
|
+
the requested window, not the total cron history.
|
|
2354
|
+
|
|
2355
|
+
Returns the same enriched row shape as ``list_sessions_rich`` (adds
|
|
2356
|
+
``preview`` + ``last_active``) so callers can reuse it.
|
|
2357
|
+
"""
|
|
2358
|
+
prefix = f"cron_{job_id}_"
|
|
2359
|
+
# Half-open upper bound for an index range scan: increment the final
|
|
2360
|
+
# byte of the prefix so the range covers exactly the ids that start
|
|
2361
|
+
# with ``prefix`` and nothing else. ``prefix`` always ends in '_', but
|
|
2362
|
+
# compute it generically rather than hardcoding the successor char.
|
|
2363
|
+
prefix_hi = prefix[:-1] + chr(ord(prefix[-1]) + 1)
|
|
2364
|
+
|
|
2365
|
+
query = """
|
|
2366
|
+
SELECT s.*,
|
|
2367
|
+
COALESCE(
|
|
2368
|
+
(SELECT SUBSTR(REPLACE(REPLACE(m.content, X'0A', ' '), X'0D', ' '), 1, 63)
|
|
2369
|
+
FROM messages m
|
|
2370
|
+
WHERE m.session_id = s.id AND m.role = 'user' AND m.content IS NOT NULL
|
|
2371
|
+
ORDER BY m.timestamp, m.id LIMIT 1),
|
|
2372
|
+
''
|
|
2373
|
+
) AS _preview_raw,
|
|
2374
|
+
COALESCE(
|
|
2375
|
+
(SELECT MAX(m2.timestamp) FROM messages m2 WHERE m2.session_id = s.id),
|
|
2376
|
+
s.started_at
|
|
2377
|
+
) AS last_active
|
|
2378
|
+
FROM sessions s
|
|
2379
|
+
WHERE s.source = 'cron' AND s.id >= ? AND s.id < ?
|
|
2380
|
+
ORDER BY s.started_at DESC, s.id DESC
|
|
2381
|
+
LIMIT ? OFFSET ?
|
|
2382
|
+
"""
|
|
2383
|
+
with self._lock:
|
|
2384
|
+
cursor = self._conn.execute(query, (prefix, prefix_hi, limit, offset))
|
|
2385
|
+
rows = cursor.fetchall()
|
|
2386
|
+
|
|
2387
|
+
runs: List[Dict[str, Any]] = []
|
|
2388
|
+
for row in rows:
|
|
2389
|
+
s = dict(row)
|
|
2390
|
+
raw = s.pop("_preview_raw", "").strip()
|
|
2391
|
+
if raw:
|
|
2392
|
+
text = raw[:60]
|
|
2393
|
+
s["preview"] = text + ("..." if len(raw) > 60 else "")
|
|
2394
|
+
else:
|
|
2395
|
+
s["preview"] = ""
|
|
2396
|
+
runs.append(s)
|
|
2397
|
+
return runs
|
|
2398
|
+
|
|
1759
2399
|
def _get_session_rich_row(self, session_id: str) -> Optional[Dict[str, Any]]:
|
|
1760
2400
|
"""Fetch a single session with the same enriched columns as
|
|
1761
2401
|
``list_sessions_rich`` (preview + last_active). Returns None if the
|
|
@@ -1854,6 +2494,7 @@ class SessionDB:
|
|
|
1854
2494
|
codex_message_items: Any = None,
|
|
1855
2495
|
platform_message_id: str = None,
|
|
1856
2496
|
observed: bool = False,
|
|
2497
|
+
timestamp: Any = None,
|
|
1857
2498
|
) -> int:
|
|
1858
2499
|
"""
|
|
1859
2500
|
Append a message to a session. Returns the message row ID.
|
|
@@ -1885,6 +2526,16 @@ class SessionDB:
|
|
|
1885
2526
|
# cannot bind list/dict parameters directly.
|
|
1886
2527
|
stored_content = self._encode_content(content)
|
|
1887
2528
|
|
|
2529
|
+
message_timestamp = time.time()
|
|
2530
|
+
if timestamp is not None:
|
|
2531
|
+
try:
|
|
2532
|
+
if hasattr(timestamp, "timestamp"):
|
|
2533
|
+
message_timestamp = float(timestamp.timestamp())
|
|
2534
|
+
else:
|
|
2535
|
+
message_timestamp = float(timestamp)
|
|
2536
|
+
except (TypeError, ValueError):
|
|
2537
|
+
logger.debug("Ignoring invalid explicit message timestamp: %r", timestamp)
|
|
2538
|
+
|
|
1888
2539
|
# Pre-compute tool call count
|
|
1889
2540
|
num_tool_calls = 0
|
|
1890
2541
|
if tool_calls is not None:
|
|
@@ -1904,7 +2555,7 @@ class SessionDB:
|
|
|
1904
2555
|
tool_call_id,
|
|
1905
2556
|
tool_calls_json,
|
|
1906
2557
|
tool_name,
|
|
1907
|
-
|
|
2558
|
+
message_timestamp,
|
|
1908
2559
|
token_count,
|
|
1909
2560
|
finish_reason,
|
|
1910
2561
|
reasoning,
|
|
@@ -1957,6 +2608,16 @@ class SessionDB:
|
|
|
1957
2608
|
for msg in messages:
|
|
1958
2609
|
role = msg.get("role", "unknown")
|
|
1959
2610
|
tool_calls = msg.get("tool_calls")
|
|
2611
|
+
message_timestamp = now_ts
|
|
2612
|
+
if msg.get("timestamp") is not None:
|
|
2613
|
+
try:
|
|
2614
|
+
ts_value = msg.get("timestamp")
|
|
2615
|
+
if hasattr(ts_value, "timestamp"):
|
|
2616
|
+
message_timestamp = float(ts_value.timestamp())
|
|
2617
|
+
else:
|
|
2618
|
+
message_timestamp = float(ts_value)
|
|
2619
|
+
except (TypeError, ValueError):
|
|
2620
|
+
logger.debug("Ignoring invalid explicit message timestamp: %r", msg.get("timestamp"))
|
|
1960
2621
|
reasoning_details = msg.get("reasoning_details") if role == "assistant" else None
|
|
1961
2622
|
codex_reasoning_items = (
|
|
1962
2623
|
msg.get("codex_reasoning_items") if role == "assistant" else None
|
|
@@ -1994,7 +2655,7 @@ class SessionDB:
|
|
|
1994
2655
|
msg.get("tool_call_id"),
|
|
1995
2656
|
tool_calls_json,
|
|
1996
2657
|
msg.get("tool_name"),
|
|
1997
|
-
|
|
2658
|
+
message_timestamp,
|
|
1998
2659
|
msg.get("token_count"),
|
|
1999
2660
|
msg.get("finish_reason"),
|
|
2000
2661
|
msg.get("reasoning") if role == "assistant" else None,
|
|
@@ -2011,7 +2672,7 @@ class SessionDB:
|
|
|
2011
2672
|
total_tool_calls += (
|
|
2012
2673
|
len(tool_calls) if isinstance(tool_calls, list) else 1
|
|
2013
2674
|
)
|
|
2014
|
-
now_ts
|
|
2675
|
+
now_ts = max(now_ts + 1e-6, message_timestamp + 1e-6)
|
|
2015
2676
|
|
|
2016
2677
|
conn.execute(
|
|
2017
2678
|
"UPDATE sessions SET message_count = ?, tool_call_count = ? WHERE id = ?",
|
|
@@ -2274,6 +2935,24 @@ class SessionDB:
|
|
|
2274
2935
|
if not session_id:
|
|
2275
2936
|
return session_id
|
|
2276
2937
|
|
|
2938
|
+
# Follow the compression-continuation chain forward to the live tip
|
|
2939
|
+
# FIRST. Auto-compression ends the current session and forks a
|
|
2940
|
+
# continuation child, but a long-lived parent keeps its own flushed
|
|
2941
|
+
# message rows — so the empty-head walk below never redirects it, and
|
|
2942
|
+
# resuming the parent id reloads the pre-compression transcript while
|
|
2943
|
+
# the turns generated *after* compression (and their responses) sit in
|
|
2944
|
+
# the continuation. ``get_compression_tip`` is lineage-aware: it only
|
|
2945
|
+
# follows children whose parent ended with ``end_reason='compression'``
|
|
2946
|
+
# (created after the parent was ended), so delegation / branch children
|
|
2947
|
+
# never hijack the resume. This is the fix for the desktop "I came back
|
|
2948
|
+
# and the reply isn't there" report on large sessions.
|
|
2949
|
+
try:
|
|
2950
|
+
tip = self.get_compression_tip(session_id)
|
|
2951
|
+
except Exception:
|
|
2952
|
+
tip = session_id
|
|
2953
|
+
if tip and tip != session_id:
|
|
2954
|
+
session_id = tip
|
|
2955
|
+
|
|
2277
2956
|
with self._lock:
|
|
2278
2957
|
# If this session already has messages, nothing to redirect.
|
|
2279
2958
|
try:
|
|
@@ -2342,9 +3021,9 @@ class SessionDB:
|
|
|
2342
3021
|
rows = self._conn.execute(
|
|
2343
3022
|
"SELECT role, content, tool_call_id, tool_calls, tool_name, "
|
|
2344
3023
|
"finish_reason, reasoning, reasoning_content, reasoning_details, "
|
|
2345
|
-
"codex_reasoning_items, codex_message_items, platform_message_id, observed "
|
|
3024
|
+
"codex_reasoning_items, codex_message_items, platform_message_id, observed, timestamp "
|
|
2346
3025
|
f"FROM messages WHERE session_id IN ({placeholders})"
|
|
2347
|
-
f"{active_clause} ORDER BY id",
|
|
3026
|
+
f"{active_clause} ORDER BY timestamp, id",
|
|
2348
3027
|
tuple(session_ids),
|
|
2349
3028
|
).fetchall()
|
|
2350
3029
|
|
|
@@ -2354,6 +3033,8 @@ class SessionDB:
|
|
|
2354
3033
|
if row["role"] in {"user", "assistant"} and isinstance(content, str):
|
|
2355
3034
|
content = sanitize_context(content).strip()
|
|
2356
3035
|
msg = {"role": row["role"], "content": content}
|
|
3036
|
+
if row["timestamp"]:
|
|
3037
|
+
msg["timestamp"] = row["timestamp"]
|
|
2357
3038
|
if row["tool_call_id"]:
|
|
2358
3039
|
msg["tool_call_id"] = row["tool_call_id"]
|
|
2359
3040
|
if row["tool_name"]:
|
|
@@ -2620,9 +3301,10 @@ class SessionDB:
|
|
|
2620
3301
|
"""Sanitize user input for safe use in FTS5 MATCH queries.
|
|
2621
3302
|
|
|
2622
3303
|
FTS5 has its own query syntax where characters like ``"``, ``(``, ``)``,
|
|
2623
|
-
``+``, ``*``, ``{``, ``}
|
|
2624
|
-
``NOT``) have special meaning.
|
|
2625
|
-
MATCH can cause
|
|
3304
|
+
``+``, ``*``, ``{``, ``}``, the column-filter operator ``:`` and bare
|
|
3305
|
+
boolean operators (``AND``, ``OR``, ``NOT``) have special meaning.
|
|
3306
|
+
Passing raw user input directly to MATCH can cause
|
|
3307
|
+
``sqlite3.OperationalError``.
|
|
2626
3308
|
|
|
2627
3309
|
Strategy:
|
|
2628
3310
|
- Preserve properly paired quoted phrases (``"exact phrase"``)
|
|
@@ -2641,8 +3323,12 @@ class SessionDB:
|
|
|
2641
3323
|
|
|
2642
3324
|
sanitized = re.sub(r'"[^"]*"', _preserve_quoted, query)
|
|
2643
3325
|
|
|
2644
|
-
# Step 2: Strip remaining (unmatched) FTS5-special characters
|
|
2645
|
-
|
|
3326
|
+
# Step 2: Strip remaining (unmatched) FTS5-special characters. ``:`` is
|
|
3327
|
+
# FTS5's column-filter operator (``col:term``); since the FTS table has a
|
|
3328
|
+
# single ``content`` column, an unquoted colon query like ``TODO: fix``
|
|
3329
|
+
# parses as ``column:term`` and raises "no such column" — swallowed at
|
|
3330
|
+
# the execute site into zero results. Strip it like the others.
|
|
3331
|
+
sanitized = re.sub(r'[+{}():\"^]', " ", sanitized)
|
|
2646
3332
|
|
|
2647
3333
|
# Step 3: Collapse repeated * (e.g. "***") into a single one,
|
|
2648
3334
|
# and remove leading * (prefix-only needs at least one char before *)
|
|
@@ -2833,7 +3519,8 @@ class SessionDB:
|
|
|
2833
3519
|
self._count_cjk(t) < 3 for t in _tokens_for_check
|
|
2834
3520
|
)
|
|
2835
3521
|
|
|
2836
|
-
|
|
3522
|
+
_trigram_succeeded = False
|
|
3523
|
+
if cjk_count >= 3 and not _any_short_cjk and self._trigram_available:
|
|
2837
3524
|
# Trigram FTS5 path — quote each non-operator token to handle
|
|
2838
3525
|
# FTS5 special chars (%, *, etc.) while preserving boolean
|
|
2839
3526
|
# operators (AND, OR, NOT) for multi-term queries.
|
|
@@ -2882,11 +3569,13 @@ class SessionDB:
|
|
|
2882
3569
|
try:
|
|
2883
3570
|
tri_cursor = self._conn.execute(tri_sql, tri_params)
|
|
2884
3571
|
except sqlite3.OperationalError:
|
|
2885
|
-
|
|
3572
|
+
# Trigram query failed at runtime — fall through to LIKE.
|
|
3573
|
+
pass
|
|
2886
3574
|
else:
|
|
2887
3575
|
matches = [dict(row) for row in tri_cursor.fetchall()]
|
|
2888
|
-
|
|
2889
|
-
|
|
3576
|
+
_trigram_succeeded = True
|
|
3577
|
+
if not _trigram_succeeded:
|
|
3578
|
+
# Short / mixed CJK query, trigram unavailable, or trigram
|
|
2890
3579
|
# <3 CJK chars. Fall back to LIKE substring search.
|
|
2891
3580
|
# For multi-token OR queries (e.g. "广西 OR 桂林 OR 漓江"),
|
|
2892
3581
|
# build one LIKE condition per non-operator token so each term
|
|
@@ -3010,6 +3699,53 @@ class SessionDB:
|
|
|
3010
3699
|
|
|
3011
3700
|
return matches
|
|
3012
3701
|
|
|
3702
|
+
def search_sessions_by_id(
|
|
3703
|
+
self,
|
|
3704
|
+
query: str,
|
|
3705
|
+
limit: int = 20,
|
|
3706
|
+
include_archived: bool = True,
|
|
3707
|
+
) -> List[Dict[str, Any]]:
|
|
3708
|
+
"""Search surfaced sessions by exact/prefix/substring session id.
|
|
3709
|
+
|
|
3710
|
+
Desktop search uses this alongside FTS message search so users can paste
|
|
3711
|
+
a session id from logs, CLI output, or another Hermes surface and jump
|
|
3712
|
+
straight to that conversation. Matching also checks ``_lineage_root_id``
|
|
3713
|
+
for projected compression-chain tips, so an old root id still resolves to
|
|
3714
|
+
the live continuation row.
|
|
3715
|
+
"""
|
|
3716
|
+
needle = (query or "").strip().lower()
|
|
3717
|
+
if not needle or limit <= 0:
|
|
3718
|
+
return []
|
|
3719
|
+
|
|
3720
|
+
# SQL-bounded: list_sessions_rich pushes the id LIKE filter into the
|
|
3721
|
+
# query (matching the row's own id AND any id in its forward
|
|
3722
|
+
# compression chain), so we only materialize matching rows instead of
|
|
3723
|
+
# scanning every session. Fetch a small multiple of `limit` so the
|
|
3724
|
+
# in-Python exact/prefix/substring ranking below has enough candidates
|
|
3725
|
+
# to order, then truncate.
|
|
3726
|
+
candidates = self.list_sessions_rich(
|
|
3727
|
+
limit=max(limit * 4, limit),
|
|
3728
|
+
offset=0,
|
|
3729
|
+
include_archived=include_archived,
|
|
3730
|
+
order_by_last_active=True,
|
|
3731
|
+
id_query=needle,
|
|
3732
|
+
)
|
|
3733
|
+
|
|
3734
|
+
def score(row: Dict[str, Any]) -> int:
|
|
3735
|
+
ids = [str(row.get("id") or ""), str(row.get("_lineage_root_id") or "")]
|
|
3736
|
+
normalized = [value.lower() for value in ids if value]
|
|
3737
|
+
if any(value == needle for value in normalized):
|
|
3738
|
+
return 0
|
|
3739
|
+
if any(value.startswith(needle) for value in normalized):
|
|
3740
|
+
return 1
|
|
3741
|
+
return 2
|
|
3742
|
+
|
|
3743
|
+
ranked = sorted(
|
|
3744
|
+
enumerate(candidates),
|
|
3745
|
+
key=lambda item: (score(item[1]), item[0]),
|
|
3746
|
+
)
|
|
3747
|
+
return [row for _, row in ranked[:limit]]
|
|
3748
|
+
|
|
3013
3749
|
def search_sessions(
|
|
3014
3750
|
self,
|
|
3015
3751
|
source: str = None,
|
|
@@ -3056,26 +3792,51 @@ class SessionDB:
|
|
|
3056
3792
|
min_message_count: int = 0,
|
|
3057
3793
|
include_archived: bool = False,
|
|
3058
3794
|
archived_only: bool = False,
|
|
3795
|
+
exclude_children: bool = False,
|
|
3796
|
+
exclude_sources: List[str] = None,
|
|
3059
3797
|
) -> int:
|
|
3060
|
-
"""Count sessions, optionally filtered by source.
|
|
3798
|
+
"""Count sessions, optionally filtered by source.
|
|
3799
|
+
|
|
3800
|
+
Pass ``exclude_children=True`` to count only the conversations that
|
|
3801
|
+
``list_sessions_rich`` surfaces (root + branch sessions), hiding
|
|
3802
|
+
sub-agent runs and compression continuations. Use it whenever the count
|
|
3803
|
+
is paired with a ``list_sessions_rich`` page (e.g. sidebar "load more"
|
|
3804
|
+
totals) so the total matches the number of listable rows — otherwise the
|
|
3805
|
+
raw row count is inflated by children and "load more" never settles.
|
|
3806
|
+
|
|
3807
|
+
Pass ``exclude_sources`` to drop whole source classes from the count
|
|
3808
|
+
(e.g. ``["cron"]`` so the recents "load more" total matches a
|
|
3809
|
+
cron-excluded ``list_sessions_rich`` page and doesn't keep "load more"
|
|
3810
|
+
stuck on for buried scheduler sessions).
|
|
3811
|
+
"""
|
|
3061
3812
|
where_clauses = []
|
|
3062
3813
|
params = []
|
|
3063
3814
|
|
|
3815
|
+
if exclude_children:
|
|
3816
|
+
# Mirror list_sessions_rich's child-exclusion clause exactly so the
|
|
3817
|
+
# count lines up with the rows: roots (no parent) plus branch
|
|
3818
|
+
# children (parent ended with end_reason='branched').
|
|
3819
|
+
where_clauses.append(_LISTABLE_CHILD_SQL)
|
|
3820
|
+
where_clauses.append(f"{_delegate_from_json('s.model_config')} IS NULL")
|
|
3064
3821
|
if source:
|
|
3065
|
-
where_clauses.append("source = ?")
|
|
3822
|
+
where_clauses.append("s.source = ?")
|
|
3066
3823
|
params.append(source)
|
|
3824
|
+
if exclude_sources:
|
|
3825
|
+
placeholders = ",".join("?" for _ in exclude_sources)
|
|
3826
|
+
where_clauses.append(f"s.source NOT IN ({placeholders})")
|
|
3827
|
+
params.extend(exclude_sources)
|
|
3067
3828
|
if min_message_count > 0:
|
|
3068
|
-
where_clauses.append("message_count >= ?")
|
|
3829
|
+
where_clauses.append("s.message_count >= ?")
|
|
3069
3830
|
params.append(min_message_count)
|
|
3070
3831
|
if archived_only:
|
|
3071
|
-
where_clauses.append("archived = 1")
|
|
3832
|
+
where_clauses.append("s.archived = 1")
|
|
3072
3833
|
elif not include_archived:
|
|
3073
|
-
where_clauses.append("archived = 0")
|
|
3834
|
+
where_clauses.append("s.archived = 0")
|
|
3074
3835
|
|
|
3075
3836
|
where_sql = f" WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
|
|
3076
3837
|
|
|
3077
3838
|
with self._lock:
|
|
3078
|
-
cursor = self._conn.execute(f"SELECT COUNT(*) FROM sessions{where_sql}", params)
|
|
3839
|
+
cursor = self._conn.execute(f"SELECT COUNT(*) FROM sessions s{where_sql}", params)
|
|
3079
3840
|
return cursor.fetchone()[0]
|
|
3080
3841
|
|
|
3081
3842
|
def message_count(self, session_id: str = None) -> int:
|
|
@@ -3159,19 +3920,24 @@ class SessionDB:
|
|
|
3159
3920
|
) -> bool:
|
|
3160
3921
|
"""Delete a session and all its messages.
|
|
3161
3922
|
|
|
3162
|
-
|
|
3163
|
-
|
|
3923
|
+
Delegate subagent children (``model_config._delegate_from``) are
|
|
3924
|
+
cascade-deleted with the parent so they never resurface in session
|
|
3925
|
+
pickers as orphaned rows. Branch / compression children are orphaned
|
|
3926
|
+
(``parent_session_id → NULL``) so they remain accessible independently.
|
|
3164
3927
|
When *sessions_dir* is provided, also removes on-disk transcript
|
|
3165
|
-
files (``.json`` / ``.jsonl`` / ``request_dump_*``) for
|
|
3928
|
+
files (``.json`` / ``.jsonl`` / ``request_dump_*``) for every deleted
|
|
3166
3929
|
session. Returns True if the session was found and deleted.
|
|
3167
3930
|
"""
|
|
3931
|
+
removed_delegate_ids: List[str] = []
|
|
3932
|
+
|
|
3168
3933
|
def _do(conn):
|
|
3169
3934
|
cursor = conn.execute(
|
|
3170
3935
|
"SELECT COUNT(*) FROM sessions WHERE id = ?", (session_id,)
|
|
3171
3936
|
)
|
|
3172
3937
|
if cursor.fetchone()[0] == 0:
|
|
3173
3938
|
return False
|
|
3174
|
-
|
|
3939
|
+
removed_delegate_ids.extend(_delete_delegate_children(conn, [session_id]))
|
|
3940
|
+
# Orphan remaining child sessions (branches, etc.) so FK is satisfied.
|
|
3175
3941
|
conn.execute(
|
|
3176
3942
|
"UPDATE sessions SET parent_session_id = NULL "
|
|
3177
3943
|
"WHERE parent_session_id = ?",
|
|
@@ -3181,10 +3947,54 @@ class SessionDB:
|
|
|
3181
3947
|
conn.execute("DELETE FROM sessions WHERE id = ?", (session_id,))
|
|
3182
3948
|
return True
|
|
3183
3949
|
|
|
3950
|
+
deleted = self._execute_write(_do)
|
|
3951
|
+
if deleted:
|
|
3952
|
+
for delegate_id in removed_delegate_ids:
|
|
3953
|
+
self._remove_session_files(sessions_dir, delegate_id)
|
|
3954
|
+
self._remove_session_files(sessions_dir, session_id)
|
|
3955
|
+
return bool(deleted)
|
|
3956
|
+
|
|
3957
|
+
def delete_session_if_empty(
|
|
3958
|
+
self,
|
|
3959
|
+
session_id: str,
|
|
3960
|
+
sessions_dir: Optional[Path] = None,
|
|
3961
|
+
) -> bool:
|
|
3962
|
+
"""Delete *session_id* only when it never gained resumable content.
|
|
3963
|
+
|
|
3964
|
+
A session is considered empty when it has no messages and no
|
|
3965
|
+
user-assigned title. Used by CLI exit / session-rotation paths so
|
|
3966
|
+
immediately-started-and-quit sessions don't pile up in ``/resume``
|
|
3967
|
+
and ``hermes sessions list`` output. (Pattern ported from
|
|
3968
|
+
google-gemini/gemini-cli#27770.)
|
|
3969
|
+
|
|
3970
|
+
The emptiness check and delete run in one transaction, so a message
|
|
3971
|
+
flushed concurrently by another writer can't be lost. Sessions with
|
|
3972
|
+
children (delegate subagent runs) are preserved — a parent that
|
|
3973
|
+
spawned work is not "empty" even if its own transcript never
|
|
3974
|
+
flushed. Returns True if the session was deleted.
|
|
3975
|
+
"""
|
|
3976
|
+
def _do(conn):
|
|
3977
|
+
cursor = conn.execute(
|
|
3978
|
+
"""
|
|
3979
|
+
DELETE FROM sessions
|
|
3980
|
+
WHERE id = ?
|
|
3981
|
+
AND title IS NULL
|
|
3982
|
+
AND NOT EXISTS (
|
|
3983
|
+
SELECT 1 FROM messages WHERE messages.session_id = sessions.id
|
|
3984
|
+
)
|
|
3985
|
+
AND NOT EXISTS (
|
|
3986
|
+
SELECT 1 FROM sessions child
|
|
3987
|
+
WHERE child.parent_session_id = sessions.id
|
|
3988
|
+
)
|
|
3989
|
+
""",
|
|
3990
|
+
(session_id,),
|
|
3991
|
+
)
|
|
3992
|
+
return cursor.rowcount > 0
|
|
3993
|
+
|
|
3184
3994
|
deleted = self._execute_write(_do)
|
|
3185
3995
|
if deleted:
|
|
3186
3996
|
self._remove_session_files(sessions_dir, session_id)
|
|
3187
|
-
return deleted
|
|
3997
|
+
return bool(deleted)
|
|
3188
3998
|
|
|
3189
3999
|
def delete_sessions(
|
|
3190
4000
|
self,
|
|
@@ -3200,10 +4010,9 @@ class SessionDB:
|
|
|
3200
4010
|
* Unknown IDs are silently skipped (no 404) — selection state
|
|
3201
4011
|
in the UI can race against another tab's delete, and we'd
|
|
3202
4012
|
rather succeed-on-the-rest than fail-the-whole-batch.
|
|
3203
|
-
*
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
delete.
|
|
4013
|
+
* Delegate subagent children (``model_config._delegate_from``) are
|
|
4014
|
+
cascade-deleted with their parent; branch children are orphaned
|
|
4015
|
+
(``parent_session_id → NULL``) so they stay accessible.
|
|
3207
4016
|
* Messages and the session row both go in one
|
|
3208
4017
|
``_execute_write`` call so a partial failure can't leave the
|
|
3209
4018
|
DB in a "messages gone but session row still there" state.
|
|
@@ -3226,6 +4035,7 @@ class SessionDB:
|
|
|
3226
4035
|
return 0
|
|
3227
4036
|
|
|
3228
4037
|
removed_ids: list[str] = []
|
|
4038
|
+
removed_delegate_ids: list[str] = []
|
|
3229
4039
|
|
|
3230
4040
|
def _do(conn):
|
|
3231
4041
|
placeholders = ",".join("?" * len(unique_ids))
|
|
@@ -3240,7 +4050,8 @@ class SessionDB:
|
|
|
3240
4050
|
return 0
|
|
3241
4051
|
|
|
3242
4052
|
existing_placeholders = ",".join("?" * len(existing))
|
|
3243
|
-
|
|
4053
|
+
removed_delegate_ids.extend(_delete_delegate_children(conn, existing))
|
|
4054
|
+
# Orphan remaining children whose parent is in the kill list so the
|
|
3244
4055
|
# FK constraint stays satisfied. Pin children whose parent
|
|
3245
4056
|
# is itself in the kill list rather than NULL-ing parents
|
|
3246
4057
|
# of survivors — the IN list on ``parent_session_id`` does
|
|
@@ -3262,6 +4073,8 @@ class SessionDB:
|
|
|
3262
4073
|
return len(existing)
|
|
3263
4074
|
|
|
3264
4075
|
count = self._execute_write(_do)
|
|
4076
|
+
for sid in removed_delegate_ids:
|
|
4077
|
+
self._remove_session_files(sessions_dir, sid)
|
|
3265
4078
|
for sid in removed_ids:
|
|
3266
4079
|
self._remove_session_files(sessions_dir, sid)
|
|
3267
4080
|
return count
|