cmux-host-budget: the host CPU/RAM budget in one Rust crate, with the app's level rules and their C ABI (no FFI input yet)

Merged
LA
4d ago
+1418-339

The macOS app judges the host's CPU/RAM budget twice: acpmux's daemon reports a verdict (host_pressure in _acpmux/status), and when the daemon does not answer, the app's own Swift copy of the same formula (CmuxNextHostPressure) judges it. This PR moves the formula out of acpmux into a crate the app can link later, and ports the app's own rules (the level tracker and the rejudge on a memory pressure event) into it as Rust, with their C ABI. Nothing in the app changes yet.

Why the app needs a local verdict at all

The app starts acpmux at launch (AgentTabs prewarm), but the host pressure read never starts a daemon and gives up after 2 s. So the local verdict covers: the daemon still starting, restarting for an update or gone; a daemon too slow to answer under load (the overload the indicator is for); and builds with no acpmux (showcase, mock agent host). Deleting the fallback would leave the indicator blind in those cases, so the formula has to reach the app in Rust, in-process.

What changes

  • New crate cmux-tui/crates/cmux-host-budget: acpmux's host_pressure module (formula, override file, probe, Meter, evaluate) moved with its tests. The behavior is the same; the code differs only in small ways: evaluate's pressure and warning steps are helpers that the rejudge shares, Sample reads from JSON, and two panic-ratchet fixes (a checked u32 conversion and get(..end)). acpmux's src/host_pressure.rs re-exports it, so daemon status and the send warning read the same Meter::shared(). The meter's tokio reads (current, peek) are behind the default tokio feature, so the app can link it without tokio.
  • level.rs: the app's HostPressureTracker (warning at one reason or 75% of the load limit, a 10% deadband while raised, critical at critical pressure or two reasons, fall after calm_samples calm verdicts).
  • rejudge.rs: the app's HostBudgetVerdict.with(pressure:) on the verdict JSON, so a daemon verdict keeps its session facts (top, local_turns).
  • json.rs, ffi.rs, include/cmux_host_budget_ffi.h (module CCmuxHostBudgetFFI): the app's own verdict from the process's Meter (blocking, off the main thread), the rejudge, a judge of given numbers (the Debug Settings samples), the level and a tracker handle. JSON in acpmux's host_pressure shape, about once per 15 s; no call panics across the boundary.
  • cmux-tui/Cargo.lock gets the crate. The lock was written on an agent VM with cargo metadata --offline. The proof that it resolves is a fleet job: test level::tests 4eb95c52fc6046138b02d75c at cd0817eb72b, the head that merged. It ran cargo nextest run --locked in the cmux-tui workspace and passed. cmux-app-ffi's lock does not change in this PR.

This is no app FFI input: cmux-app-ffi does not depend on the crate here (check-app-ffi-pin.sh --list-inputs does not list it), so this PR needs no re-pin. The FFI move PR adds the cmux-app-ffi link, the header in build-app-ffi.sh, the crate in app-ffi-release.yml's push paths (needed for correctness: without it a change to the crate would publish no FFI release), the re-pin and the Swift half together. That Swift half replaces LocalHostPressureSource, HostSampler, HostBudgetFormula, the verdict's formula init, with(pressure:) and HostPressureTracker with calls into this ABI; the dispatch memory pressure source, the 15 s timer and the views stay in Swift.

The app's tracker and formula tests are ported as the Rust corpus (level_tests.rs, json_tests.rs, ffi_tests.rs). Swift tests not ported, and why: the override-file path from ACPMUX_HOST_BUDGET (a Rust test would set a process environment variable, which is unsafe in Rust 2024; it stays on the Swift side until the Swift half deletes it); theVerdictComesFromDaemonStatus, anAcpmuxWithoutAVerdictIsAskedRarely and all of HostPressureMonitorTests (they test Swift code that stays: the daemon read, its retry and the monitor); theSwapRateIsPerMinute and the HostSampler perf-level tests (the moved meter_tests.rs swap test and performance_cores_are_every_level_but_efficiency already cover them). The small differences between the Swift copy and the daemon's code are listed in the FFI move PR, where the app starts to use this code.

Risk: normal (the label). A crate move behind a re-export plus new rules nothing calls yet; acpmux's behavior is the same code.

Changelog

none

Observed / Not run

Observed (fleet, Linux):

  • Red at da425a23455 (the tests with stub rules), test level::tests 672cef6bf7a189222df3da3a: 17 run, 11 failed on assertions (for example a_raised_level_falls_only_after_calm_samples panicked at level_tests.rs:95), 6 passed (the moved formula, which has no stub).
  • Green at cd0817eb72b: test level::tests 4eb95c52fc6046138b02d75c, json::tests e4e2f6a7b51f035f2c0b52c1, ffi::tests 019c69376ac54408b9eef413, meter::tests f70355df532147e45932adbc: all passed.
  • At cd0817eb72b, clippy d71f8e3cd0f282526d5aad1e, fmt a6b5a475a90a24eb1f95b81e and panic ratchet f6cf68450d300b1e8d0712df are red only on base findings in files this PR does not touch; none names a cmux-host-budget file (cmux-host-budget clippy is clean):
    • clippy: suspicious_open_options at cmux-tui src/plugin_manager.rs:233 (from #31462; fix #31697).
    • fmt: acpmux src/hub/list_guard.rs (837, 925) and src/hub/mod.rs (1372).
    • panic ratchet: acpmux indexing_slicing 697 > 686 (fix #31773). This PR only removes acpmux code.
  • At bc20fdc3d5f the same three steps found two issues of this branch: an unneeded crate:: path in ffi_tests.rs and one as u32 cast. db00697f4ad fixes both.
  • scripts/cmux-next/tests/app-ffi-release-paths.test.sh: ok on this VM.

Known, for the FFI move PR (review comment at this head): cmux_host_budget_with_pressure_json takes one level (1, 2 or 4) and reads any other value as unknown. The dispatch source gives bit flags, and the Swift monitor reads 6 as critical. Before the app calls this ABI, the FFI move PR reads the bits (4 critical, then 2 warn, then 1 normal) and tests 6 and 3, or the Swift side maps the flags first. The Swift half also keeps the last verdict itself: the Rust Tracker does not hold it.

Not run: the crate-root tests module (src/tests.rs, the moved formula, override-file and /proc parser tests) and the acpmux tests at this head were not run as their own fleet steps; the FFI move PR runs cargo test -p cmux-host-budget in full, also with --no-default-features. anything in Swift or the app (no app change here).