Repository navigation
acpmux: host load sample and a muted host notice for Cloud right-sizing - #30437
Conversation
There was a problem hiding this comment.
5 issues found across 22 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="webviews/test/agent-pane-host-notice.test.tsx">
<violation number="1" location="webviews/test/agent-pane-host-notice.test.tsx:96">
P3: This test does not verify the muted pane rendering promised by its name. Render the pane and assert the host notice produces the muted notice element, or rename this test to make its reducer-only coverage explicit and add a separate UI assertion.</violation>
</file>
<file name="cmux-tui/crates/acpmux/src/host_load.rs">
<violation number="1" location="cmux-tui/crates/acpmux/src/host_load.rs:190">
P2: The disk sample ignores `ACPMUX_HOME` and stats `$HOME` instead. Use `crate::config::home()` so a custom acpmux state volume reports the disk that actually hosts the daemon's machine data.</violation>
</file>
<file name="cmux-tui/crates/acpmux/src/host_pressure_probe.rs">
<violation number="1" location="cmux-tui/crates/acpmux/src/host_pressure_probe.rs:79">
P2: `map_while` silently truncates at the first malformed CPU counter and still returns a partial sample. Reject the whole aggregate line when any counter cannot be parsed, otherwise a damaged or changed `/proc/stat` line produces a plausible but incorrect `cpuBusy` value.</violation>
</file>
<file name="cmux-tui/crates/acpmux/src/server/host_notice.rs">
<violation number="1" location="cmux-tui/crates/acpmux/src/server/host_notice.rs:46">
P2: `append_all` reports every selected session as appended even when persistence fails, so a notice can appear live but disappear from replay while the caller receives success. Use `append_stored` and count only successful writes.</violation>
<violation number="2" location="cmux-tui/crates/acpmux/src/server/host_notice.rs:52">
P1: This guard does not enforce the method's local-only contract: an authenticated peer can add a notice to every live session on this daemon. Require `Control::Local` so only the Unix socket and LocalApp can invoke it.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| } | ||
|
|
||
| pub(super) async fn handle(hub: &Arc<Hub>, conn: &Conn, params: &Value) -> Result<Value, RpcError> { | ||
| if super::remote_guard::control_of(conn.origin, conn.auth, params) == Control::Web { |
There was a problem hiding this comment.
P1: This guard does not enforce the method's local-only contract: an authenticated peer can add a notice to every live session on this daemon. Require Control::Local so only the Unix socket and LocalApp can invoke it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/server/host_notice.rs, line 52:
<comment>This guard does not enforce the method's local-only contract: an authenticated peer can add a notice to every live session on this daemon. Require `Control::Local` so only the Unix socket and LocalApp can invoke it.</comment>
<file context>
@@ -0,0 +1,81 @@
+}
+
+pub(super) async fn handle(hub: &Arc<Hub>, conn: &Conn, params: &Value) -> Result<Value, RpcError> {
+ if super::remote_guard::control_of(conn.origin, conn.auth, params) == Control::Web {
+ return Err(RpcError::new(-32000, "a host notice is added only from this machine"));
+ }
</file context>
| /// `idle` and `iowait`; `guest` and `guest_nice` are already inside `user` and `nice`. | ||
| pub fn parse_proc_stat_cpu(text: &str) -> Option<(u64, u64)> { | ||
| let line = text.lines().find(|l| l.split_whitespace().next() == Some("cpu"))?; | ||
| let n: Vec<u64> = line.split_whitespace().skip(1).map_while(|w| w.parse().ok()).collect(); |
There was a problem hiding this comment.
P2: map_while silently truncates at the first malformed CPU counter and still returns a partial sample. Reject the whole aggregate line when any counter cannot be parsed, otherwise a damaged or changed /proc/stat line produces a plausible but incorrect cpuBusy value.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/host_pressure_probe.rs, line 79:
<comment>`map_while` silently truncates at the first malformed CPU counter and still returns a partial sample. Reject the whole aggregate line when any counter cannot be parsed, otherwise a damaged or changed `/proc/stat` line produces a plausible but incorrect `cpuBusy` value.</comment>
<file context>
@@ -50,6 +50,45 @@ pub fn parse_vmstat_pswpout(text: &str) -> Option<u64> {
+/// `idle` and `iowait`; `guest` and `guest_nice` are already inside `user` and `nice`.
+pub fn parse_proc_stat_cpu(text: &str) -> Option<(u64, u64)> {
+ let line = text.lines().find(|l| l.split_whitespace().next() == Some("cpu"))?;
+ let n: Vec<u64> = line.split_whitespace().skip(1).map_while(|w| w.parse().ok()).collect();
+ let idle = n.get(3)?.saturating_add(n.get(4).copied().unwrap_or(0));
+ let total = n.iter().take(8).fold(0u64, |sum, j| sum.saturating_add(*j));
</file context>
| let n: Vec<u64> = line.split_whitespace().skip(1).map_while(|w| w.parse().ok()).collect(); | |
| let n: Vec<u64> = line.split_whitespace().skip(1).map(|w| w.parse().ok()).collect::<Option<_>>()?; |
| hub.append(s, "mux", KIND, json!({"text": text, "source": "cloud"})); | ||
| } | ||
| live.len() | ||
| } |
There was a problem hiding this comment.
P2: append_all reports every selected session as appended even when persistence fails, so a notice can appear live but disappear from replay while the caller receives success. Use append_stored and count only successful writes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/server/host_notice.rs, line 46:
<comment>`append_all` reports every selected session as appended even when persistence fails, so a notice can appear live but disappear from replay while the caller receives success. Use `append_stored` and count only successful writes.</comment>
<file context>
@@ -0,0 +1,81 @@
+ let live: Vec<_> =
+ hub.sessions().into_iter().filter(|s| s.meta().status != SessionStatus::Closed).collect();
+ for s in &live {
+ hub.append(s, "mux", KIND, json!({"text": text, "source": "cloud"}));
+ }
+ live.len()
</file context>
| hub.append(s, "mux", KIND, json!({"text": text, "source": "cloud"})); | |
| } | |
| live.len() | |
| } | |
| live.iter() | |
| .filter(|s| { | |
| hub.append_stored(s, "mux", KIND, json!({"text": text, "source": "cloud"})) | |
| }) | |
| .count() |
| #[cfg(unix)] | ||
| fn disk(load: &mut Load) { | ||
| use std::os::unix::ffi::OsStrExt; | ||
| let Some(home) = std::env::var_os("HOME") else { return }; |
There was a problem hiding this comment.
P2: The disk sample ignores ACPMUX_HOME and stats $HOME instead. Use crate::config::home() so a custom acpmux state volume reports the disk that actually hosts the daemon's machine data.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/host_load.rs, line 190:
<comment>The disk sample ignores `ACPMUX_HOME` and stats `$HOME` instead. Use `crate::config::home()` so a custom acpmux state volume reports the disk that actually hosts the daemon's machine data.</comment>
<file context>
@@ -0,0 +1,327 @@
+#[cfg(unix)]
+fn disk(load: &mut Load) {
+ use std::os::unix::ffi::OsStrExt;
+ let Some(home) = std::env::var_os("HOME") else { return };
+ let Ok(path) = std::ffi::CString::new(home.as_bytes()) else { return };
+ // SAFETY: an all-zero statvfs is a valid value of this plain C struct.
</file context>
|
|
||
| const GREW = "Machine grew to 4 vCPU / 8 GiB (it ran out of memory)"; | ||
|
|
||
| test("a host notice is a muted notice row where acpmux logged it", async () => { |
There was a problem hiding this comment.
P3: This test does not verify the muted pane rendering promised by its name. Render the pane and assert the host notice produces the muted notice element, or rename this test to make its reducer-only coverage explicit and add a separate UI assertion.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/test/agent-pane-host-notice.test.tsx, line 96:
<comment>This test does not verify the muted pane rendering promised by its name. Render the pane and assert the host notice produces the muted notice element, or rename this test to make its reducer-only coverage explicit and add a separate UI assertion.</comment>
<file context>
@@ -0,0 +1,123 @@
+
+const GREW = "Machine grew to 4 vCPU / 8 GiB (it ran out of memory)";
+
+test("a host notice is a muted notice row where acpmux logged it", async () => {
+ const daemon = fakeDaemon(
+ record([
</file context>
47d10bd to
5b80715
Compare
`acpmux machine load --json` (also `host load`) prints one JSON object of this
machine's load now: CPU busy share over 1 s, CPU and memory PSI avg60, available
memory, swap used, OOM kills, free disk under $HOME, whether a turn runs on the
daemon, and a running build or test with what it needs. Reads only; never starts
a daemon; off Linux it prints what it can (always `at`).
`acpmux machine notice --text TEXT` asks the daemon (`_acpmux/host_notice`) to
append a mux `host_notice` record {text, source: "cloud"} to every live chat; it
is a transcript kind and the agent pane draws it as a muted notice row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
…tice splits a reply Review follow-ups at 5b80715: the two /proc/stat reads now bracket the one-second sleep alone; `bun x TOOL`, `bunx` and `npx` count as TOOL; a host notice that comes mid-reply ends the assistant segment, so later text goes below it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
…activity segment too Review at 9c4a7c6 (gpt-6.1-sol xhigh): the constant is dead code off Linux, which -D warnings refuses on macOS; a thought or tool call after a notice joined the activity row above it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
|
Review (codex gpt-6.1-sol xhigh, router-review): SHIP at c559cd0 Visual verdict: no images supplied (range review); the visual verdict must come separately
Protocol generation matched and TypeScript syntax checks passed. Bun and Rust suites were unavailable. Follow-ups:
|
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Gallery before/after for
States
Pick other states with a red: cmux-next checks (god files, concurrency, crash safety, l10n); NEW: Shipped in:
|
|
Found 18 test failures on Blacksmith runners: Failures
...and 8 more test failures. View all on Blacksmith: run 41289308650, run 41289308372
|
|
Review (codex, router-review): SHIP at 5b80715 Visual verdict: no images; Leo tries the build
Focused local tests were unavailable because Follow-ups:
|
7e4772d to
9c4a7c6
Compare
|
Conflict wave: the branch now has a new history at 9c4a7c6 (rebased onto feat), so my merge 7e4772d is not on it and I stop work on this PR. Your head merges clean with feat-cmux-next. One clippy red is this PR's own: The other errors in that clippy run are base findings on feat, not from this PR: |
Bugbot needs on-demand usage enabledBugbot uses usage-based billing for this team and requires on-demand usage to be enabled. A team admin can enable on-demand usage in the Cursor dashboard. |
|
For Leo (teamleaderleo): this PR made "No process scanning" in cmux-next checks red on feat-cmux-next (host_load.rs heavy_now walks /proc). Fix: #31652. The daemon now names |
…nts run, not a /proc scan (#30437) (#31652) #30437 (60b7d7d) found `heavy` (a running build or test and what it needs) by walking /proc/*/comm and cmdline (host_load.rs:242 read_dir("/proc")), so "No process scanning" in cmux-next checks fails on every push. cmux-next never scans processes (plans/cmux-next/pty-holder.md 4): holders and agents report themselves. The daemon already sees every command its agents run, so it names `heavy` itself: - TurnActivity (hub/activity.rs) keeps, for each tool call in flight, the build or test its shell command runs (rawInput.command: Claude Code's Bash script or Codex's argv), from the call or a later update; it ends with the call. - Hub::running_heavy_read (hub/processes.rs) reads the commands of the session's running background processes from the table it holds. - daemon status's host_pressure gets `heavy`: the heaviest of those over the daemon's sessions (same HEAVY table, same heavier rule, same JSON shape). - acpmux machine load takes `heavy` from the daemon status it already asks for `busy` and `localTurns`; host_load::classify_command reads a shell command's simple commands (cd ... &&, NAME=value, nice/env/time wrappers, sh -c SCRIPT) with the existing classify. Behavior: the field and its values stay. What counts is now a build or test that the daemon's agents run (in flight or in the background), not one any process on the machine runs; on a Cloud machine those are the agents' work that right-sizing is for. It is now also there off Linux, and left out when the daemon does not answer (as busy). check-no-process-scan.py: red at origin/feat-cmux-next 60e7952 (host_load.rs 1 line, baseline 0), ok here. Tests: host_load a_shell_command_is_known_from_its_simple_commands, a_tool_call_runs_its_execute_command, heavy_comes_from_daemon_status; activity a_build_in_flight_is_heavy_until_it_ends. Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
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(alsoacpmux 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/localTurnsare left out. Every field exceptatis left out when it can't be read. Off Linux it printsat, 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/statcpureads 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/vmstatoom_kill(cumulative).diskFreeBytes,diskFreeFraction: statvfs of$HOME(space available to an unprivileged user, over the total).busy,localTurns:daemon statushost_pressure.local_turns.heavy: one pass over/proc/*/commandcmdline(argv[0..3]). The heaviest known build or test tool wins. The table is one declarative const (host_load.rsHEAVY) 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
/procparsers sit beside the existing ones inhost_pressure_probe.rsand 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_acpmux/host_notice {text}(server/host_notice.rs). It refuses Web clients, the same wayvm_hostdoes. The text is trimmed and must be one line of at most 300 characters. The handler appends the mux recordhost_notice{text, source: "cloud"}to every session on this daemon that isn't Closed, and returns{"appended": n}.host_noticeis added toTRANSCRIPT_MUX_KINDS, so clients attached to the transcript get it. It is also indocs/acpmux-schema.json(muxEventKinds,EventKinds, the record type and the method) and in the protocol IR (spec/acpmux-protocol.json, with the webmethods.tsregenerated). It is excluded from peer session forwarding.direct.ts): ahost_noticerecord with non-empty text becomes the row{id: "host-notice-<seq>", kind: "notice", text}, which NoticeRow draws muted.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 job87a57c01da760559563011e1. 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 jobdad636f773a4d2591aa2d586.bun test test/agent-pane-host-notice.test.tsx test/agent-pane-policy-unenforced.test.tsx: 5 passed. Fleet jobcd3366470d2327dfcdc850a7.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
Need help on this PR? Tag
@codesmith-botwith what you need.Summary by cubic
Adds the machine side of cmux Cloud's right-sizing loop:
acpmux machine load --jsontakes one load sample a minute, andacpmux machine notice --textadds one muted line to every live chat after a resize.Host load
machine load --jsonprints 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/proconly and never starts a daemon.busy/localTurnscome from a running daemon if it answers within 1.5 s.bun x,bunx, andnpxwrappers as the tool they run; a node or bun script counts as its tool.Host notice
machine notice --textappends ahost_noticetranscript record to every non-closed session on the daemon via the new local-only_acpmux/host_noticeRPC.Written for commit c559cd0. Summary will update on new commits.