Skip to content

Stop burning CPU on decoration: idle mascots, hidden polls, lazy sidecar - #425

Merged
milind-soni merged 1 commit into
mainfrom
perf/idle-cpu
Aug 24, 2026
Merged

Stop burning CPU on decoration: idle mascots, hidden polls, lazy sidecar#425
milind-soni merged 1 commit into
mainfrom
perf/idle-cpu

Conversation

@milind-soni

@milind-soni milind-soni commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Why

An iPhone 17 Pro simulator left on the roster overnight sat at ~41% CPU (OpenMausCompanion) with SimRenderServer at ~38% for 8 hours. Two audits traced it: every mounted mascot face was an always-on animation loop — 30fps TimelineView+Canvas per chat row on iOS, a 60fps rAF loop per avatar on desktop whose paused check ran after re-arming — and the computer drawer's preview polls ran at full cadence regardless of visibility or bot activity. On a real phone this is battery, not just heat.

What changed

Motion is now signal, not decoration — animation is opt-in on both platforms.

  • iOS: animated defaults to false in MausAvatar/BotAvatarView/ChatAvatarView. Opt-in sites: roster rows and the chat header only while the bot's state shows activity (listening/thinking/searching/working — new MausState.showsActivity), the open profile sheet, and the needs-you island. Opted-in faces also stop when scenePhase != .active, and the island's comet orbit (the costliest draw path) goes still 30s after appearing, so an unattended approval can't keep a 30fps orbit running until morning.
  • Desktop: the rAF loop parks on a 4Hz wake-poll while paused instead of ticking at display rate; sidebar rows animate only for busy/unread/motion-beat bots (states are keyword-derived, so "working" can be decorative — busy/unread are the real signals); decorative avatar strips are still.
  • Computer drawer: cloud (4s), Local VM (3s), host ScreenCaptureKit (3s), adb devices (2s), and phone screencap (850ms) polls all gate on document.visibilityState and drop to a 30s heartbeat when the bot is idle.
  • Companion sidecar: interface-table poll 5s → 30s (its only recurring wakeup).

Deliberately deferred

Pausing idle Local VM containers (the other big idle burner: a full Linux desktop per bot runs for 8h after last use). docker pause reports paused containers as Running, so the status parser and the turn/screenshot/panel choke points all need to learn the state and unpause lazily — that deserves its own PR against the fresh #332 lease fences, not a rider here. Design notes in the commit message.

Verification

  • Renderer + companion: typecheck clean, 377 tests green locally.
  • iOS: compile-checked by CI's Swift job (behavioral change is prop defaults + a scenePhase gate).
  • Audit reports (two independent agents, one per platform) available on request.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Avatar animations now appear selectively for active, busy, or attention-needed states.
    • Avatars pause when the app is inactive or the page is hidden, while respecting Reduce Motion settings.
    • Activity indicators can remain animated briefly when attention is needed.
  • Performance
    • Background polling and screen previews pause when the page is hidden.
    • Idle devices and bots use less frequent polling, while active bots remain responsive.
    • Address monitoring now checks every 30 seconds instead of every 5 seconds.

An iPhone simulator left on the roster overnight sat at ~41% CPU with
SimRenderServer at ~38% — every chat row mounts a mascot face, and
animation was opt-out. Both platforms now treat motion as signal:

- iOS: animated defaults to false in MausAvatar/BotAvatarView/
  ChatAvatarView; faces opt in only where motion means something (a bot
  actively listening/thinking/searching/working, the open profile sheet,
  the needs-you island). An opted-in face still stops when the app is not
  active (scenePhase), and the island's comet orbit — the costliest draw
  in the app — goes still after 30s so an unattended approval cannot keep
  a 30fps orbit running until morning.
- Desktop: the mascot rAF loop re-armed BEFORE its paused check, so even
  paused faces woke at display rate; paused now parks the loop on a 4Hz
  wake-poll. Sidebar rows animate only for busy/unread/motion-beat bots,
  and the decorative strips are still.
- Computer drawer: every preview poll (cloud 4s, VM 3s, host
  ScreenCaptureKit 3s, adb devices 2s, phone screencap 850ms) now gates
  on document visibility and drops to a 30s heartbeat for idle bots.
- Companion sidecar: the interface-table poll relaxes 5s -> 30s; it is
  the process's only recurring wakeup and networks change on the minutes
  scale.

Deliberately NOT here: pausing idle Local VM containers (the other big
idle burner). docker pause reports Paused containers as Running, so the
status parser and every consumer need to learn the state and unpause
lazily at the turn/screenshot/panel choke points — a follow-up, not a
rider on a decoration patch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openmausbot-docs Ready Ready Preview Aug 24, 2026 5:17pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c49aee0a-cdb0-4733-8169-14397b4cefed

📥 Commits

Reviewing files that changed from the base of the PR and between c2dc783 and 92a6664.

📒 Files selected for processing (13)
  • companion/src/advertise-watch.ts
  • ios/App/AgentProfileView.swift
  • ios/App/BotAvatarView.swift
  • ios/App/ChatListView.swift
  • ios/App/ChatView.swift
  • ios/App/Island.swift
  • ios/App/MausAvatar.swift
  • ios/App/MausFaceData.swift
  • src/components/AndroidDevicePanel.tsx
  • src/components/ComputerPanel.tsx
  • src/components/CursorAvatar.tsx
  • src/components/Sidebar.tsx
  • src/lib/page-visible.ts

📝 Walkthrough

Walkthrough

The change reduces background polling and animation. It adds page visibility handling for web capture, makes avatar animation opt-in, enables activity-based animation in iOS and web views, and increases companion address-watch polling to 30 seconds.

Changes

iOS avatar activity

Layer / File(s) Summary
iOS animation contract
ios/App/BotAvatarView.swift, ios/App/MausFaceData.swift, ios/App/MausAvatar.swift
Avatar animation is now opt-in. Activity states identify listening, thinking, searching, and working. Animation also requires an active scene and remains disabled by Reduce Motion.
Context-specific animation wiring
ios/App/AgentProfileView.swift, ios/App/ChatListView.swift, ios/App/ChatView.swift, ios/App/Island.swift
Profile, chat, and list avatars receive explicit animation state. NeedsYouIsland enables animation and comets for the displayed chat, then disables them after 30 seconds or when the chat changes.

Web visibility-aware polling

Layer / File(s) Summary
Visibility-aware device capture
src/lib/page-visible.ts, src/components/AndroidDevicePanel.tsx
usePageVisible tracks document visibility. Android status polling and screen capture pause while the page is hidden.
Activity-aware computer polling
src/components/ComputerPanel.tsx
Cloud, Local VM, and macOS capture poll faster for busy bots and every 30 seconds for idle bots. Hidden pages stop capture polling.
Paused avatar scheduling
src/components/CursorAvatar.tsx
Paused avatars use a 250 ms wake timer instead of continuous animation frames. Cleanup cancels the timer and animation frame.
Sidebar avatar usage
src/components/Sidebar.tsx
Static sidebar avatars disable animation. Active bot rows enable it for busy, unread, or mascot-motion states.

Companion advertisement polling

Layer / File(s) Summary
Advertisement watcher interval
companion/src/advertise-watch.ts
The default network advertisement check changes from 5 seconds to 30 seconds. The documentation describes the updated cadence.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant usePageVisible
  participant ComputerPanel
  participant CaptureSource
  Browser->>usePageVisible: emit visibilitychange
  usePageVisible->>ComputerPanel: provide visible state
  ComputerPanel->>CaptureSource: poll at activity-based cadence
  ComputerPanel->>CaptureSource: pause polling when hidden
Loading

Suggested reviewers: aivsomkar, willsigmon

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/idle-cpu

Comment @coderabbitai help to get the list of available commands.

@milind-soni
milind-soni merged commit 782a53e into main Aug 24, 2026
7 of 8 checks passed
@milind-soni
milind-soni deleted the perf/idle-cpu branch August 24, 2026 17:23
milind-soni added a commit that referenced this pull request Aug 24, 2026
#425 made sidebar mascots mount paused — and exposed that the parked
loop never draws: the SVG layers hold no expression until the first
draw() positions them, so an idle bot's avatar rendered blank. The
paused branch now paints the still face once, re-painting only when
what it shows changes (state, pinned expression, gradient), then parks
on the same 4Hz wake-poll. Animation stays opt-in; the resting pose is
simply visible again.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
milind-soni added a commit that referenced this pull request Aug 25, 2026
…#442)

* feat(ui): composer attach button and per-bot permission mode selector

Adds a paperclip button that opens a file picker feeding the shared
attachment pipeline, and an Approve-for-me / Ask-for-approval pill that
toggles autoApprove per bot without opening settings. The composer is
restructured into two rows — text on top, controls below — matching
common chat-app layouts.

* feat(ui): remove the Always allow button from approval cards

The per-bot permission mode selector in the composer (Ask for approval /
Approve for me) is the single mechanism for reducing approval prompts;
the per-program Always-allow grant duplicated it with a worse model.
Allow-once and Deny remain.

* Paint the resting face when a mascot mounts paused (#444)

#425 made sidebar mascots mount paused — and exposed that the parked
loop never draws: the SVG layers hold no expression until the first
draw() positions them, so an idle bot's avatar rendered blank. The
paused branch now paints the still face once, re-painting only when
what it shows changes (state, pinned expression, gradient), then parks
on the same 4Hz wake-poll. Animation stays opt-in; the resting pose is
simply visible again.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(composio): preload connected account state (#445)

* chore(release): bump version to 0.1.33 (#446)

* Add portable BotMRR Markdown playbooks (#426)

* Add portable BotMRR package installs

* Keep package link tests out of Vitest discovery

* Add universal BotMRR Markdown imports

* Document portable team playbooks

* fix(composio): accept empty authorization bodies (#451)

* ci: stop retaining disposable Linux packages (#452)

* fix(linux): harden Ubuntu upgrades and Xorg local control (#346)

* fix(linux): fail closed on unsafe local control startup

* fix(desktop): keep optional connected apps off startup path

* fix(linux): repair inherited DEB upgrade permissions

* docs(linux): document the local-control safety hold

* fix(ci): configure dependencies in DEB upgrade smoke

* fix(linux): configure DEB Chromium sandbox

* fix(linux): restore safe Xorg local control

* fix(desktop): close review security and refresh races

* fix(linux): isolate local control safety opt-in

* docs(linux): explain private CUA cursor behavior

* fix(linux): clean CUA runtime on termination signals

* fix(linux): reap stale AppImage CUA stages

* docs(linux): clarify release CUA coverage

* test(linux): preserve packaged smoke diagnostics

* ci(linux): normalize runner package parent

* fix(linux): close final Ubuntu review gaps

* fix(ci): fail closed before Ubuntu package install

* fix(composio): enforce broker URL parity

* Let Antigravity models control computers (mount the computer MCP) (#447)

* Let Antigravity models control computers (mount the computer MCP)

agy has no per-turn MCP flag and provably no project-level MCP config
(1.1.19: embedded docs list only the global
~/.gemini/config/mcp_config.json and per-plugin files; agy mcp list
ignores .gemini/{settings,mcp_config}.json in the cwd). So each turn
upserts one key — openmausbot-computer — into the global file right
before the spawn, preserving every other byte of the user's config and
tolerating malformed JSON, and removes that key on the next
computer-less turn so tools and box/control tokens cannot leak into
later turns or the user's own agy sessions.

Cloud boxes mount OpenMausBot's REST-to-MCP computer proxy (resolved
via SPAWNED_PROXIES — never relative to the module, the 0.1.24
lesson); Local VM and VPS connections pass through as the stdio Cua
command they already are.

computerMcp is advertised only by full-auto instances: print mode has
no interactive approval channel, and outside
--dangerously-skip-permissions agy auto-denies tools that would
prompt, so a non-fullAuto mount could never fire. localComputerMcp
stays unset — the host desktop requires per-action human approval,
which print mode cannot deliver in any mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: isolate Antigravity computer mounts

* fix: reap settled Antigravity children

* fix: keep MCP lease until child exit

* fix: preserve Antigravity MCP ownership

* fix: clear failed Antigravity turns

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Milind Soni <46266943+milind-soni@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Kesley David <39314443+KesleyDavid@users.noreply.github.com>
Co-authored-by: milind-soni <milindsoni201@gmail.com>
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.

1 participant