fix(display): suppress KawaiiSpinner \r animation under patch_stdout#2908
Closed
Mibayy wants to merge 6 commits intoNousResearch:mainfrom
Closed
fix(display): suppress KawaiiSpinner \r animation under patch_stdout#2908Mibayy wants to merge 6 commits intoNousResearch:mainfrom
Mibayy wants to merge 6 commits intoNousResearch:mainfrom
Conversation
added 6 commits
March 24, 2026 22:43
Previously, all subagents inherited the parent's shared IterationBudget
object. This caused two compounding problems:
1. The parent's already-consumed turns reduced the budget available to
children before they even started.
2. In batch mode (3 parallel subagents), all children raced to consume
the same depleted pool, each getting roughly 1/N of what remained.
Example of the bug:
Parent uses 30 of 90 turns, then spawns 3 subagents with max_iterations=50.
Only 60 turns remain in the shared budget.
3 children race for 60 → each gets ~20 on average → all hit max_iterations
mid-task and exit with exit_reason: max_iterations.
Fix: introduce budget_mode config option (delegation.budget_mode).
isolated (default): each child gets a fresh IterationBudget(max_iterations)
starting at 0. max_iterations=50 means exactly 50 turns for that child,
regardless of parent consumption or sibling activity.
shared (legacy): preserves the old behaviour for users who explicitly
want a session-wide hard cap across parent + all children.
After this change:
Parent: 30/90 consumed (irrelevant to children in isolated mode)
Subagent 1: 0/50 fresh budget → can use all 50 turns
Subagent 2: 0/50 fresh budget → can use all 50 turns
Subagent 3: 0/50 fresh budget → can use all 50 turns
Fixes NousResearch#2873
Extends the existing migration script from ~15% to ~95% coverage of OpenClaw's configuration surface. Adds 17 new migration modules: Direct migrations (written to config.yaml/.env): - MCP servers: full server definitions with transport, tools, sampling - Agent defaults: reasoning_effort, compression, human_delay, timezone - Session config: reset triggers (daily/idle) -> session_reset - Full model providers: custom_providers with base_url/api_mode - Deep channel config: Matrix, Mattermost, IRC, Discord deep settings - Browser config: timeout settings - Tools config: exec timeout -> terminal.timeout - Approvals: mode mapping (smart/manual/auto -> Hermes equivalents) Archived for manual review (no direct Hermes equivalent): - Plugins config + installed extensions - Cron jobs (with note to use 'hermes cron') - Hooks/webhooks config - Multi-agent list + routing bindings - Gateway config (port, auth, TLS) - Memory backend config (QMD, vector search) - Skills registry per-entry config - UI/identity settings - Logging/diagnostics preferences Also adds: - MIGRATION_NOTES.md generation with PM2 reassurance message - _set_env_var helper for consistent env file management - Updated presets to include all new options - Comprehensive mock test passing (12 migrated, 12 archived)
Replaces raw JSON dump with a formatted box showing migrated/archived/ skipped/conflict/error counts, detailed item lists with labels, PM2 reassurance message, and actionable next steps. JSON output available via MIGRATION_JSON_OUTPUT=1 env var.
…lls guard test MIGRATION_JSON_OUTPUT env var is a legitimate CLI feature flag that enables JSON output mode, not an env dump. Add it alongside agent_config_mod as an accepted finding in test_skill_installs_cleanly_under_skills_guard.
…uard test The scanner flags two print statements that tell the user to *review* ~/.hermes/config.yaml in the post-migration summary. The script never writes to that file — those are informational strings, not config mutations.
When the CLI runs inside prompt_toolkit's patch_stdout context, sys.stdout is replaced by a StdoutProxy that queues writes and injects newlines around each flush(). This causes every \r spinner frame to land on its own line instead of overwriting the previous one, producing a visible overdraw glitch where the spinner text and the status bar repeatedly swap positions. The CLI already renders spinner state via a dedicated TUI widget (_spinner_text / get_spinner_text) driven by prompt_toolkit's own renderer. KawaiiSpinner running a parallel \r loop on the same fd is therefore both redundant and harmful. Fix: detect StdoutProxy via the '_raw' sentinel attribute (with a class-name fallback) and skip the animation loop entirely when the proxy is active. The spinner thread still runs so start()/stop() semantics are preserved; it just sleeps instead of writing frames. Also removes the 0.4s flush rate-limit workaround that was papering over the same issue. Fixes the visual glitch reported in Screen_Recording_2026-03-24_at_23.20.21.mov where tool-output lines and the token-budget status bar visibly flickered / exchanged positions during long agent runs.
teknium1
added a commit
that referenced
this pull request
Mar 25, 2026
When the CLI is active, sys.stdout is prompt_toolkit's StdoutProxy which queues writes and injects newlines around each flush(). This causes every \r spinner frame to land on its own line instead of overwriting the previous one, producing visible flickering where the spinner and status bar repeatedly swap positions. The CLI already renders spinner state via a dedicated TUI widget (_spinner_text / get_spinner_text), so KawaiiSpinner's \r-based loop is redundant under StdoutProxy. Detect the proxy and suppress the animation entirely — the thread still runs to preserve start()/stop() semantics. Also removes the 0.4s flush rate-limit workaround that was papering over the same issue, and cleans up the unused _last_flush_time attribute. Salvaged from PR #2908 by Mibayy (fixed _raw -> raw detection, dropped unrelated bundled changes).
teknium1
added a commit
that referenced
this pull request
Mar 25, 2026
) When the CLI is active, sys.stdout is prompt_toolkit's StdoutProxy which queues writes and injects newlines around each flush(). This causes every \r spinner frame to land on its own line instead of overwriting the previous one, producing visible flickering where the spinner and status bar repeatedly swap positions. The CLI already renders spinner state via a dedicated TUI widget (_spinner_text / get_spinner_text), so KawaiiSpinner's \r-based loop is redundant under StdoutProxy. Detect the proxy and suppress the animation entirely — the thread still runs to preserve start()/stop() semantics. Also removes the 0.4s flush rate-limit workaround that was papering over the same issue, and cleans up the unused _last_flush_time attribute. Salvaged from PR #2908 by Mibayy (fixed _raw -> raw detection, dropped unrelated bundled changes).
Contributor
|
The spinner fix (display.py only) was salvaged and merged in PR #2994. Credit to @Mibayy for identifying the root cause. Fixes applied during salvage:
The unrelated bundled changes (OpenClaw migration, delegation budgets, docs) were not included — please submit those as separate PRs. |
InB4DevOps
pushed a commit
to InB4DevOps/hermes-agent
that referenced
this pull request
Mar 25, 2026
…usResearch#2994) When the CLI is active, sys.stdout is prompt_toolkit's StdoutProxy which queues writes and injects newlines around each flush(). This causes every \r spinner frame to land on its own line instead of overwriting the previous one, producing visible flickering where the spinner and status bar repeatedly swap positions. The CLI already renders spinner state via a dedicated TUI widget (_spinner_text / get_spinner_text), so KawaiiSpinner's \r-based loop is redundant under StdoutProxy. Detect the proxy and suppress the animation entirely — the thread still runs to preserve start()/stop() semantics. Also removes the 0.4s flush rate-limit workaround that was papering over the same issue, and cleans up the unused _last_flush_time attribute. Salvaged from PR NousResearch#2908 by Mibayy (fixed _raw -> raw detection, dropped unrelated bundled changes).
outsourc-e
pushed a commit
to outsourc-e/hermes-agent
that referenced
this pull request
Mar 26, 2026
…usResearch#2994) When the CLI is active, sys.stdout is prompt_toolkit's StdoutProxy which queues writes and injects newlines around each flush(). This causes every \r spinner frame to land on its own line instead of overwriting the previous one, producing visible flickering where the spinner and status bar repeatedly swap positions. The CLI already renders spinner state via a dedicated TUI widget (_spinner_text / get_spinner_text), so KawaiiSpinner's \r-based loop is redundant under StdoutProxy. Detect the proxy and suppress the animation entirely — the thread still runs to preserve start()/stop() semantics. Also removes the 0.4s flush rate-limit workaround that was papering over the same issue, and cleans up the unused _last_flush_time attribute. Salvaged from PR NousResearch#2908 by Mibayy (fixed _raw -> raw detection, dropped unrelated bundled changes).
StreamOfRon
pushed a commit
to StreamOfRon/hermes-agent
that referenced
this pull request
Mar 29, 2026
…usResearch#2994) When the CLI is active, sys.stdout is prompt_toolkit's StdoutProxy which queues writes and injects newlines around each flush(). This causes every \r spinner frame to land on its own line instead of overwriting the previous one, producing visible flickering where the spinner and status bar repeatedly swap positions. The CLI already renders spinner state via a dedicated TUI widget (_spinner_text / get_spinner_text), so KawaiiSpinner's \r-based loop is redundant under StdoutProxy. Detect the proxy and suppress the animation entirely — the thread still runs to preserve start()/stop() semantics. Also removes the 0.4s flush rate-limit workaround that was papering over the same issue, and cleans up the unused _last_flush_time attribute. Salvaged from PR NousResearch#2908 by Mibayy (fixed _raw -> raw detection, dropped unrelated bundled changes).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug
During long agent runs in the interactive CLI, the spinner text and the token-budget status bar visibly flickered and exchanged positions — each spinner frame appeared on its own line instead of overwriting the previous one, making the two UI elements fight for the same screen real estate in a loop.
Root cause:
KawaiiSpinner._animate()writes\r{frame}directly tosys.stdout. When the CLI is active,sys.stdouthas been replaced by prompt_toolkit'sStdoutProxy(viapatch_stdout). That proxy queues writes and injects a newline before/after eachflush()call so the main thread's TUI re-renders cleanly. The side-effect is that the\roverwrite never reaches the same line — every spinner frame becomes a new line and overwrites whatever is below it, including the status bar.There was already a partial workaround (rate-limiting flushes to 0.4s, added in an earlier PR) but it only reduced the frequency of the glitch, not its cause.
Fix
Detect when stdout is a
StdoutProxyand suppress the\r-based animation loop entirely. The CLI already drives a separate TUI widget (get_spinner_text/_spinner_text) that shows the same information through prompt_toolkit's own renderer — there is nothing forKawaiiSpinnerto draw in that context.Detection uses the
_rawsentinel attribute thatStdoutProxysets on itself, with a class-name fallback for forward compatibility.The spinner thread still runs (preserving
start()/stop()semantics); it just sleeps instead of writing frames. The rate-limit workaround is removed since it no longer applies.Demo
https://github.com/user-attachments/assets/screen-recording-2026-03-24-glitch
Files changed
agent/display.py— add_is_patch_stdout_proxy()method; skip\ranimation loop when proxy detected; remove 0.4s flush rate-limit workaround