fix(cli): first repaint no longer swallowed when monotonic clock is small
time.monotonic() counts from an arbitrary epoch (boot on Linux). Two CLI repaint throttles used 0.0 as the never-fired sentinel, so on a freshly booted VM (CI runners, containers) now - 0.0 < min_interval suppressed the FIRST repaint ever requested: - _schedule_focus_regain_redraw: min_interval=60 suppressed the first focus-regain redraw whenever uptime < 60s — the exact failure in CI run 32494557030 (test_focus_regain_redraw_is_rate_limited, both attempts red on a fresh runner, green everywhere else). - _invalidate: same 0.0 sentinel; a first spinner/stream repaint inside the first 250ms of uptime was droppable the same way. Both now use None as the never-fired sentinel. Regression test pins monotonic()=3.0 with min_interval=60 and asserts the first redraw fires.
This commit is contained in:
@@ -2837,7 +2837,7 @@ class HermesCLI(CLIProcessNotificationsMixin, CLIAgentSetupMixin, CLICommandsMix
|
|||||||
getattr(self, "_write_terminal_breadcrumb", lambda: None)()
|
getattr(self, "_write_terminal_breadcrumb", lambda: None)()
|
||||||
|
|
||||||
self._history_file = _hermes_home / ".hermes_history"
|
self._history_file = _hermes_home / ".hermes_history"
|
||||||
self._last_invalidate: float = 0.0 # throttles UI repaints
|
self._last_invalidate: float | None = None # throttles UI repaints (None = never; monotonic epoch is arbitrary)
|
||||||
self._init_ui_state()
|
self._init_ui_state()
|
||||||
|
|
||||||
def _init_session_store(self):
|
def _init_session_store(self):
|
||||||
|
|||||||
@@ -90,7 +90,11 @@ class CLITerminalMixin:
|
|||||||
if getattr(self, "_terminal_io_broken", False) or getattr(self, "_resize_recovery_pending", False):
|
if getattr(self, "_terminal_io_broken", False) or getattr(self, "_resize_recovery_pending", False):
|
||||||
return
|
return
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
if hasattr(self, "_app") and self._app and (now - getattr(self, "_last_invalidate", 0.0)) >= min_interval:
|
# None sentinel, not 0.0 — monotonic's epoch is arbitrary (boot on Linux), so on
|
||||||
|
# a fresh VM ``now`` can be < min_interval and a 0.0 sentinel would swallow the
|
||||||
|
# first repaint (same class as _schedule_focus_regain_redraw below).
|
||||||
|
last = getattr(self, "_last_invalidate", None)
|
||||||
|
if hasattr(self, "_app") and self._app and (last is None or now - last >= min_interval):
|
||||||
self._last_invalidate = now
|
self._last_invalidate = now
|
||||||
self._app_invalidate(self._app, "invalidate", swallow=False)
|
self._app_invalidate(self._app, "invalidate", swallow=False)
|
||||||
|
|
||||||
@@ -138,7 +142,13 @@ class CLITerminalMixin:
|
|||||||
stacks on stale content (#60920, #25337); terminals without it never emit ``CSI I``.
|
stacks on stale content (#60920, #25337); terminals without it never emit ``CSI I``.
|
||||||
"""
|
"""
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
if now - getattr(self, "_last_focus_regain_redraw", 0.0) < min_interval:
|
# Sentinel is None, not 0.0: time.monotonic() counts from an arbitrary epoch
|
||||||
|
# (boot on Linux). On a fresh VM (CI runners, containers) ``now`` can be smaller
|
||||||
|
# than ``min_interval``, and ``now - 0.0 < min_interval`` would suppress the
|
||||||
|
# FIRST redraw ever requested (CI run 32494557030: uptime < 60s made the
|
||||||
|
# min_interval=60 rate-limit test fail both attempts).
|
||||||
|
last = getattr(self, "_last_focus_regain_redraw", None)
|
||||||
|
if last is not None and now - last < min_interval:
|
||||||
return
|
return
|
||||||
self._last_focus_regain_redraw = now
|
self._last_focus_regain_redraw = now
|
||||||
self._force_full_redraw()
|
self._force_full_redraw()
|
||||||
|
|||||||
@@ -426,3 +426,21 @@ class TestFocusRegainRedraw:
|
|||||||
bare_cli._schedule_focus_regain_redraw(min_interval=0.0)
|
bare_cli._schedule_focus_regain_redraw(min_interval=0.0)
|
||||||
|
|
||||||
assert calls == ["redraw", "redraw"]
|
assert calls == ["redraw", "redraw"]
|
||||||
|
|
||||||
|
def test_first_redraw_fires_even_with_small_monotonic_clock(
|
||||||
|
self, bare_cli, monkeypatch
|
||||||
|
):
|
||||||
|
"""The first-ever redraw must fire regardless of monotonic's epoch.
|
||||||
|
|
||||||
|
time.monotonic() starts from an arbitrary point (boot on Linux); on a
|
||||||
|
fresh CI VM it can be smaller than min_interval. The old 0.0 sentinel
|
||||||
|
turned that into ``now - 0.0 < min_interval`` and silently swallowed
|
||||||
|
the FIRST redraw (CI run 32494557030 — both retry attempts failed).
|
||||||
|
"""
|
||||||
|
calls = []
|
||||||
|
bare_cli._force_full_redraw = lambda: calls.append("redraw")
|
||||||
|
monkeypatch.setattr(cli_mod.time, "monotonic", lambda: 3.0)
|
||||||
|
|
||||||
|
bare_cli._schedule_focus_regain_redraw(min_interval=60.0)
|
||||||
|
|
||||||
|
assert calls == ["redraw"]
|
||||||
|
|||||||
Reference in New Issue
Block a user