mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
In classic CLI mode the dangerous-command approval prompt (and the clarify, sudo, and secret-capture prompts) could fail to render: the user saw '⏱ Timeout — denying command' after 60s without ever seeing the panel, making approvals.mode: manual unusable. Root cause. These prompts run their wait loop on the agent/background thread: they set modal state that a ConditionalContainer's filter reads, then call self._invalidate() to repaint so the panel appears. _invalidate() is a THROTTLED wrapper built for high-frequency background repaints (spinner frames, streaming) — it (a) returns early while a SIGWINCH resize-recovery is pending, and (b) otherwise only repaints if 250ms elapsed since the last paint. Under either condition the modal's entry paint is silently dropped, the ConditionalContainer never re-evaluates, and the prompt times out unseen. The throttle never belonged on these paths. Originally the callbacks painted with a direct self._app.invalidate() and worked; a throttle PR blanket-replaced every invalidate (including these rare, one-shot, user-blocking modal paints) with the throttled _invalidate(); a later commit removed an idle 1Hz repaint that had been masking dropped modal paints, surfacing the bug. Notably the modal KEY-BINDING handlers (↑/↓/Enter) already paint with a direct event.app.invalidate(), never the throttle — the background-thread callbacks were the inconsistent ones. Fix. Add a small _paint_now() helper that paints directly (guarded for a missing _app, exception-safe) and route the four modal paths' entry, response, countdown, and teardown paints through it — matching the key-handler idiom. This covers approval, clarify, sudo, and the secret-capture teardown (_submit_secret_response, which previously used the throttled _invalidate() so its panel could linger after submit). _invalidate() is left untouched and its docstring now states it is for high-frequency background repaints only; modal/interactive paints must use _paint_now()/_app.invalidate() directly. This also fixes the resize-recovery edge case for free (a direct paint never consults the resize guard) without a throttle-bypass flag that could be cargo-culted onto hot paths. Countdown refresh cadence tightened 5s->1s so the timer stays visible while waiting, and a copy-pasted duplicate countdown block in _clarify_callback is removed. Tests: TestModalPaintNow drives all three wait-loop callbacks on a background thread with BOTH gates active (_resize_recovery_pending=True + a recent _last_invalidate in the throttle window) and asserts the panel paints on entry AND repaints on teardown; plus a secret-teardown test, a direct _paint_now-vs-_invalidate gate test, and a no-_app safety test. Each modal test fails if its paint is reverted to _invalidate(). 17 in-file tests pass; full tests/cli suite green (900). Diagnosis credit: the throttle-drop root cause was identified by @sanidhyasin in #41116; @islam666 independently reached the same direct-invalidate approach in #41166; original report #41098 by @jodonnel. |
||
|---|---|---|
| .. | ||
| __init__.py | ||
| test_bracketed_paste_timeout.py | ||
| test_branch_command.py | ||
| test_busy_input_mode_command.py | ||
| test_cli_approval_ui.py | ||
| test_cli_background_status_indicator.py | ||
| test_cli_background_tui_refresh.py | ||
| test_cli_bracketed_paste_sanitizer.py | ||
| test_cli_browser_connect.py | ||
| test_cli_context_warning.py | ||
| test_cli_copy_command.py | ||
| test_cli_extension_hooks.py | ||
| test_cli_external_editor.py | ||
| test_cli_file_drop.py | ||
| test_cli_force_redraw.py | ||
| test_cli_goal_interrupt.py | ||
| test_cli_image_command.py | ||
| test_cli_init.py | ||
| test_cli_insights_command.py | ||
| test_cli_interrupt_subagent.py | ||
| test_cli_light_mode.py | ||
| test_cli_loading_indicator.py | ||
| test_cli_markdown_rendering.py | ||
| test_cli_mcp_config_watch.py | ||
| test_cli_new_session.py | ||
| test_cli_prefix_matching.py | ||
| test_cli_preloaded_skills.py | ||
| test_cli_provider_resolution.py | ||
| test_cli_reload_skills.py | ||
| test_cli_resume_command.py | ||
| test_cli_retry.py | ||
| test_cli_save_config_value.py | ||
| test_cli_secret_capture.py | ||
| test_cli_shift_enter_newline.py | ||
| test_cli_shutdown_memory_messages.py | ||
| test_cli_skin_integration.py | ||
| test_cli_status_bar.py | ||
| test_cli_status_command.py | ||
| test_cli_steer_busy_path.py | ||
| test_cli_terminal_response_sanitizer.py | ||
| test_cli_terminal_shortcuts.py | ||
| test_cli_tools_command.py | ||
| test_cli_user_message_preview.py | ||
| test_cli_yolo_toggle.py | ||
| test_compress_focus.py | ||
| test_compress_here.py | ||
| test_cprint_bg_thread.py | ||
| test_ctrl_enter_newline.py | ||
| test_cwd_env_respect.py | ||
| test_destructive_slash_confirm.py | ||
| test_destructive_slash_inline_skip_e2e.py | ||
| test_exit_delete_session.py | ||
| test_exit_summary_resume_hint.py | ||
| test_fast_command.py | ||
| test_gquota_command.py | ||
| test_manual_compress.py | ||
| test_partial_compress.py | ||
| test_personality_none.py | ||
| test_prefill_config.py | ||
| test_prepend_note_to_message.py | ||
| test_prompt_text_input_thread_safety.py | ||
| test_quick_commands.py | ||
| test_reasoning_command.py | ||
| test_resume_display.py | ||
| test_resume_quiet_stderr.py | ||
| test_save_conversation_location.py | ||
| test_session_boundary_hooks.py | ||
| test_slash_command_interrupt.py | ||
| test_slash_confirm_windows.py | ||
| test_steer_inline_repaint_34569.py | ||
| test_stream_delta_think_tag.py | ||
| test_surrogate_sanitization.py | ||
| test_tool_progress_scrollback.py | ||
| test_tui_terminal_reset_on_exit.py | ||
| test_update_command.py | ||
| test_version_command.py | ||
| test_worktree.py | ||
| test_worktree_security.py | ||