Skip to content

fix(display): suppress KawaiiSpinner \r animation under patch_stdout#2908

Closed
Mibayy wants to merge 6 commits intoNousResearch:mainfrom
Mibayy:fix/kawaii-spinner-patch-stdout-glitch
Closed

fix(display): suppress KawaiiSpinner \r animation under patch_stdout#2908
Mibayy wants to merge 6 commits intoNousResearch:mainfrom
Mibayy:fix/kawaii-spinner-patch-stdout-glitch

Conversation

@Mibayy
Copy link
Copy Markdown
Contributor

@Mibayy Mibayy commented Mar 25, 2026

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 to sys.stdout. When the CLI is active, sys.stdout has been replaced by prompt_toolkit's StdoutProxy (via patch_stdout). That proxy queues writes and injects a newline before/after each flush() call so the main thread's TUI re-renders cleanly. The side-effect is that the \r overwrite 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 StdoutProxy and 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 for KawaiiSpinner to draw in that context.

Detection uses the _raw sentinel attribute that StdoutProxy sets 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

Screen recording showing the overdraw glitch: spinner text and status bar repeatedly swapping positions during a long FFI porting task. After this fix the status bar stays stable and spinner updates appear only in the TUI widget.

Files changed

  • agent/display.py — add _is_patch_stdout_proxy() method; skip \r animation loop when proxy detected; remove 0.4s flush rate-limit workaround

Hermes 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).
@teknium1
Copy link
Copy Markdown
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 PR checked for _raw attribute on StdoutProxy, but the actual attribute is raw (no underscore). Fixed to use hasattr(out, 'raw') and type(out).__name__ == 'StdoutProxy'.
  • Cleaned up unused _last_flush_time attribute from __init__.

The unrelated bundled changes (OpenClaw migration, delegation budgets, docs) were not included — please submit those as separate PRs.

@teknium1 teknium1 closed this Mar 25, 2026
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants