@clawpump/claw-agent 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +2177 -3162
- 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/cli_agent_setup_mixin.py +684 -0
- package/agent/hermes_cli/cli_commands_mixin.py +2293 -0
- package/agent/hermes_cli/commands.py +215 -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/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 +6190 -973
- 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 +4 -2
- package/agent/optional-mcps/clawpump-stdio/manifest.yaml +2 -0
- 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 +4 -1
- 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 +300 -684
- 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
|
@@ -0,0 +1,1146 @@
|
|
|
1
|
+
"""Kanban board watcher methods for GatewayRunner.
|
|
2
|
+
|
|
3
|
+
Extracted verbatim from ``gateway/run.py`` (god-file decomposition Phase 3).
|
|
4
|
+
These are the background-loop methods that subscribe to kanban boards, deliver
|
|
5
|
+
notifications/artifacts, and drive the multi-agent dispatcher. They use only
|
|
6
|
+
``self`` state, so they live on a mixin that ``GatewayRunner`` inherits — the
|
|
7
|
+
``self._kanban_*`` call sites resolve identically via the MRO, making this a
|
|
8
|
+
behavior-neutral move that lifts ~1,000 LOC out of run.py.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import sqlite3
|
|
17
|
+
import time
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Optional
|
|
20
|
+
|
|
21
|
+
# Match the logger run.py uses (logging.getLogger(__name__) where __name__ ==
|
|
22
|
+
# "gateway.run") so extracted log records keep their original logger name.
|
|
23
|
+
logger = logging.getLogger("gateway.run")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _acquire_singleton_lock(lock_path) -> "tuple[Optional[object], str]":
|
|
27
|
+
"""Take an exclusive, non-blocking advisory lock for the sole dispatcher.
|
|
28
|
+
|
|
29
|
+
Only one gateway process machine-wide may run the embedded kanban
|
|
30
|
+
dispatcher: concurrent dispatchers double the reclaim frequency (each
|
|
31
|
+
runs its own ``release_stale_claims`` → promote → dispatch loop), double
|
|
32
|
+
claim-attempt events in the event log, and — with ``wal_autocheckpoint=0`` —
|
|
33
|
+
concurrent manual WAL checkpoints can corrupt index pages. The
|
|
34
|
+
``dispatch_in_gateway`` config flag is the primary control; this lock is the
|
|
35
|
+
backstop that survives config drift and same-profile restart races.
|
|
36
|
+
|
|
37
|
+
Delegates to :func:`gateway.status._try_acquire_file_lock` (``fcntl`` on
|
|
38
|
+
POSIX, ``msvcrt`` on Windows) so the guard is cross-platform.
|
|
39
|
+
|
|
40
|
+
Returns ``(handle, "held")`` on success — the caller keeps the file handle
|
|
41
|
+
for the process lifetime and **must** release it via
|
|
42
|
+
:func:`_release_singleton_lock` when done. ``(None, "contended")`` when
|
|
43
|
+
another process holds the lock (caller must NOT dispatch). ``(None,
|
|
44
|
+
"unavailable")`` when locking cannot be performed (non-POSIX filesystem
|
|
45
|
+
without flock, or the status.py helpers are unimportable) — caller falls
|
|
46
|
+
back to config-only control.
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
from gateway.status import _try_acquire_file_lock # deferred; same package
|
|
50
|
+
except ImportError:
|
|
51
|
+
return None, "unavailable"
|
|
52
|
+
try:
|
|
53
|
+
Path(lock_path).parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
handle = open(str(lock_path), "a+", encoding="utf-8")
|
|
55
|
+
except OSError:
|
|
56
|
+
return None, "unavailable"
|
|
57
|
+
if not _try_acquire_file_lock(handle):
|
|
58
|
+
handle.close()
|
|
59
|
+
return None, "contended"
|
|
60
|
+
return handle, "held"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _release_singleton_lock(handle) -> None:
|
|
64
|
+
"""Release a dispatcher singleton lock acquired via :func:`_acquire_singleton_lock`."""
|
|
65
|
+
if handle is None:
|
|
66
|
+
return
|
|
67
|
+
try:
|
|
68
|
+
from gateway.status import _release_file_lock
|
|
69
|
+
_release_file_lock(handle)
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
try:
|
|
73
|
+
handle.close()
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class GatewayKanbanWatchersMixin:
|
|
79
|
+
"""Kanban watcher / notifier / dispatcher loops for GatewayRunner."""
|
|
80
|
+
|
|
81
|
+
async def _kanban_notifier_watcher(self, interval: float = 5.0) -> None:
|
|
82
|
+
"""Poll ``kanban_notify_subs`` and deliver terminal events to users.
|
|
83
|
+
|
|
84
|
+
For each subscription row, fetches ``task_events`` newer than the
|
|
85
|
+
stored cursor with kind in the terminal set (``completed``,
|
|
86
|
+
``blocked``, ``gave_up``, ``crashed``, ``timed_out``). Sends one
|
|
87
|
+
message per new event to ``(platform, chat_id, thread_id)``,
|
|
88
|
+
then advances the cursor. When a task reaches a terminal state
|
|
89
|
+
(``completed`` / ``archived``), the subscription is removed.
|
|
90
|
+
|
|
91
|
+
Runs in the gateway event loop; all SQLite work is pushed to a
|
|
92
|
+
thread via ``asyncio.to_thread`` so the loop never blocks on the
|
|
93
|
+
WAL lock. Failures in one tick don't stop subsequent ticks.
|
|
94
|
+
|
|
95
|
+
**Multi-board:** iterates every board discovered on disk per
|
|
96
|
+
tick. Subscriptions live inside each board's own DB and cannot
|
|
97
|
+
cross boards, so delivery semantics are unchanged — this is
|
|
98
|
+
purely a fan-out of the single-DB poll.
|
|
99
|
+
"""
|
|
100
|
+
# Gate: only the dispatch-owning gateway opens kanban DBs for notifier polling.
|
|
101
|
+
# Non-dispatch gateways have no subscriptions to deliver — all kanban state lives
|
|
102
|
+
# in the dispatch owner's per-board DBs. This prevents N-gateway -shm contention.
|
|
103
|
+
# TODO: gate per-board when per-board dispatcher_owner tracking lands.
|
|
104
|
+
try:
|
|
105
|
+
from hermes_cli.config import load_config as _load_config
|
|
106
|
+
except Exception:
|
|
107
|
+
logger.warning("kanban notifier: config loader unavailable; disabled")
|
|
108
|
+
return
|
|
109
|
+
env_override = os.environ.get("HERMES_KANBAN_DISPATCH_IN_GATEWAY", "").strip().lower()
|
|
110
|
+
if env_override in {"0", "false", "no", "off"}:
|
|
111
|
+
logger.info("kanban notifier: disabled via HERMES_KANBAN_DISPATCH_IN_GATEWAY env")
|
|
112
|
+
return
|
|
113
|
+
try:
|
|
114
|
+
cfg = _load_config()
|
|
115
|
+
except Exception as exc:
|
|
116
|
+
logger.warning("kanban notifier: cannot load config (%s); disabled", exc)
|
|
117
|
+
return
|
|
118
|
+
kanban_cfg = cfg.get("kanban", {}) if isinstance(cfg, dict) else {}
|
|
119
|
+
if not kanban_cfg.get("dispatch_in_gateway", True):
|
|
120
|
+
logger.info(
|
|
121
|
+
"kanban notifier: disabled via config kanban.dispatch_in_gateway=false"
|
|
122
|
+
)
|
|
123
|
+
return
|
|
124
|
+
from gateway.config import Platform as _Platform
|
|
125
|
+
try:
|
|
126
|
+
from hermes_cli import kanban_db as _kb
|
|
127
|
+
except Exception:
|
|
128
|
+
logger.warning("kanban notifier: kanban_db not importable; notifier disabled")
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
TERMINAL_KINDS = ("completed", "blocked", "gave_up", "crashed", "timed_out")
|
|
132
|
+
# Subscriptions are removed only when the task reaches a truly final
|
|
133
|
+
# status (done / archived). We used to also unsub on any terminal
|
|
134
|
+
# event kind (gave_up / crashed / timed_out / blocked), but that
|
|
135
|
+
# silently dropped the user out of the loop whenever the dispatcher
|
|
136
|
+
# respawned the task: a worker that crashes, gets reclaimed, runs
|
|
137
|
+
# again, and crashes a second time would only notify on the first
|
|
138
|
+
# crash because the subscription was deleted after the first event.
|
|
139
|
+
# Same shape as the reblock-after-unblock cycle that PR #22941
|
|
140
|
+
# fixed for `blocked`. Keeping the subscription alive until the
|
|
141
|
+
# task is genuinely done lets the cursor (advanced atomically by
|
|
142
|
+
# claim_unseen_events_for_sub) handle dedup, and any retry-loop
|
|
143
|
+
# event reaches the user.
|
|
144
|
+
# Per-subscription send-failure counter. Adapter.send raising
|
|
145
|
+
# means the chat is dead (deleted, bot kicked, etc.) — after N
|
|
146
|
+
# consecutive send failures the sub is dropped so we don't spin
|
|
147
|
+
# against a dead chat every 5 seconds forever.
|
|
148
|
+
MAX_SEND_FAILURES = 3
|
|
149
|
+
sub_fail_counts: dict[tuple, int] = getattr(
|
|
150
|
+
self, "_kanban_sub_fail_counts", {}
|
|
151
|
+
)
|
|
152
|
+
self._kanban_sub_fail_counts = sub_fail_counts
|
|
153
|
+
notifier_profile = getattr(self, "_kanban_notifier_profile", None)
|
|
154
|
+
if not notifier_profile:
|
|
155
|
+
notifier_profile = self._active_profile_name()
|
|
156
|
+
self._kanban_notifier_profile = notifier_profile
|
|
157
|
+
|
|
158
|
+
# Initial delay so the gateway can finish wiring adapters.
|
|
159
|
+
await asyncio.sleep(5)
|
|
160
|
+
|
|
161
|
+
while self._running:
|
|
162
|
+
try:
|
|
163
|
+
def _collect():
|
|
164
|
+
deliveries: list[dict] = []
|
|
165
|
+
active_platforms = {
|
|
166
|
+
getattr(platform, "value", str(platform)).lower()
|
|
167
|
+
for platform in self.adapters.keys()
|
|
168
|
+
}
|
|
169
|
+
if not active_platforms:
|
|
170
|
+
logger.debug("kanban notifier: no connected adapters; skipping tick")
|
|
171
|
+
return deliveries
|
|
172
|
+
|
|
173
|
+
# Enumerate every board on disk, but poll each resolved DB
|
|
174
|
+
# path once. Multiple slugs can point at the same DB when
|
|
175
|
+
# HERMES_KANBAN_DB pins the board path; without this guard
|
|
176
|
+
# one gateway could collect the same subscription/event
|
|
177
|
+
# more than once before advancing the cursor.
|
|
178
|
+
try:
|
|
179
|
+
boards = _kb.list_boards(include_archived=False)
|
|
180
|
+
except Exception:
|
|
181
|
+
boards = [_kb.read_board_metadata(_kb.DEFAULT_BOARD)]
|
|
182
|
+
seen_db_paths: set[str] = set()
|
|
183
|
+
for board_meta in boards:
|
|
184
|
+
slug = board_meta.get("slug") or _kb.DEFAULT_BOARD
|
|
185
|
+
db_path = board_meta.get("db_path")
|
|
186
|
+
try:
|
|
187
|
+
resolved_db_path = str(Path(db_path).expanduser().resolve()) if db_path else str(_kb.kanban_db_path(slug).resolve())
|
|
188
|
+
except Exception:
|
|
189
|
+
resolved_db_path = f"slug:{slug}"
|
|
190
|
+
if resolved_db_path in seen_db_paths:
|
|
191
|
+
logger.debug(
|
|
192
|
+
"kanban notifier: skipping duplicate board slug %s for DB %s",
|
|
193
|
+
slug, resolved_db_path,
|
|
194
|
+
)
|
|
195
|
+
continue
|
|
196
|
+
seen_db_paths.add(resolved_db_path)
|
|
197
|
+
try:
|
|
198
|
+
conn = _kb.connect(board=slug)
|
|
199
|
+
except Exception as exc:
|
|
200
|
+
logger.debug("kanban notifier: cannot open board %s: %s", slug, exc)
|
|
201
|
+
continue
|
|
202
|
+
try:
|
|
203
|
+
# `connect()` runs the schema + idempotent migration
|
|
204
|
+
# on first open per process, so an explicit
|
|
205
|
+
# `init_db()` here would be redundant. Worse:
|
|
206
|
+
# `init_db()` deliberately busts the per-process
|
|
207
|
+
# cache and re-runs the migration on a *second*
|
|
208
|
+
# connection, which races the first and used to
|
|
209
|
+
# log a benign but noisy `duplicate column name`
|
|
210
|
+
# traceback (and intermittent "database is locked"
|
|
211
|
+
# — issue #21378) on every gateway start against
|
|
212
|
+
# a legacy DB. `_add_column_if_missing` now
|
|
213
|
+
# tolerates that race, but we still skip the
|
|
214
|
+
# redundant call to avoid the wasted work.
|
|
215
|
+
subs = _kb.list_notify_subs(conn)
|
|
216
|
+
if not subs:
|
|
217
|
+
logger.debug("kanban notifier: board %s has no subscriptions", slug)
|
|
218
|
+
for sub in subs:
|
|
219
|
+
owner_profile = sub.get("notifier_profile") or None
|
|
220
|
+
if owner_profile and owner_profile != notifier_profile:
|
|
221
|
+
logger.debug(
|
|
222
|
+
"kanban notifier: subscription for %s owned by profile %s; current profile %s skipping",
|
|
223
|
+
sub.get("task_id"), owner_profile, notifier_profile,
|
|
224
|
+
)
|
|
225
|
+
continue
|
|
226
|
+
platform = (sub.get("platform") or "").lower()
|
|
227
|
+
if platform not in active_platforms:
|
|
228
|
+
logger.debug(
|
|
229
|
+
"kanban notifier: subscription for %s on %s skipped; adapter not connected",
|
|
230
|
+
sub.get("task_id"), platform or "<missing>",
|
|
231
|
+
)
|
|
232
|
+
continue
|
|
233
|
+
old_cursor, cursor, events = _kb.claim_unseen_events_for_sub(
|
|
234
|
+
conn,
|
|
235
|
+
task_id=sub["task_id"],
|
|
236
|
+
platform=sub["platform"],
|
|
237
|
+
chat_id=sub["chat_id"],
|
|
238
|
+
thread_id=sub.get("thread_id") or "",
|
|
239
|
+
kinds=TERMINAL_KINDS,
|
|
240
|
+
)
|
|
241
|
+
if not events:
|
|
242
|
+
continue
|
|
243
|
+
task = _kb.get_task(conn, sub["task_id"])
|
|
244
|
+
logger.debug(
|
|
245
|
+
"kanban notifier: claimed %d event(s) for %s on board %s cursor %s→%s",
|
|
246
|
+
len(events), sub["task_id"], slug, old_cursor, cursor,
|
|
247
|
+
)
|
|
248
|
+
deliveries.append({
|
|
249
|
+
"sub": sub,
|
|
250
|
+
"old_cursor": old_cursor,
|
|
251
|
+
"cursor": cursor,
|
|
252
|
+
"events": events,
|
|
253
|
+
"task": task,
|
|
254
|
+
"board": slug,
|
|
255
|
+
})
|
|
256
|
+
finally:
|
|
257
|
+
conn.close()
|
|
258
|
+
return deliveries
|
|
259
|
+
|
|
260
|
+
deliveries = await asyncio.to_thread(_collect)
|
|
261
|
+
for d in deliveries:
|
|
262
|
+
sub = d["sub"]
|
|
263
|
+
task = d["task"]
|
|
264
|
+
board_slug = d.get("board")
|
|
265
|
+
platform_str = (sub["platform"] or "").lower()
|
|
266
|
+
try:
|
|
267
|
+
plat = _Platform(platform_str)
|
|
268
|
+
except ValueError:
|
|
269
|
+
# Unknown platform string; skip and advance cursor so
|
|
270
|
+
# we don't replay forever.
|
|
271
|
+
await asyncio.to_thread(
|
|
272
|
+
self._kanban_advance, sub, d["cursor"], board_slug,
|
|
273
|
+
)
|
|
274
|
+
continue
|
|
275
|
+
adapter = self.adapters.get(plat)
|
|
276
|
+
if adapter is None:
|
|
277
|
+
logger.debug(
|
|
278
|
+
"kanban notifier: adapter %s disconnected before delivery for %s; rewinding claim",
|
|
279
|
+
platform_str, sub["task_id"],
|
|
280
|
+
)
|
|
281
|
+
await asyncio.to_thread(
|
|
282
|
+
self._kanban_rewind,
|
|
283
|
+
sub,
|
|
284
|
+
d["cursor"],
|
|
285
|
+
d.get("old_cursor", 0),
|
|
286
|
+
board_slug,
|
|
287
|
+
)
|
|
288
|
+
continue
|
|
289
|
+
title = (task.title if task else sub["task_id"])[:120]
|
|
290
|
+
for ev in d["events"]:
|
|
291
|
+
kind = ev.kind
|
|
292
|
+
# Identity prefix: attribute terminal pings to the
|
|
293
|
+
# worker that did the work. Makes fleets (where one
|
|
294
|
+
# chat subscribes to many tasks) legible at a glance.
|
|
295
|
+
who = (task.assignee if task and task.assignee else None)
|
|
296
|
+
tag = f"@{who} " if who else ""
|
|
297
|
+
if kind == "completed":
|
|
298
|
+
# Prefer the run's summary (the worker's
|
|
299
|
+
# intentional human-facing handoff, carried
|
|
300
|
+
# in the event payload), then fall back to
|
|
301
|
+
# task.result for legacy rows written before
|
|
302
|
+
# runs shipped.
|
|
303
|
+
handoff = ""
|
|
304
|
+
payload_summary = None
|
|
305
|
+
if ev.payload and ev.payload.get("summary"):
|
|
306
|
+
payload_summary = str(ev.payload["summary"])
|
|
307
|
+
if payload_summary:
|
|
308
|
+
lines = payload_summary.strip().splitlines()
|
|
309
|
+
h = lines[0][:200] if lines else payload_summary[:200]
|
|
310
|
+
handoff = f"\n{h}"
|
|
311
|
+
elif task and task.result:
|
|
312
|
+
lines = task.result.strip().splitlines()
|
|
313
|
+
r = lines[0][:160] if lines else task.result[:160]
|
|
314
|
+
handoff = f"\n{r}"
|
|
315
|
+
msg = (
|
|
316
|
+
f"✔ {tag}Kanban {sub['task_id']} done"
|
|
317
|
+
f" — {title}{handoff}"
|
|
318
|
+
)
|
|
319
|
+
elif kind == "blocked":
|
|
320
|
+
reason = ""
|
|
321
|
+
if ev.payload and ev.payload.get("reason"):
|
|
322
|
+
reason = f": {str(ev.payload['reason'])[:160]}"
|
|
323
|
+
msg = f"⏸ {tag}Kanban {sub['task_id']} blocked{reason}"
|
|
324
|
+
elif kind == "gave_up":
|
|
325
|
+
err = ""
|
|
326
|
+
if ev.payload and ev.payload.get("error"):
|
|
327
|
+
err = f"\n{str(ev.payload['error'])[:200]}"
|
|
328
|
+
msg = (
|
|
329
|
+
f"✖ {tag}Kanban {sub['task_id']} gave up "
|
|
330
|
+
f"after repeated spawn failures{err}"
|
|
331
|
+
)
|
|
332
|
+
elif kind == "crashed":
|
|
333
|
+
msg = (
|
|
334
|
+
f"✖ {tag}Kanban {sub['task_id']} worker crashed "
|
|
335
|
+
f"(pid gone); dispatcher will retry"
|
|
336
|
+
)
|
|
337
|
+
elif kind == "timed_out":
|
|
338
|
+
limit = 0
|
|
339
|
+
if ev.payload and ev.payload.get("limit_seconds"):
|
|
340
|
+
limit = int(ev.payload["limit_seconds"])
|
|
341
|
+
msg = (
|
|
342
|
+
f"⏱ {tag}Kanban {sub['task_id']} timed out "
|
|
343
|
+
f"(max_runtime={limit}s); will retry"
|
|
344
|
+
)
|
|
345
|
+
else:
|
|
346
|
+
continue
|
|
347
|
+
metadata: dict[str, Any] = {}
|
|
348
|
+
if sub.get("thread_id"):
|
|
349
|
+
metadata["thread_id"] = sub["thread_id"]
|
|
350
|
+
sub_key = (
|
|
351
|
+
sub["task_id"], sub["platform"],
|
|
352
|
+
sub["chat_id"], sub.get("thread_id") or "",
|
|
353
|
+
)
|
|
354
|
+
try:
|
|
355
|
+
await adapter.send(
|
|
356
|
+
sub["chat_id"], msg, metadata=metadata,
|
|
357
|
+
)
|
|
358
|
+
logger.debug(
|
|
359
|
+
"kanban notifier: delivered %s event for %s to %s/%s on board %s",
|
|
360
|
+
kind, sub["task_id"], platform_str, sub["chat_id"], board_slug,
|
|
361
|
+
)
|
|
362
|
+
# After delivering the text notification, surface
|
|
363
|
+
# any artifact paths the worker referenced in
|
|
364
|
+
# ``kanban_complete(summary=..., artifacts=[...])``
|
|
365
|
+
# (or the legacy ``result`` field) as native
|
|
366
|
+
# uploads. ``extract_local_files`` finds bare
|
|
367
|
+
# absolute paths in the summary;
|
|
368
|
+
# ``send_document`` / ``send_image_file`` uploads
|
|
369
|
+
# them. Only fires on the ``completed`` event so
|
|
370
|
+
# we never spam attachments on retries.
|
|
371
|
+
if kind == "completed":
|
|
372
|
+
try:
|
|
373
|
+
await self._deliver_kanban_artifacts(
|
|
374
|
+
adapter=adapter,
|
|
375
|
+
chat_id=sub["chat_id"],
|
|
376
|
+
metadata=metadata,
|
|
377
|
+
event_payload=getattr(ev, "payload", None),
|
|
378
|
+
task=task,
|
|
379
|
+
)
|
|
380
|
+
except Exception as art_exc:
|
|
381
|
+
logger.debug(
|
|
382
|
+
"kanban notifier: artifact delivery for %s failed: %s",
|
|
383
|
+
sub["task_id"], art_exc,
|
|
384
|
+
)
|
|
385
|
+
# Reset the failure counter on success.
|
|
386
|
+
sub_fail_counts.pop(sub_key, None)
|
|
387
|
+
except Exception as exc:
|
|
388
|
+
fails = sub_fail_counts.get(sub_key, 0) + 1
|
|
389
|
+
sub_fail_counts[sub_key] = fails
|
|
390
|
+
logger.warning(
|
|
391
|
+
"kanban notifier: send failed for %s on %s "
|
|
392
|
+
"(attempt %d/%d): %s",
|
|
393
|
+
sub["task_id"], platform_str, fails,
|
|
394
|
+
MAX_SEND_FAILURES, exc,
|
|
395
|
+
)
|
|
396
|
+
if fails >= MAX_SEND_FAILURES:
|
|
397
|
+
logger.warning(
|
|
398
|
+
"kanban notifier: dropping subscription "
|
|
399
|
+
"%s on %s after %d consecutive send failures",
|
|
400
|
+
sub["task_id"], platform_str, fails,
|
|
401
|
+
)
|
|
402
|
+
await asyncio.to_thread(self._kanban_unsub, sub, board_slug)
|
|
403
|
+
sub_fail_counts.pop(sub_key, None)
|
|
404
|
+
else:
|
|
405
|
+
await asyncio.to_thread(
|
|
406
|
+
self._kanban_rewind,
|
|
407
|
+
sub,
|
|
408
|
+
d["cursor"],
|
|
409
|
+
d.get("old_cursor", 0),
|
|
410
|
+
board_slug,
|
|
411
|
+
)
|
|
412
|
+
# Rewind the pre-send claim on transient failure so
|
|
413
|
+
# a later tick can retry. After too many failures,
|
|
414
|
+
# dropping the subscription is the terminal action.
|
|
415
|
+
break
|
|
416
|
+
else:
|
|
417
|
+
# All events delivered; advance cursor. The cursor
|
|
418
|
+
# is the dedup mechanism — it prevents re-delivery
|
|
419
|
+
# of the same event on subsequent ticks.
|
|
420
|
+
await asyncio.to_thread(
|
|
421
|
+
self._kanban_advance, sub, d["cursor"], board_slug,
|
|
422
|
+
)
|
|
423
|
+
# Unsubscribe only when the task has reached a truly
|
|
424
|
+
# final status (done / archived). For blocked /
|
|
425
|
+
# gave_up / crashed / timed_out the subscription is
|
|
426
|
+
# kept alive so the user gets notified again if the
|
|
427
|
+
# dispatcher respawns the task and it cycles into the
|
|
428
|
+
# same state. See the longer comment on TERMINAL_KINDS
|
|
429
|
+
# above for the failure mode this prevents.
|
|
430
|
+
task_terminal = task and task.status in {"done", "archived"}
|
|
431
|
+
if task_terminal:
|
|
432
|
+
await asyncio.to_thread(
|
|
433
|
+
self._kanban_unsub, sub, board_slug,
|
|
434
|
+
)
|
|
435
|
+
except Exception as exc:
|
|
436
|
+
logger.warning("kanban notifier tick failed: %s", exc)
|
|
437
|
+
# Sleep with cancellation checks.
|
|
438
|
+
for _ in range(int(max(1, interval))):
|
|
439
|
+
if not self._running:
|
|
440
|
+
return
|
|
441
|
+
await asyncio.sleep(1)
|
|
442
|
+
|
|
443
|
+
def _kanban_advance(
|
|
444
|
+
self, sub: dict, cursor: int, board: Optional[str] = None,
|
|
445
|
+
) -> None:
|
|
446
|
+
"""Sync helper: advance a subscription's cursor. Runs in to_thread.
|
|
447
|
+
|
|
448
|
+
``board`` scopes the DB connection to the board that owns this
|
|
449
|
+
subscription. Unsub cursors in one board can't touch another's.
|
|
450
|
+
"""
|
|
451
|
+
from hermes_cli import kanban_db as _kb
|
|
452
|
+
conn = _kb.connect(board=board)
|
|
453
|
+
try:
|
|
454
|
+
_kb.advance_notify_cursor(
|
|
455
|
+
conn,
|
|
456
|
+
task_id=sub["task_id"],
|
|
457
|
+
platform=sub["platform"],
|
|
458
|
+
chat_id=sub["chat_id"],
|
|
459
|
+
thread_id=sub.get("thread_id") or "",
|
|
460
|
+
new_cursor=cursor,
|
|
461
|
+
)
|
|
462
|
+
finally:
|
|
463
|
+
conn.close()
|
|
464
|
+
|
|
465
|
+
def _kanban_unsub(self, sub: dict, board: Optional[str] = None) -> None:
|
|
466
|
+
from hermes_cli import kanban_db as _kb
|
|
467
|
+
conn = _kb.connect(board=board)
|
|
468
|
+
try:
|
|
469
|
+
_kb.remove_notify_sub(
|
|
470
|
+
conn,
|
|
471
|
+
task_id=sub["task_id"],
|
|
472
|
+
platform=sub["platform"],
|
|
473
|
+
chat_id=sub["chat_id"],
|
|
474
|
+
thread_id=sub.get("thread_id") or "",
|
|
475
|
+
)
|
|
476
|
+
finally:
|
|
477
|
+
conn.close()
|
|
478
|
+
|
|
479
|
+
def _kanban_rewind(
|
|
480
|
+
self,
|
|
481
|
+
sub: dict,
|
|
482
|
+
claimed_cursor: int,
|
|
483
|
+
old_cursor: int,
|
|
484
|
+
board: Optional[str] = None,
|
|
485
|
+
) -> None:
|
|
486
|
+
"""Sync helper: undo a claimed notification cursor after send failure."""
|
|
487
|
+
from hermes_cli import kanban_db as _kb
|
|
488
|
+
conn = _kb.connect(board=board)
|
|
489
|
+
try:
|
|
490
|
+
_kb.rewind_notify_cursor(
|
|
491
|
+
conn,
|
|
492
|
+
task_id=sub["task_id"],
|
|
493
|
+
platform=sub["platform"],
|
|
494
|
+
chat_id=sub["chat_id"],
|
|
495
|
+
thread_id=sub.get("thread_id") or "",
|
|
496
|
+
claimed_cursor=claimed_cursor,
|
|
497
|
+
old_cursor=old_cursor,
|
|
498
|
+
)
|
|
499
|
+
finally:
|
|
500
|
+
conn.close()
|
|
501
|
+
|
|
502
|
+
async def _deliver_kanban_artifacts(
|
|
503
|
+
self,
|
|
504
|
+
*,
|
|
505
|
+
adapter,
|
|
506
|
+
chat_id: str,
|
|
507
|
+
metadata: dict,
|
|
508
|
+
event_payload: Optional[dict],
|
|
509
|
+
task,
|
|
510
|
+
) -> None:
|
|
511
|
+
"""Upload artifact files referenced by a completed kanban task.
|
|
512
|
+
|
|
513
|
+
Workers passing ``kanban_complete(artifacts=[...])`` ship absolute
|
|
514
|
+
file paths through the completion event so downstream humans get
|
|
515
|
+
the deliverable as a native upload instead of a path printed in
|
|
516
|
+
chat.
|
|
517
|
+
|
|
518
|
+
Sources scanned, in priority order:
|
|
519
|
+
1. ``event_payload['artifacts']`` (explicit list — preferred)
|
|
520
|
+
2. ``event_payload['summary']`` (truncated first line)
|
|
521
|
+
3. ``task.result`` (legacy fallback)
|
|
522
|
+
|
|
523
|
+
Files are deduplicated, missing files are silently skipped (the
|
|
524
|
+
path may have been mentioned for reference only), and delivery
|
|
525
|
+
errors are logged but do not break the notifier loop.
|
|
526
|
+
"""
|
|
527
|
+
from pathlib import Path as _Path
|
|
528
|
+
|
|
529
|
+
candidates: list[str] = []
|
|
530
|
+
seen: set[str] = set()
|
|
531
|
+
|
|
532
|
+
def _add(path: str) -> None:
|
|
533
|
+
if not path:
|
|
534
|
+
return
|
|
535
|
+
expanded = os.path.expanduser(path)
|
|
536
|
+
if expanded in seen:
|
|
537
|
+
return
|
|
538
|
+
if not os.path.isfile(expanded):
|
|
539
|
+
return
|
|
540
|
+
seen.add(expanded)
|
|
541
|
+
candidates.append(expanded)
|
|
542
|
+
|
|
543
|
+
# 1. Explicit artifacts list in payload.
|
|
544
|
+
if isinstance(event_payload, dict):
|
|
545
|
+
raw = event_payload.get("artifacts")
|
|
546
|
+
if isinstance(raw, (list, tuple)):
|
|
547
|
+
for item in raw:
|
|
548
|
+
if isinstance(item, str):
|
|
549
|
+
_add(item)
|
|
550
|
+
|
|
551
|
+
# 2. Paths embedded in the payload summary.
|
|
552
|
+
summary = event_payload.get("summary")
|
|
553
|
+
if isinstance(summary, str) and summary:
|
|
554
|
+
paths, _ = adapter.extract_local_files(summary)
|
|
555
|
+
for p in paths:
|
|
556
|
+
_add(p)
|
|
557
|
+
|
|
558
|
+
# 3. Legacy: paths embedded in task.result.
|
|
559
|
+
if task is not None and getattr(task, "result", None):
|
|
560
|
+
result_text = str(task.result)
|
|
561
|
+
paths, _ = adapter.extract_local_files(result_text)
|
|
562
|
+
for p in paths:
|
|
563
|
+
_add(p)
|
|
564
|
+
|
|
565
|
+
if not candidates:
|
|
566
|
+
return
|
|
567
|
+
|
|
568
|
+
from gateway.platforms.base import BasePlatformAdapter
|
|
569
|
+
candidates = BasePlatformAdapter.filter_local_delivery_paths(candidates)
|
|
570
|
+
if not candidates:
|
|
571
|
+
return
|
|
572
|
+
|
|
573
|
+
_IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".webp"}
|
|
574
|
+
_VIDEO_EXTS = {".mp4", ".mov", ".avi", ".mkv", ".webm", ".3gp"}
|
|
575
|
+
|
|
576
|
+
from urllib.parse import quote as _quote
|
|
577
|
+
|
|
578
|
+
# Partition images so they ride a single send_multiple_images call
|
|
579
|
+
# on platforms that support batch image uploads (Signal/Slack RPCs).
|
|
580
|
+
image_paths = [p for p in candidates if _Path(p).suffix.lower() in _IMAGE_EXTS]
|
|
581
|
+
other_paths = [p for p in candidates if _Path(p).suffix.lower() not in _IMAGE_EXTS]
|
|
582
|
+
|
|
583
|
+
if image_paths:
|
|
584
|
+
try:
|
|
585
|
+
batch = [(f"file://{_quote(p)}", "") for p in image_paths]
|
|
586
|
+
await adapter.send_multiple_images(
|
|
587
|
+
chat_id=chat_id, images=batch, metadata=metadata,
|
|
588
|
+
)
|
|
589
|
+
except Exception as exc:
|
|
590
|
+
logger.warning(
|
|
591
|
+
"kanban notifier: image batch upload failed: %s", exc,
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
for path in other_paths:
|
|
595
|
+
ext = _Path(path).suffix.lower()
|
|
596
|
+
try:
|
|
597
|
+
if ext in _VIDEO_EXTS:
|
|
598
|
+
await adapter.send_video(
|
|
599
|
+
chat_id=chat_id, video_path=path, metadata=metadata,
|
|
600
|
+
)
|
|
601
|
+
else:
|
|
602
|
+
await adapter.send_document(
|
|
603
|
+
chat_id=chat_id, file_path=path, metadata=metadata,
|
|
604
|
+
)
|
|
605
|
+
except Exception as exc:
|
|
606
|
+
logger.warning(
|
|
607
|
+
"kanban notifier: artifact upload (%s) failed: %s",
|
|
608
|
+
path, exc,
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
async def _kanban_dispatcher_watcher(self) -> None:
|
|
612
|
+
"""Embedded kanban dispatcher — one tick every `dispatch_interval_seconds`.
|
|
613
|
+
|
|
614
|
+
Gated by `kanban.dispatch_in_gateway` in config.yaml (default True).
|
|
615
|
+
When true, the gateway hosts the single dispatcher for this profile:
|
|
616
|
+
no separate `hermes kanban daemon` process needed. When false, the
|
|
617
|
+
loop exits immediately and an external daemon is expected.
|
|
618
|
+
|
|
619
|
+
Each tick calls :func:`kanban_db.dispatch_once` inside
|
|
620
|
+
``asyncio.to_thread`` so the SQLite WAL lock never blocks the
|
|
621
|
+
event loop. Failures in one tick don't stop subsequent ticks —
|
|
622
|
+
same pattern as `_kanban_notifier_watcher`.
|
|
623
|
+
|
|
624
|
+
Shutdown: the loop checks ``self._running`` between ticks; gateway
|
|
625
|
+
stop() flips it to False and cancels pending tasks, and the
|
|
626
|
+
in-flight ``to_thread`` returns on its own after the current
|
|
627
|
+
``dispatch_once`` call finishes (typically <1ms on an idle board).
|
|
628
|
+
"""
|
|
629
|
+
# Read config once at boot. If the user flips the flag later, they
|
|
630
|
+
# restart the gateway; same pattern as every other background
|
|
631
|
+
# watcher here. Honours HERMES_KANBAN_DISPATCH_IN_GATEWAY env var
|
|
632
|
+
# as an escape hatch (false-y value disables without editing YAML).
|
|
633
|
+
try:
|
|
634
|
+
from hermes_cli.config import load_config as _load_config
|
|
635
|
+
except Exception:
|
|
636
|
+
logger.warning("kanban dispatcher: config loader unavailable; disabled")
|
|
637
|
+
return
|
|
638
|
+
env_override = os.environ.get("HERMES_KANBAN_DISPATCH_IN_GATEWAY", "").strip().lower()
|
|
639
|
+
if env_override in {"0", "false", "no", "off"}:
|
|
640
|
+
logger.info("kanban dispatcher: disabled via HERMES_KANBAN_DISPATCH_IN_GATEWAY env")
|
|
641
|
+
return
|
|
642
|
+
|
|
643
|
+
try:
|
|
644
|
+
cfg = _load_config()
|
|
645
|
+
except Exception as exc:
|
|
646
|
+
logger.warning("kanban dispatcher: cannot load config (%s); disabled", exc)
|
|
647
|
+
return
|
|
648
|
+
kanban_cfg = cfg.get("kanban", {}) if isinstance(cfg, dict) else {}
|
|
649
|
+
if not kanban_cfg.get("dispatch_in_gateway", True):
|
|
650
|
+
logger.info(
|
|
651
|
+
"kanban dispatcher: disabled via config kanban.dispatch_in_gateway=false"
|
|
652
|
+
)
|
|
653
|
+
return
|
|
654
|
+
|
|
655
|
+
try:
|
|
656
|
+
from hermes_cli import kanban_db as _kb
|
|
657
|
+
except Exception:
|
|
658
|
+
logger.warning("kanban dispatcher: kanban_db not importable; dispatcher disabled")
|
|
659
|
+
return
|
|
660
|
+
|
|
661
|
+
# Single-dispatcher backstop. dispatch_in_gateway defaults to true, so a
|
|
662
|
+
# new profile gateway (or a same-profile restart race) can silently
|
|
663
|
+
# start a second dispatcher; concurrent dispatchers double reclaim
|
|
664
|
+
# frequency, double claim-attempt events, and — with
|
|
665
|
+
# wal_autocheckpoint=0 — concurrent manual WAL checkpoints can corrupt
|
|
666
|
+
# index pages. The lock lives at the machine-global kanban root
|
|
667
|
+
# (shared across profiles by design), so it serialises ALL gateways.
|
|
668
|
+
self._kanban_dispatcher_lock_handle = None
|
|
669
|
+
_lock_path = _kb.kanban_home() / "kanban" / ".dispatcher.lock"
|
|
670
|
+
_lock_handle, _lock_state = _acquire_singleton_lock(_lock_path)
|
|
671
|
+
if _lock_state == "contended":
|
|
672
|
+
logger.info(
|
|
673
|
+
"kanban dispatcher: another gateway already holds the dispatcher "
|
|
674
|
+
"lock (%s); this gateway will NOT dispatch.", _lock_path,
|
|
675
|
+
)
|
|
676
|
+
return
|
|
677
|
+
if _lock_state == "held":
|
|
678
|
+
self._kanban_dispatcher_lock_handle = _lock_handle # hold for process lifetime
|
|
679
|
+
logger.info("kanban dispatcher: holding singleton dispatcher lock (%s)", _lock_path)
|
|
680
|
+
else:
|
|
681
|
+
logger.warning(
|
|
682
|
+
"kanban dispatcher: advisory lock unavailable at %s; proceeding "
|
|
683
|
+
"on config control alone.", _lock_path,
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
try:
|
|
687
|
+
interval = float(kanban_cfg.get("dispatch_interval_seconds", 60) or 60)
|
|
688
|
+
except (ValueError, TypeError):
|
|
689
|
+
logger.warning(
|
|
690
|
+
"kanban dispatcher: invalid dispatch_interval_seconds=%r, using default 60",
|
|
691
|
+
kanban_cfg.get("dispatch_interval_seconds"),
|
|
692
|
+
)
|
|
693
|
+
interval = 60.0
|
|
694
|
+
interval = max(interval, 1.0) # sanity floor — tighter than this is a footgun
|
|
695
|
+
|
|
696
|
+
# Read max_spawn config to limit concurrent kanban tasks
|
|
697
|
+
max_spawn = kanban_cfg.get("max_spawn", None)
|
|
698
|
+
if max_spawn is not None:
|
|
699
|
+
logger.info(f"kanban dispatcher: max_spawn={max_spawn}")
|
|
700
|
+
|
|
701
|
+
# Cap the number of simultaneously running tasks so slow workers
|
|
702
|
+
# (local LLMs, resource-constrained hosts) don't pile up and time
|
|
703
|
+
# out. When set, the dispatcher skips spawning when the board
|
|
704
|
+
# already has this many tasks in 'running' status.
|
|
705
|
+
raw_max_in_progress = kanban_cfg.get("max_in_progress", None)
|
|
706
|
+
max_in_progress = None
|
|
707
|
+
if raw_max_in_progress is not None:
|
|
708
|
+
try:
|
|
709
|
+
max_in_progress = int(raw_max_in_progress)
|
|
710
|
+
except (TypeError, ValueError):
|
|
711
|
+
logger.warning(
|
|
712
|
+
"kanban dispatcher: invalid kanban.max_in_progress=%r; ignoring",
|
|
713
|
+
raw_max_in_progress,
|
|
714
|
+
)
|
|
715
|
+
max_in_progress = None
|
|
716
|
+
else:
|
|
717
|
+
if max_in_progress < 1:
|
|
718
|
+
logger.warning(
|
|
719
|
+
"kanban dispatcher: kanban.max_in_progress=%r is below 1; ignoring",
|
|
720
|
+
raw_max_in_progress,
|
|
721
|
+
)
|
|
722
|
+
max_in_progress = None
|
|
723
|
+
else:
|
|
724
|
+
logger.info(f"kanban dispatcher: max_in_progress={max_in_progress}")
|
|
725
|
+
|
|
726
|
+
raw_failure_limit = kanban_cfg.get("failure_limit", _kb.DEFAULT_FAILURE_LIMIT)
|
|
727
|
+
try:
|
|
728
|
+
failure_limit = int(raw_failure_limit)
|
|
729
|
+
except (TypeError, ValueError):
|
|
730
|
+
logger.warning(
|
|
731
|
+
"kanban dispatcher: invalid kanban.failure_limit=%r; using default %d",
|
|
732
|
+
raw_failure_limit,
|
|
733
|
+
_kb.DEFAULT_FAILURE_LIMIT,
|
|
734
|
+
)
|
|
735
|
+
failure_limit = _kb.DEFAULT_FAILURE_LIMIT
|
|
736
|
+
if failure_limit < 1:
|
|
737
|
+
logger.warning(
|
|
738
|
+
"kanban dispatcher: kanban.failure_limit=%r is below 1; using default %d",
|
|
739
|
+
raw_failure_limit,
|
|
740
|
+
_kb.DEFAULT_FAILURE_LIMIT,
|
|
741
|
+
)
|
|
742
|
+
failure_limit = _kb.DEFAULT_FAILURE_LIMIT
|
|
743
|
+
|
|
744
|
+
# Read stale_timeout_seconds — 0 disables stale detection.
|
|
745
|
+
raw_stale = kanban_cfg.get("dispatch_stale_timeout_seconds", 0)
|
|
746
|
+
try:
|
|
747
|
+
stale_timeout_seconds = int(raw_stale or 0)
|
|
748
|
+
except (TypeError, ValueError):
|
|
749
|
+
logger.warning(
|
|
750
|
+
"kanban dispatcher: invalid kanban.dispatch_stale_timeout_seconds=%r; "
|
|
751
|
+
"disabling stale detection",
|
|
752
|
+
raw_stale,
|
|
753
|
+
)
|
|
754
|
+
stale_timeout_seconds = 0
|
|
755
|
+
|
|
756
|
+
# Read kanban.default_assignee — fallback profile for tasks
|
|
757
|
+
# created without an explicit assignee (e.g. via the dashboard).
|
|
758
|
+
# When set, the dispatcher applies it to unassigned ready tasks
|
|
759
|
+
# instead of skipping them indefinitely (#27145). Empty string
|
|
760
|
+
# (the schema default) means "no fallback, keep skipping" —
|
|
761
|
+
# backward-compatible with existing installs.
|
|
762
|
+
default_assignee = (kanban_cfg.get("default_assignee") or "").strip() or None
|
|
763
|
+
if default_assignee:
|
|
764
|
+
logger.info(
|
|
765
|
+
"kanban dispatcher: default_assignee=%r (unassigned ready tasks "
|
|
766
|
+
"will route to this profile)",
|
|
767
|
+
default_assignee,
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
# Read kanban.max_in_progress_per_profile — per-profile concurrency
|
|
771
|
+
# cap (#21582). When set, no single profile gets more than N
|
|
772
|
+
# workers running at once, even if the global max_in_progress
|
|
773
|
+
# would allow it. Prevents one profile's local model / API quota
|
|
774
|
+
# / browser pool from being overwhelmed by a fan-out.
|
|
775
|
+
raw_per_profile = kanban_cfg.get("max_in_progress_per_profile", None)
|
|
776
|
+
max_in_progress_per_profile = None
|
|
777
|
+
if raw_per_profile is not None:
|
|
778
|
+
try:
|
|
779
|
+
max_in_progress_per_profile = int(raw_per_profile)
|
|
780
|
+
except (TypeError, ValueError):
|
|
781
|
+
logger.warning(
|
|
782
|
+
"kanban dispatcher: invalid kanban.max_in_progress_per_profile=%r; ignoring",
|
|
783
|
+
raw_per_profile,
|
|
784
|
+
)
|
|
785
|
+
max_in_progress_per_profile = None
|
|
786
|
+
else:
|
|
787
|
+
if max_in_progress_per_profile < 1:
|
|
788
|
+
logger.warning(
|
|
789
|
+
"kanban dispatcher: kanban.max_in_progress_per_profile=%r is below 1; ignoring",
|
|
790
|
+
raw_per_profile,
|
|
791
|
+
)
|
|
792
|
+
max_in_progress_per_profile = None
|
|
793
|
+
else:
|
|
794
|
+
logger.info(
|
|
795
|
+
"kanban dispatcher: max_in_progress_per_profile=%d",
|
|
796
|
+
max_in_progress_per_profile,
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
# Initial delay so the gateway finishes wiring adapters before the
|
|
800
|
+
# dispatcher spawns workers (those workers may hit gateway notify
|
|
801
|
+
# subscriptions etc.). Matches the notifier watcher's delay.
|
|
802
|
+
await asyncio.sleep(5)
|
|
803
|
+
|
|
804
|
+
# Health telemetry mirrored from `_cmd_daemon`: warn when ready
|
|
805
|
+
# queue is non-empty but spawns are 0 for N consecutive ticks —
|
|
806
|
+
# usually means broken PATH, missing venv, or credential loss.
|
|
807
|
+
HEALTH_WINDOW = 6
|
|
808
|
+
bad_ticks = 0
|
|
809
|
+
last_warn_at = 0
|
|
810
|
+
# Avoid hot-looping corrupt-looking board DBs, but do not suppress
|
|
811
|
+
# same-fingerprint retries forever: transient WAL/open races can
|
|
812
|
+
# surface as "database disk image is malformed" for one tick.
|
|
813
|
+
CORRUPT_BOARD_RETRY_AFTER_SECONDS = 300
|
|
814
|
+
disabled_corrupt_boards: dict[
|
|
815
|
+
str, tuple[tuple[str, int | None, int | None], float]
|
|
816
|
+
] = {}
|
|
817
|
+
|
|
818
|
+
def _board_db_fingerprint(slug: str) -> tuple[str, int | None, int | None]:
|
|
819
|
+
path = _kb.kanban_db_path(slug)
|
|
820
|
+
try:
|
|
821
|
+
resolved = str(path.expanduser().resolve())
|
|
822
|
+
except Exception:
|
|
823
|
+
resolved = str(path)
|
|
824
|
+
try:
|
|
825
|
+
stat = path.stat()
|
|
826
|
+
except OSError:
|
|
827
|
+
return (resolved, None, None)
|
|
828
|
+
return (resolved, stat.st_mtime_ns, stat.st_size)
|
|
829
|
+
|
|
830
|
+
def _is_corrupt_board_db_error(exc: Exception) -> bool:
|
|
831
|
+
corrupt_guard_error = getattr(_kb, "KanbanDbCorruptError", None)
|
|
832
|
+
if corrupt_guard_error is not None and isinstance(exc, corrupt_guard_error):
|
|
833
|
+
return True
|
|
834
|
+
if not isinstance(exc, sqlite3.DatabaseError):
|
|
835
|
+
return False
|
|
836
|
+
msg = str(exc).lower()
|
|
837
|
+
return (
|
|
838
|
+
"file is not a database" in msg
|
|
839
|
+
or "database disk image is malformed" in msg
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
def _tick_once_for_board(slug: str) -> "Optional[object]":
|
|
843
|
+
"""Run one dispatch_once for a specific board.
|
|
844
|
+
|
|
845
|
+
Runs in a worker thread via `asyncio.to_thread`. `board=slug`
|
|
846
|
+
is passed through `dispatch_once` so `resolve_workspace` and
|
|
847
|
+
`_default_spawn` see the right paths. The per-board DB is
|
|
848
|
+
opened explicitly so concurrent boards never share a
|
|
849
|
+
connection handle or accidentally claim across each other.
|
|
850
|
+
"""
|
|
851
|
+
conn = None
|
|
852
|
+
fingerprint = _board_db_fingerprint(slug)
|
|
853
|
+
disabled_entry = disabled_corrupt_boards.get(slug)
|
|
854
|
+
if disabled_entry is not None:
|
|
855
|
+
disabled_fingerprint, disabled_at = disabled_entry
|
|
856
|
+
age = time.monotonic() - disabled_at
|
|
857
|
+
if (
|
|
858
|
+
disabled_fingerprint == fingerprint
|
|
859
|
+
and age < CORRUPT_BOARD_RETRY_AFTER_SECONDS
|
|
860
|
+
):
|
|
861
|
+
return None
|
|
862
|
+
if disabled_fingerprint == fingerprint:
|
|
863
|
+
logger.info(
|
|
864
|
+
"kanban dispatcher: board %s database fingerprint unchanged "
|
|
865
|
+
"after %.0fs quarantine; retrying dispatch",
|
|
866
|
+
slug,
|
|
867
|
+
age,
|
|
868
|
+
)
|
|
869
|
+
else:
|
|
870
|
+
logger.info(
|
|
871
|
+
"kanban dispatcher: board %s database changed; retrying dispatch",
|
|
872
|
+
slug,
|
|
873
|
+
)
|
|
874
|
+
disabled_corrupt_boards.pop(slug, None)
|
|
875
|
+
try:
|
|
876
|
+
conn = _kb.connect(board=slug)
|
|
877
|
+
# `connect()` runs the schema + idempotent migration on
|
|
878
|
+
# first open per process; the previous explicit
|
|
879
|
+
# `init_db()` call here busted the per-process cache and
|
|
880
|
+
# re-ran the migration on a second connection, racing
|
|
881
|
+
# the first. See the matching comment in
|
|
882
|
+
# `_kanban_notifier_watcher` and issue #21378.
|
|
883
|
+
return _kb.dispatch_once(
|
|
884
|
+
conn,
|
|
885
|
+
board=slug,
|
|
886
|
+
max_spawn=max_spawn,
|
|
887
|
+
max_in_progress=max_in_progress,
|
|
888
|
+
failure_limit=failure_limit,
|
|
889
|
+
stale_timeout_seconds=stale_timeout_seconds,
|
|
890
|
+
default_assignee=default_assignee,
|
|
891
|
+
max_in_progress_per_profile=max_in_progress_per_profile,
|
|
892
|
+
)
|
|
893
|
+
except sqlite3.DatabaseError as exc:
|
|
894
|
+
if _is_corrupt_board_db_error(exc):
|
|
895
|
+
disabled_corrupt_boards[slug] = (fingerprint, time.monotonic())
|
|
896
|
+
logger.error(
|
|
897
|
+
"kanban dispatcher: board %s database %s is not a valid "
|
|
898
|
+
"SQLite database; pausing dispatch for this board until "
|
|
899
|
+
"the file changes, the gateway restarts, or the "
|
|
900
|
+
"quarantine timer expires. Move or restore the file, "
|
|
901
|
+
"then run `hermes kanban init` if you need a fresh board.",
|
|
902
|
+
slug,
|
|
903
|
+
fingerprint[0],
|
|
904
|
+
)
|
|
905
|
+
return None
|
|
906
|
+
logger.exception("kanban dispatcher: tick failed on board %s", slug)
|
|
907
|
+
return None
|
|
908
|
+
except Exception as exc:
|
|
909
|
+
if _is_corrupt_board_db_error(exc):
|
|
910
|
+
disabled_corrupt_boards[slug] = (fingerprint, time.monotonic())
|
|
911
|
+
logger.error(
|
|
912
|
+
"kanban dispatcher: board %s database %s is not a valid "
|
|
913
|
+
"SQLite database; pausing dispatch for this board until "
|
|
914
|
+
"the file changes, the gateway restarts, or the "
|
|
915
|
+
"quarantine timer expires. Move or restore the file, "
|
|
916
|
+
"then run `hermes kanban init` if you need a fresh board.",
|
|
917
|
+
slug,
|
|
918
|
+
fingerprint[0],
|
|
919
|
+
)
|
|
920
|
+
return None
|
|
921
|
+
logger.exception("kanban dispatcher: tick failed on board %s", slug)
|
|
922
|
+
return None
|
|
923
|
+
finally:
|
|
924
|
+
if conn is not None:
|
|
925
|
+
try:
|
|
926
|
+
conn.close()
|
|
927
|
+
except Exception:
|
|
928
|
+
pass
|
|
929
|
+
|
|
930
|
+
def _tick_once() -> "list[tuple[str, Optional[object]]]":
|
|
931
|
+
"""Run one dispatch_once per board. Returns (slug, result) pairs.
|
|
932
|
+
|
|
933
|
+
Enumerating boards on every tick keeps the dispatcher honest
|
|
934
|
+
when users create a new board mid-run: no restart required,
|
|
935
|
+
the next tick picks it up automatically.
|
|
936
|
+
"""
|
|
937
|
+
try:
|
|
938
|
+
boards = _kb.list_boards(include_archived=False)
|
|
939
|
+
except Exception:
|
|
940
|
+
boards = [_kb.read_board_metadata(_kb.DEFAULT_BOARD)]
|
|
941
|
+
out: list[tuple[str, "Optional[object]"]] = []
|
|
942
|
+
for b in boards:
|
|
943
|
+
slug = b.get("slug") or _kb.DEFAULT_BOARD
|
|
944
|
+
out.append((slug, _tick_once_for_board(slug)))
|
|
945
|
+
return out
|
|
946
|
+
|
|
947
|
+
def _ready_nonempty() -> bool:
|
|
948
|
+
"""Cheap probe: is there at least one ready+assigned+unclaimed
|
|
949
|
+
task on ANY board whose assignee maps to a real Hermes profile
|
|
950
|
+
(i.e. one the dispatcher would actually spawn for)?
|
|
951
|
+
|
|
952
|
+
Tasks assigned to control-plane lanes (e.g. ``orion-cc``,
|
|
953
|
+
``orion-research``) are pulled by terminals via
|
|
954
|
+
``claim_task`` directly and never spawnable, so a queue full
|
|
955
|
+
of those is "correctly idle", not "stuck". Filtering them out
|
|
956
|
+
here keeps the stuck-warn fire only on real failures (broken
|
|
957
|
+
PATH, missing venv, credential loss for a real Hermes profile).
|
|
958
|
+
"""
|
|
959
|
+
try:
|
|
960
|
+
boards = _kb.list_boards(include_archived=False)
|
|
961
|
+
except Exception:
|
|
962
|
+
boards = [_kb.read_board_metadata(_kb.DEFAULT_BOARD)]
|
|
963
|
+
for b in boards:
|
|
964
|
+
slug = b.get("slug") or _kb.DEFAULT_BOARD
|
|
965
|
+
conn = None
|
|
966
|
+
try:
|
|
967
|
+
conn = _kb.connect(board=slug)
|
|
968
|
+
if _kb.has_spawnable_ready(conn):
|
|
969
|
+
return True
|
|
970
|
+
if _kb.has_spawnable_review(conn):
|
|
971
|
+
return True
|
|
972
|
+
except Exception:
|
|
973
|
+
continue
|
|
974
|
+
finally:
|
|
975
|
+
if conn is not None:
|
|
976
|
+
try:
|
|
977
|
+
conn.close()
|
|
978
|
+
except Exception:
|
|
979
|
+
pass
|
|
980
|
+
return False
|
|
981
|
+
|
|
982
|
+
# Auto-decompose: turn fresh triage tasks into ready workgraphs
|
|
983
|
+
# before the dispatcher fans out workers. Gated by
|
|
984
|
+
# ``kanban.auto_decompose`` (default True). Capped by
|
|
985
|
+
# ``kanban.auto_decompose_per_tick`` (default 3) so a bulk-load
|
|
986
|
+
# of triage tasks doesn't burst-spend the aux LLM in one tick;
|
|
987
|
+
# remainder defers to subsequent ticks.
|
|
988
|
+
auto_decompose_enabled = bool(kanban_cfg.get("auto_decompose", True))
|
|
989
|
+
try:
|
|
990
|
+
auto_decompose_per_tick = int(
|
|
991
|
+
kanban_cfg.get("auto_decompose_per_tick", 3) or 3
|
|
992
|
+
)
|
|
993
|
+
except (TypeError, ValueError):
|
|
994
|
+
auto_decompose_per_tick = 3
|
|
995
|
+
if auto_decompose_per_tick < 1:
|
|
996
|
+
auto_decompose_per_tick = 1
|
|
997
|
+
|
|
998
|
+
def _auto_decompose_tick() -> int:
|
|
999
|
+
"""Run the auto-decomposer for up to N triage tasks across all
|
|
1000
|
+
boards. Returns the number of triage tasks that were
|
|
1001
|
+
successfully decomposed or specified this tick.
|
|
1002
|
+
"""
|
|
1003
|
+
try:
|
|
1004
|
+
from hermes_cli import kanban_decompose as _decomp
|
|
1005
|
+
except Exception as exc: # pragma: no cover
|
|
1006
|
+
logger.warning(
|
|
1007
|
+
"kanban auto-decompose: import failed (%s); skipping", exc,
|
|
1008
|
+
)
|
|
1009
|
+
return 0
|
|
1010
|
+
try:
|
|
1011
|
+
boards = _kb.list_boards(include_archived=False)
|
|
1012
|
+
except Exception:
|
|
1013
|
+
boards = [_kb.read_board_metadata(_kb.DEFAULT_BOARD)]
|
|
1014
|
+
attempted = 0
|
|
1015
|
+
successes = 0
|
|
1016
|
+
for b in boards:
|
|
1017
|
+
slug = b.get("slug") or _kb.DEFAULT_BOARD
|
|
1018
|
+
if attempted >= auto_decompose_per_tick:
|
|
1019
|
+
break
|
|
1020
|
+
# Pin this board for the duration of the call — same
|
|
1021
|
+
# pattern as the dashboard specify endpoint. The
|
|
1022
|
+
# decomposer module connects with no board kwarg and
|
|
1023
|
+
# relies on the env var.
|
|
1024
|
+
prev_env = os.environ.get("HERMES_KANBAN_BOARD")
|
|
1025
|
+
try:
|
|
1026
|
+
os.environ["HERMES_KANBAN_BOARD"] = slug
|
|
1027
|
+
try:
|
|
1028
|
+
triage_ids = _decomp.list_triage_ids()
|
|
1029
|
+
except Exception as exc:
|
|
1030
|
+
logger.debug(
|
|
1031
|
+
"kanban auto-decompose: list_triage_ids failed on board %s (%s)",
|
|
1032
|
+
slug, exc,
|
|
1033
|
+
)
|
|
1034
|
+
triage_ids = []
|
|
1035
|
+
for tid in triage_ids:
|
|
1036
|
+
if attempted >= auto_decompose_per_tick:
|
|
1037
|
+
break
|
|
1038
|
+
attempted += 1
|
|
1039
|
+
try:
|
|
1040
|
+
outcome = _decomp.decompose_task(
|
|
1041
|
+
tid, author="auto-decomposer",
|
|
1042
|
+
)
|
|
1043
|
+
except Exception:
|
|
1044
|
+
logger.exception(
|
|
1045
|
+
"kanban auto-decompose: decompose_task crashed on %s",
|
|
1046
|
+
tid,
|
|
1047
|
+
)
|
|
1048
|
+
continue
|
|
1049
|
+
if outcome.ok:
|
|
1050
|
+
successes += 1
|
|
1051
|
+
if outcome.fanout and outcome.child_ids:
|
|
1052
|
+
logger.info(
|
|
1053
|
+
"kanban auto-decompose [%s]: %s → %d children",
|
|
1054
|
+
slug, tid, len(outcome.child_ids),
|
|
1055
|
+
)
|
|
1056
|
+
else:
|
|
1057
|
+
logger.info(
|
|
1058
|
+
"kanban auto-decompose [%s]: %s → single task (no fanout)",
|
|
1059
|
+
slug, tid,
|
|
1060
|
+
)
|
|
1061
|
+
else:
|
|
1062
|
+
# Common no-op reasons (no aux client configured) shouldn't
|
|
1063
|
+
# spam logs every tick. Log at debug.
|
|
1064
|
+
logger.debug(
|
|
1065
|
+
"kanban auto-decompose [%s]: %s skipped: %s",
|
|
1066
|
+
slug, tid, outcome.reason,
|
|
1067
|
+
)
|
|
1068
|
+
finally:
|
|
1069
|
+
if prev_env is None:
|
|
1070
|
+
os.environ.pop("HERMES_KANBAN_BOARD", None)
|
|
1071
|
+
else:
|
|
1072
|
+
os.environ["HERMES_KANBAN_BOARD"] = prev_env
|
|
1073
|
+
return successes
|
|
1074
|
+
|
|
1075
|
+
logger.info(
|
|
1076
|
+
"kanban dispatcher: embedded in gateway (interval=%.1fs)", interval
|
|
1077
|
+
)
|
|
1078
|
+
while self._running:
|
|
1079
|
+
try:
|
|
1080
|
+
# Reap zombie children before per-board work so a board DB
|
|
1081
|
+
# failure cannot block cleanup of unrelated workers.
|
|
1082
|
+
pids = await asyncio.to_thread(_kb.reap_worker_zombies)
|
|
1083
|
+
if pids:
|
|
1084
|
+
logger.info(
|
|
1085
|
+
"kanban dispatcher: reaped %d zombie worker(s), pids=%s",
|
|
1086
|
+
len(pids),
|
|
1087
|
+
pids,
|
|
1088
|
+
)
|
|
1089
|
+
except Exception:
|
|
1090
|
+
logger.exception("kanban dispatcher: zombie reaper failed")
|
|
1091
|
+
|
|
1092
|
+
try:
|
|
1093
|
+
if auto_decompose_enabled:
|
|
1094
|
+
await asyncio.to_thread(_auto_decompose_tick)
|
|
1095
|
+
results = await asyncio.to_thread(_tick_once)
|
|
1096
|
+
any_spawned = False
|
|
1097
|
+
for slug, res in (results or []):
|
|
1098
|
+
if res is not None and getattr(res, "spawned", None):
|
|
1099
|
+
any_spawned = True
|
|
1100
|
+
# Quiet by default — only log when something actually
|
|
1101
|
+
# happened, so an idle gateway stays silent.
|
|
1102
|
+
logger.info(
|
|
1103
|
+
"kanban dispatcher [%s]: spawned=%d reclaimed=%d "
|
|
1104
|
+
"crashed=%d timed_out=%d promoted=%d auto_blocked=%d",
|
|
1105
|
+
slug,
|
|
1106
|
+
len(res.spawned),
|
|
1107
|
+
res.reclaimed,
|
|
1108
|
+
len(res.crashed) if hasattr(res.crashed, "__len__") else 0,
|
|
1109
|
+
len(res.timed_out) if hasattr(res.timed_out, "__len__") else 0,
|
|
1110
|
+
res.promoted,
|
|
1111
|
+
len(res.auto_blocked) if hasattr(res.auto_blocked, "__len__") else 0,
|
|
1112
|
+
)
|
|
1113
|
+
# Health telemetry (aggregate across boards)
|
|
1114
|
+
ready_pending = await asyncio.to_thread(_ready_nonempty)
|
|
1115
|
+
if ready_pending and not any_spawned:
|
|
1116
|
+
bad_ticks += 1
|
|
1117
|
+
else:
|
|
1118
|
+
bad_ticks = 0
|
|
1119
|
+
if bad_ticks >= HEALTH_WINDOW:
|
|
1120
|
+
now = int(time.time())
|
|
1121
|
+
if now - last_warn_at >= 300:
|
|
1122
|
+
logger.warning(
|
|
1123
|
+
"kanban dispatcher stuck: ready queue non-empty for "
|
|
1124
|
+
"%d consecutive ticks but 0 workers spawned. Check "
|
|
1125
|
+
"profile health (venv, PATH, credentials) and "
|
|
1126
|
+
"`hermes kanban list --status ready`.",
|
|
1127
|
+
bad_ticks,
|
|
1128
|
+
)
|
|
1129
|
+
last_warn_at = now
|
|
1130
|
+
except asyncio.CancelledError:
|
|
1131
|
+
logger.debug("kanban dispatcher: cancelled")
|
|
1132
|
+
_release_singleton_lock(self._kanban_dispatcher_lock_handle)
|
|
1133
|
+
self._kanban_dispatcher_lock_handle = None
|
|
1134
|
+
raise
|
|
1135
|
+
except Exception:
|
|
1136
|
+
logger.exception("kanban dispatcher: unexpected watcher error")
|
|
1137
|
+
|
|
1138
|
+
# Sleep in 1s slices so shutdown is snappy — otherwise a stop()
|
|
1139
|
+
# waits up to `interval` seconds for the current sleep to finish.
|
|
1140
|
+
slept = 0.0
|
|
1141
|
+
while slept < interval and self._running:
|
|
1142
|
+
await asyncio.sleep(min(1.0, interval - slept))
|
|
1143
|
+
slept += 1.0
|
|
1144
|
+
|
|
1145
|
+
_release_singleton_lock(self._kanban_dispatcher_lock_handle)
|
|
1146
|
+
self._kanban_dispatcher_lock_handle = None
|