acpmux: host load sample and a muted host notice for Cloud right-sizing

TE
4d ago
+753-4

What and why

cmux NEXT Cloud machines (Linux VMs running the acpmux daemon) get right-sized automatically by the server (manaflow-ai/cmux-platform). The server takes one load sample a minute by running a command on the machine through the provider's exec. After a resize it tells the chats with one muted line. This PR is the machine side of both.

Leo, 2026-10-31: "we ofc wanted to have the thing be having snapshots of whatever vms to do the right-sizing" and "this goes on automatically right?"

A. acpmux machine load --json (also acpmux host load)

Prints one JSON object with the machine's load now and exits 0. It only reads: it never starts a daemon. If the running daemon doesn't answer within 1.5 s, busy/localTurns are left out. Every field except at is left out when it can't be read. Off Linux it prints at, plus disk on any Unix.

{
  "at": 1793500000000,
  "cpuBusy": 0.62,
  "cpuSomeAvg60": 12.4,
  "memSomeAvg60": 3.1,
  "memFullAvg60": 0.2,
  "memAvailableFraction": 0.18,
  "swapUsedFraction": 0.4,
  "oomKills": 1,
  "diskFreeBytes": 21474836480,
  "diskFreeFraction": 0.31,
  "busy": true,
  "localTurns": 2,
  "heavy": { "kind": "build", "need": { "vcpu": 8, "memGiB": 15 } }
}
  • cpuBusy: two /proc/stat cpu reads 1 s apart (idle = idle + iowait).
  • cpuSomeAvg60, memSomeAvg60, memFullAvg60: PSI avg60 from /proc/pressure/{cpu,memory}.
  • memAvailableFraction, swapUsedFraction: /proc/meminfo. Swap is left out when SwapTotal is 0.
  • oomKills: /proc/vmstat oom_kill (cumulative).
  • diskFreeBytes, diskFreeFraction: statvfs of $HOME (space available to an unprivileged user, over the total).
  • busy, localTurns: daemon status host_pressure.local_turns.
  • heavy: one pass over /proc/*/comm and cmdline (argv[0..3]). The heaviest known build or test tool wins. The table is one declarative const (host_load.rs HEAVY) taken from cmux#26617's sizing model, and counts on top of the session's own 2 GiB / 2 cores: cargo build/clippy and rustc → build 15 GiB / 8 cores (cargo test → test); swift build/swift-frontend → 13 / 4, swift test → 11 / 6; go build/test, make, cmake, ninja, gcc/clang (and cc1/cc1plus) → 8 / 8; bun/npm/pnpm/yarn test, vitest, jest, playwright → test 2 / 6; tsc and vite build → build 2 / 6. A node or bun script counts as its tool (node …/vitest.mjs).

The new /proc parsers sit beside the existing ones in host_pressure_probe.rs and are pure functions tested on literal file text. The classifier is a pure function over (comm, argv).

B. acpmux machine notice --text "…" → muted notice in every live chat

  • New RPC _acpmux/host_notice {text} (server/host_notice.rs). It refuses Web clients, the same way vm_host does. The text is trimmed and must be one line of at most 300 characters. The handler appends the mux record host_notice {text, source: "cloud"} to every session on this daemon that isn't Closed, and returns {"appended": n}.
  • host_notice is added to TRANSCRIPT_MUX_KINDS, so clients attached to the transcript get it. It is also in docs/acpmux-schema.json (muxEventKinds, EventKinds, the record type and the method) and in the protocol IR (spec/acpmux-protocol.json, with the web methods.ts regenerated). It is excluded from peer session forwarding.
  • Webview (direct.ts): a host_notice record with non-empty text becomes the row {id: "host-notice-<seq>", kind: "notice", text}, which NoticeRow draws muted.
  • The CLI talks to the running daemon's socket and never starts a daemon (no daemon means no chats to tell).

Tests (cmux-ci, Linux pool)

  • cargo test --locked -p acpmux --lib -- host_pressure host_load host_notice hub::paging protocol:: help_examples command::tests: 53 passed, 0 failed. Fleet job 87a57c01da760559563011e1. This covers the new parsers, the classifier, the JSON shape, the host_notice journal and transcript test, the paging kinds, the protocol IR being current, and the help examples parsing.
  • cargo clippy --locked -p acpmux --all-targets -- -D warnings: clean. Fleet job dad636f773a4d2591aa2d586.
  • bun test test/agent-pane-host-notice.test.tsx test/agent-pane-policy-unenforced.test.tsx: 5 passed. Fleet job cd3366470d2327dfcdc850a7.

Size: about 700 changed lines, a bit over the ~600 target. Roughly half is tests, schema text and the regenerated protocol files. The two halves share the PR because Cloud's right-sizing loop needs both.

Risk: small and additive. There is a new read-only CLI and a new local-only RPC that appends one record per live chat. No Swift.

Agent session: agent session

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Adds the machine side of cmux Cloud's right-sizing loop: acpmux machine load --json takes one load sample a minute, and acpmux machine notice --text adds one muted line to every live chat after a resize.

Host load

  • machine load --json prints one JSON object with CPU busy share, PSI avg60, available memory, swap, OOM kills, free disk, and the heaviest running build or test; it reads /proc only and never starts a daemon.
  • Fields it can't read are left out; busy/localTurns come from a running daemon if it answers within 1.5 s.
  • The classifier reads comm and argv and counts bun x, bunx, and npx wrappers as the tool they run; a node or bun script counts as its tool.

Host notice

  • machine notice --text appends a host_notice transcript record to every non-closed session on the daemon via the new local-only _acpmux/host_notice RPC.
  • The agent pane draws it as a muted notice row; Web clients and peer forwarding are refused, and the text must be one line of at most 300 characters.
  • A notice that arrives mid-reply ends the current assistant or activity segment, so later lines sit below it.