mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-29 01:31:41 +00:00
* fix(terminal): three-layer defense against watch_patterns notification spam Background processes that stack notify_on_complete=True with watch_patterns can flood the user with duplicate, delayed notifications — matches deliver asynchronously via the completion queue and continue arriving minutes after the process has exited. The docstring warning against this (PR #12113) has proven insufficient; agents still misuse the combination. Three layered defenses, each sufficient on its own: 1. Mutual exclusion (terminal_tool.py): When both flags are set on a background process, drop watch_patterns with a warning. notify_on_complete wins because 'let me know when it's done' is the more useful signal and fires exactly once. Extracted as _resolve_notification_flag_conflict() so the rule is testable in isolation. 2. Suppress-after-exit (process_registry.py): _check_watch_patterns() now bails the moment session.exited is True. Post-exit chunks (buffered reads draining after the process is gone) no longer produce notifications. This is the fix flagged as future work in session 20260418_020302_79881c. 3. Global circuit breaker (process_registry.py): Per-session rate limits don't catch the sibling-flood case — N concurrent processes can each stay under 8/10s and still collectively spam. New WATCH_GLOBAL_MAX_PER_WINDOW=15 cap trips a 30-second cooldown across ALL sessions, emits a single watch_overflow_tripped event, silently counts dropped events, and emits a watch_overflow_released summary when the cooldown ends. Also updates the tool schema + docstring to document the new behavior. Tests: 8 new tests covering all three fixes (suppress-after-exit x2, mutual-exclusion resolver x4, global breaker trip/cooldown/release x2). All 60 tests across test_watch_patterns.py, test_notify_on_complete.py, test_terminal_tool.py pass. Real-world trigger: self-inflicted in session 20260425_051924 — three concurrent hermes-sweeper review subprocesses each set watch_patterns= ['failed validation', 'errored'] AND notify_on_complete=True, then iterated over multiple items, producing enough matches per process to defeat the per-session cap while staying under the global cap that didn't yet exist. * fix(terminal): aggressive 1-per-15s watch_patterns rate limit + strike-3 promotion Per Teknium's direction, the watch_patterns rate limit is now much more aggressive and self-healing. ## New rule — per session - HARD cap: 1 watch-match notification per 15 seconds per process. - Any match arriving inside the cooldown window is dropped and counts as ONE strike for that window (many drops in the same window still = 1 strike). - After 3 consecutive strike windows, watch_patterns is permanently disabled for the session and the session is auto-promoted to notify_on_complete semantics — exactly one notification when the process actually exits. - A cooldown window that expires with zero drops resets the consecutive strike counter — healthy cadence is forgiven. ## Schema + docstring rewritten The tool schema description now gives the model explicit guidance: - notify_on_complete is 'the right choice for almost every long-running task' - watch_patterns is for RARE one-shot signals on LONG-LIVED processes - Do NOT use watch_patterns with loops/batch jobs — error patterns fire every iteration and will hit the strike limit fast - Mutual exclusion is stated on both parameter descriptions - 1/15s cooldown and 3-strike promotion are stated in the watch_patterns description so the model sees the contract every turn ## Removed - WATCH_MAX_PER_WINDOW (8/10s) and WATCH_OVERLOAD_KILL_SECONDS (45) — the new 1/15s limit subsumes both; keeping them would double-count. - _watch_window_hits / _watch_window_start / _watch_overload_since fields on ProcessSession. Replaced by _watch_last_emit_at / _watch_cooldown_until / _watch_strike_candidate / _watch_consecutive_strikes. ## Kept - Global circuit breaker across all sessions (15/10s → 30s cooldown) as a secondary safety net for concurrent siblings. Still valuable when 20 short-lived processes each fire once — none individually violates the per-session limit. - Suppress-after-exit guard. - Mutual exclusion resolver at the tool entry point. ## Tests - 6 new tests in TestPerSessionRateLimit covering: first match delivers, second in cooldown suppressed, multi-drop = single strike, 3 strikes disables + promotes, clean window resets counter, suppressed count carried to next emit. - Global circuit breaker tests rewritten to use fresh sessions instead of hacking removed per-window fields. - 50/50 watch_patterns + notify_on_complete tests pass. - 60/60 including test_terminal_tool.py pass. * feat(dashboard): page-scoped plugin slots for built-in pages Dashboard plugins can now inject components into specific built-in pages (Sessions, Analytics, Logs, Cron, Skills, Config, Env, Docs, Chat) without overriding the whole route. Previously, plugins could only: 1. Add new tabs (tab.path) 2. Replace whole built-in pages (tab.override) 3. Inject into global shell slots (header-*, footer-*, pre-main, ...) None of those let a plugin add a banner, card, or widget to an existing page. The new <page>:top / <page>:bottom slots close that gap, reusing the existing registerSlot() API. Changes - web/src/plugins/slots.ts: 18 new KNOWN_SLOT_NAMES entries (sessions:top, sessions:bottom, analytics:top, ..., chat:bottom), grouped under "Shell-wide" vs "Page-scoped" in the docblock - web/src/pages/*: each built-in page now renders <PluginSlot name="<page>:top" /> as the first child of its outer wrapper and <PluginSlot name="<page>:bottom" /> as the last child -- zero visual cost when no plugin registers - plugins/example-dashboard: registers a demo banner into sessions:top via registerSlot(), with matching slots entry in the manifest -- so freshly-setup users can see what page-scoped slots look like without writing any plugin code - website/docs: new "Page-scoped slots" table in the plugin authoring guide, with a worked example - tests/hermes_cli/test_web_server.py: round-trip test for colon-bearing slot names (sessions:top, analytics:bottom, ...) Validation - npm run build: clean (tsc -b + vite build, 2761 modules) - scripts/run_tests.sh tests/hermes_cli/test_web_server.py::TestDashboardPluginManifestExtensions: 5/5 pass |
||
|---|---|---|
| .. | ||
| __init__.py | ||
| test_ai_gateway_models.py | ||
| test_anthropic_model_flow_stale_oauth.py | ||
| test_anthropic_oauth_flow.py | ||
| test_anthropic_provider_persistence.py | ||
| test_api_key_providers.py | ||
| test_arcee_provider.py | ||
| test_argparse_flag_propagation.py | ||
| test_at_context_completion_filter.py | ||
| test_atomic_json_write.py | ||
| test_atomic_yaml_write.py | ||
| test_auth_codex_provider.py | ||
| test_auth_commands.py | ||
| test_auth_nous_provider.py | ||
| test_auth_provider_gate.py | ||
| test_auth_qwen_provider.py | ||
| test_auth_ssl_macos.py | ||
| test_aux_config.py | ||
| test_backup.py | ||
| test_banner.py | ||
| test_banner_git_state.py | ||
| test_banner_skills.py | ||
| test_chat_skills_flag.py | ||
| test_claw.py | ||
| test_clear_stale_base_url.py | ||
| test_cmd_update.py | ||
| test_coalesce_session_args.py | ||
| test_codex_cli_model_picker.py | ||
| test_codex_models.py | ||
| test_commands.py | ||
| test_completion.py | ||
| test_config.py | ||
| test_config_drift.py | ||
| test_config_env_expansion.py | ||
| test_config_env_refs.py | ||
| test_config_validation.py | ||
| test_container_aware_cli.py | ||
| test_copilot_auth.py | ||
| test_copilot_context.py | ||
| test_copilot_in_model_list.py | ||
| test_copilot_token_exchange.py | ||
| test_cron.py | ||
| test_custom_provider_model_switch.py | ||
| test_debug.py | ||
| test_deprecated_cwd_warning.py | ||
| test_detect_api_mode_for_url.py | ||
| test_determine_api_mode_hostname.py | ||
| test_dingtalk_auth.py | ||
| test_doctor.py | ||
| test_doctor_command_install.py | ||
| test_env_loader.py | ||
| test_env_sanitize_on_load.py | ||
| test_gateway.py | ||
| test_gateway_linger.py | ||
| test_gateway_runtime_health.py | ||
| test_gateway_service.py | ||
| test_gateway_wsl.py | ||
| test_gemini_free_tier_setup_block.py | ||
| test_gemini_provider.py | ||
| test_hooks_cli.py | ||
| test_ignore_user_config_flags.py | ||
| test_image_gen_picker.py | ||
| test_launcher.py | ||
| test_logs.py | ||
| test_managed_installs.py | ||
| test_mcp_config.py | ||
| test_mcp_tools_config.py | ||
| test_memory_reset.py | ||
| test_model_normalize.py | ||
| test_model_picker_viewport.py | ||
| test_model_provider_persistence.py | ||
| test_model_switch_context_display.py | ||
| test_model_switch_copilot_api_mode.py | ||
| test_model_switch_custom_providers.py | ||
| test_model_switch_opencode_anthropic.py | ||
| test_model_switch_variant_tags.py | ||
| test_model_validation.py | ||
| test_models.py | ||
| test_models_dev_preferred_merge.py | ||
| test_non_ascii_credential.py | ||
| test_nous_hermes_non_agentic.py | ||
| test_nous_subscription.py | ||
| test_ollama_cloud_auth.py | ||
| test_ollama_cloud_provider.py | ||
| test_opencode_go_in_model_list.py | ||
| test_opencode_go_validation_fallback.py | ||
| test_overlay_slug_resolution.py | ||
| test_path_completion.py | ||
| test_placeholder_usage.py | ||
| test_plugin_cli_registration.py | ||
| test_plugin_scanner_recursion.py | ||
| test_plugins.py | ||
| test_plugins_cmd.py | ||
| test_profile_export_credentials.py | ||
| test_profiles.py | ||
| test_provider_config_validation.py | ||
| test_pty_bridge.py | ||
| test_reasoning_effort_menu.py | ||
| test_redact_config_bridge.py | ||
| test_runtime_provider_resolution.py | ||
| test_session_browse.py | ||
| test_sessions_delete.py | ||
| test_set_config_value.py | ||
| test_setup.py | ||
| test_setup_agent_settings.py | ||
| test_setup_hermes_script.py | ||
| test_setup_matrix_e2ee.py | ||
| test_setup_model_provider.py | ||
| test_setup_noninteractive.py | ||
| test_setup_openclaw_migration.py | ||
| test_setup_prompt_menus.py | ||
| test_skills_config.py | ||
| test_skills_hub.py | ||
| test_skills_install_flags.py | ||
| test_skills_skip_confirm.py | ||
| test_skills_subparser.py | ||
| test_skin_engine.py | ||
| test_spotify_auth.py | ||
| test_status.py | ||
| test_status_model_provider.py | ||
| test_subparser_routing_fallback.py | ||
| test_subprocess_timeouts.py | ||
| test_terminal_menu_fallbacks.py | ||
| test_timeouts.py | ||
| test_tips.py | ||
| test_tool_token_estimation.py | ||
| test_tools_config.py | ||
| test_tools_disable_enable.py | ||
| test_tui_npm_install.py | ||
| test_tui_resume_flow.py | ||
| test_update_autostash.py | ||
| test_update_check.py | ||
| test_update_config_clears_custom_fields.py | ||
| test_update_gateway_restart.py | ||
| test_update_hangup_protection.py | ||
| test_user_providers_model_switch.py | ||
| test_voice_wrapper.py | ||
| test_web_server.py | ||
| test_web_server_host_header.py | ||
| test_webhook_cli.py | ||
| test_xiaomi_provider.py | ||