Skip to content

fix(desktop): park the bot face clock when idle and tear it down on disable - #88543

Merged
teknium1 merged 2 commits into
mainfrom
fix/bots-face-clock-dormancy
Aug 17, 2026
Merged

teknium1 merged 2 commits into
mainfrom
fix/bots-face-clock-dormancy

Conversation

@teknium1

Copy link
Copy Markdown
Collaborator

Summary

The bundled Bots plugin's face-animation clock no longer burns CPU when idle and actually stops when the plugin is disabled. Salvages #88219 by @digitalbase (visible-faces-only painting via IntersectionObserver + 15 fps cap, authorship preserved) and completes it with dormancy and teardown.

Root cause of the field report (Desktop pinned at 100% CPU, persisting after disabling Bots): the clock started at plugin load, ran a requestAnimationFrame loop forever, rescanned the whole document — including every shadow root via querySelectorAll('*') — every second, and repainted all mounted faces at 60 fps. Disabling the plugin never tore any of it down.

Changes

  • apps/desktop/src/plugins/hermes-bots/plugin.js:
    • (salvaged fix(desktop): bound bot face animation work #88219) observe faces, paint only viewport-visible ones, cap at 15 fps
    • dormancy: the loop parks (stops scheduling frames, no 1 Hz document scan) when no faces are mounted or none are visible; a mounting BotFace or a face scrolling into view wakes it
    • teardown: register() hooks ctx.onDispose(stopFaceClock) — disabling/reloading the plugin cancels the frame, disconnects the observer, drops cached nodes, clears window.__hbFaceClock
  • tests/face-clock.test.mjs: behavioral park/wake/stop coverage via a vm-extracted clock harness (verified to fail against the pre-fix source)

Validation

Before After
No visible faces 60 fps repaint + 1 Hz full-document shadow-root scan, forever clock parked, zero frames scheduled
Plugin disabled loop runs until app restart rAF cancelled, observer disconnected, handle cleared
Plugin tests 143 160, all pass (node --test)

Field report: Discord (rahlquist) — Desktop at 100% CPU after this morning's update, unchanged by disabling Bots without a restart. Likely also issue #73082.

Credit: @digitalbase for the visibility/15 fps work in #88219, cherry-picked with authorship preserved. Diagnosis by @helix4u.

Infographic

Bot face clock idle CPU fix

digitalbase and others added 2 commits August 17, 2026 10:46
…lugin disable

Follow-up to the salvaged #88219 visibility/15fps work:

- Dormancy: the rAF loop stops scheduling frames when no faces are mounted
  or none are visible, instead of running the 1Hz whole-document shadow-root
  scan forever. A mounting BotFace or a face scrolling into view wakes it.
- Teardown: register() now hooks ctx.onDispose so disabling the Bots plugin
  (or a hot reload) cancels the animation frame, disconnects the
  IntersectionObserver, drops cached nodes, and clears window.__hbFaceClock.
  Previously the loop ran until app restart even with the plugin disabled.
- Behavioral tests for park/wake/stop via a vm-extracted clock harness,
  verified to fail against the pre-fix source.
@github-actions

github-actions Bot commented Aug 17, 2026 •

Copy link
Copy Markdown

૮ >ﻌ< ა ci review

ran on 0207ab9 — fix(desktop): park the bot face clock when idle and tear it

⚠️ Warnings

OSV vulnerability scan · View job

7 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.

@teknium1
teknium1 merged commit ce1f5dd into main Aug 17, 2026
39 checks passed
@teknium1
teknium1 deleted the fix/bots-face-clock-dormancy branch August 17, 2026 17:58
teknium1 added a commit that referenced this pull request Aug 17, 2026
…imations

Desktop shipped the same bug class four times in one week: a decorative
animation loop that never sleeps (bots face clock #88543, pixel egg #88406,
diffusion placeholder #88564, plus the #77651 hidden-renderer wave). Each fix
hand-rolled the same four behaviors. This extracts them into one helper in
src/lib/budgeted-loop.ts:

- fps budget (default 15) on top of rAF
- observability pause via the existing createRendererLoopPauseController
- idle dormancy: idleWhen() true after a draw parks the loop with zero
  pending work until wake() — the piece every hand-rolled loop forgot
- teardown: dispose() cancels the frame, disposes the controller, and makes
  wake() a no-op

DiffusionCanvas migrates onto it as the first consumer (net -40 lines at the
call site); its existing scheduling/budget/instance-cap tests pass unchanged
against the migrated implementation. Helper suite covers budget, pause,
park/wake, dormancy-survives-focus-churn, and dispose idempotency; sabotage
run (budget+dormancy stripped) fails 4/5.
lisajlau pushed a commit to lisajlau/hermes-agent that referenced this pull request Aug 20, 2026
…imations

Desktop shipped the same bug class four times in one week: a decorative
animation loop that never sleeps (bots face clock NousResearch#88543, pixel egg NousResearch#88406,
diffusion placeholder NousResearch#88564, plus the NousResearch#77651 hidden-renderer wave). Each fix
hand-rolled the same four behaviors. This extracts them into one helper in
src/lib/budgeted-loop.ts:

- fps budget (default 15) on top of rAF
- observability pause via the existing createRendererLoopPauseController
- idle dormancy: idleWhen() true after a draw parks the loop with zero
  pending work until wake() — the piece every hand-rolled loop forgot
- teardown: dispose() cancels the frame, disposes the controller, and makes
  wake() a no-op

DiffusionCanvas migrates onto it as the first consumer (net -40 lines at the
call site); its existing scheduling/budget/instance-cap tests pass unchanged
against the migrated implementation. Helper suite covers budget, pause,
park/wake, dormancy-survives-focus-churn, and dispose idempotency; sabotage
run (budget+dormancy stripped) fails 4/5.
bobaba76 pushed a commit to bobaba76/hermes-agent that referenced this pull request Aug 27, 2026
…imations

Desktop shipped the same bug class four times in one week: a decorative
animation loop that never sleeps (bots face clock NousResearch#88543, pixel egg NousResearch#88406,
diffusion placeholder NousResearch#88564, plus the NousResearch#77651 hidden-renderer wave). Each fix
hand-rolled the same four behaviors. This extracts them into one helper in
src/lib/budgeted-loop.ts:

- fps budget (default 15) on top of rAF
- observability pause via the existing createRendererLoopPauseController
- idle dormancy: idleWhen() true after a draw parks the loop with zero
  pending work until wake() — the piece every hand-rolled loop forgot
- teardown: dispose() cancels the frame, disposes the controller, and makes
  wake() a no-op

DiffusionCanvas migrates onto it as the first consumer (net -40 lines at the
call site); its existing scheduling/budget/instance-cap tests pass unchanged
against the migrated implementation. Helper suite covers budget, pause,
park/wake, dormancy-survives-focus-churn, and dispose idempotency; sabotage
run (budget+dormancy stripped) fails 4/5.
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…imations

Desktop shipped the same bug class four times in one week: a decorative
animation loop that never sleeps (bots face clock NousResearch#88543, pixel egg NousResearch#88406,
diffusion placeholder NousResearch#88564, plus the NousResearch#77651 hidden-renderer wave). Each fix
hand-rolled the same four behaviors. This extracts them into one helper in
src/lib/budgeted-loop.ts:

- fps budget (default 15) on top of rAF
- observability pause via the existing createRendererLoopPauseController
- idle dormancy: idleWhen() true after a draw parks the loop with zero
  pending work until wake() — the piece every hand-rolled loop forgot
- teardown: dispose() cancels the frame, disposes the controller, and makes
  wake() a no-op

DiffusionCanvas migrates onto it as the first consumer (net -40 lines at the
call site); its existing scheduling/budget/instance-cap tests pass unchanged
against the migrated implementation. Helper suite covers budget, pause,
park/wake, dormancy-survives-focus-churn, and dispose idempotency; sabotage
run (budget+dormancy stripped) fails 4/5.
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