caproom 0.7.5 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,226 +7,152 @@ Prevent RAM OOM for long-running terminal coding agents, builds, and background
7
7
 
8
8
  ## Why
9
9
 
10
- macOS has no reliable way to cap a process's memory from userspace. A runaway process — a leaking build tool, an AI agent stuck in a loop, a stray `find /` — has nothing standing between it and system OOM. `caproom` fills that gap with mechanisms that actually enforce a limit.
10
+ macOS has no reliable way to cap a process's memory from userspace. A runaway process — a leaking build tool, an AI agent stuck in a loop, a stray `find /` — has nothing standing between it and system OOM. `caproom` fills that gap with a host-native polling watchdog that actually enforces a limit on the whole process tree.
11
11
 
12
- ## What it does
12
+ ## What it does (Rust-only v0.9.0)
13
13
 
14
14
  ```
15
15
  caproom --limit <mb> -- <command> [args...]
16
+ # or explicit subcommand
17
+ caproom run --limit 2048 -- npm run build
16
18
  ```
17
19
 
18
- Two backends:
20
+ Single binary `bin/caproom-rs` (`1.2M`, `target/release/caproom` copy) — no bash, no JS wrapper, no PowerShell shim. The 1K-line bash watchdog (`bin/caproom`), `bin/caproom.js`, `bin/caproom-mcp.js`, `bin/caproom.ps1`, `scripts/postinstall.js` were removed in `f017051` (0.8.1). Install via `cargo`, `brew`, or `npm` (npm unpacks prebuilt `caproom-darwin-arm64/x64`, `caproom-linux-x64`, `caproom-win32-x64.exe`).
19
21
 
20
- 1. **Host-native polling watchdog** (`ps` RSS + `SIGKILL`) — the **default** on macOS/Linux. Runs in your real environment: same PATH, auth, native binaries, tty. Measures the **whole process tree** each poll (agents keep their memory in children — MCP servers, bundler daemons, headless browsers — while the parent's own RSS stays flat). Has a small race window bounded by `--interval` (default 200ms).
21
- 2. **Docker cgroup** (`--memory`) — opt-in with `--docker`. Hard cap, real kernel enforcement, zero race window — at the cost of running inside a Linux container (see caveats below). Fails loudly if the daemon isn't reachable rather than silently switching backends.
22
+ Core mechanism:
23
+
24
+ - **Collector:** `proc_listallpids` + `proc_pidinfo PROC_PIDTBSDINFO/TASKINFO` + `proc_pid_rusage ri_phys_footprint` (libproc, not `ps`). Measures **phys_footprint** (Mach `TASK_VM_INFO`, Jetsam/Activity Monitor truth) — not RSS. RSS overcounts shared libs. R/S state comes from TASKINFO `pti_numrunning`, because `pbi_status` reports SRUN for processes blocked in `time.sleep`.
25
+ - **Tree walk:** whole process tree per poll (agents keep memory in children — MCP servers, bundler daemons, headless browsers — while parent RSS stays flat). Ancestry walk depth 64, reparented to `launchd`/`init` (ppid 1) escapes.
26
+ - **Parking predicate `is_idle_subtree`:** `still_in_tree` + `footprint >=512KB` + `!is_session_leader` (`pid == sid` primary, `pgid == pid` fallback — foreground group never parks) + `cpu_delta <0.02` (2% over 500ms window via `CpuRing` from `ri_user_time+ri_system_time` delta). Dropped `pbi_status S|I` gate in 0.8.0 — multithreaded Python/Node reports `R` while sleeping in `time.sleep`.
27
+ - **Escalation:** park idle subtrees (`SIGSTOP`) → resample after 1s → only `TERM` → grace `5s` → `KILL` if still over `effective_limit`. `effective_limit = limit * (80 + 20*free_pct/15)/100` (free_pct from `vm_stat`/`MemAvailable`), so low headroom lowers trigger but doesn't escalate without park.
28
+ - **Polling-only v1:** `vm_stat`/`free_mem_pct` every `200ms`. `dispatch_source_memorypressure` event-driven (`0% idle`) is **not built** — `pressure::try_init_pressure_source() -> false` and logs `poll fallback 200ms (dispatch unavailable, v1.1 daemon will use GCD source)`. Deferred to daemon v1.1 single Mach source.
29
+ - **Docker backend dropped:** no `--docker`/`--image` in Rust (native only). `--pty`/`--no-intercept-tty`/`--force-watchdog`/`init`/`setup`/`SAFE_RUN` also dropped — they were bash-only.
22
30
 
23
31
  ## Install
24
32
 
25
33
  ```bash
34
+ # from source (requires Rust stable)
35
+ cargo install --locked --path crates/cli
36
+ # or
37
+ cargo install caproom # once published to crates.io
38
+
39
+ # Homebrew (tap exists, formula builds from source)
40
+ brew tap intelogroup/caproom
41
+ brew install caproom
42
+
43
+ # npm (unpacks prebuilt binaries, no wrapper)
26
44
  npm install -g caproom
45
+ # clean up old global npm shim if you migrated from bash
46
+ npm rm -g caproom; nvm use system; hash -r; which caproom # should be ~/.cargo/bin/caproom
27
47
  ```
28
48
 
29
- or clone and symlink `bin/caproom` onto your `PATH`.
49
+ Binary: `~/.cargo/bin/caproom` `v0.9.0`, `caproom --help` prints `Memory-cap any command — Rust CLI-first v1`.
30
50
 
31
51
  ## Usage
32
52
 
33
53
  ```bash
34
- # cap a build at 2GB (host-native watchdog, the default)
54
+ # cap a build at 2GB
35
55
  caproom --limit 2048 -- npm run build
56
+ caproom run --limit 2048 -- npm run build
36
57
 
37
58
  # cap an AI coding agent run at 512MB
38
59
  caproom --limit 512 -- claude -p "refactor this module"
39
60
 
40
- # opt in to the Docker cgroup backend for a zero-race hard cap
41
- caproom --limit 4096 --docker --image python:3.12-slim -- python train.py
61
+ # inspect trees
62
+ caproom freemem # prints free %
63
+ caproom top # human table sorted by TREE_MB
64
+ caproom top --json # machine schema 1
65
+ caproom top --json --pid 45057 # one subtree
66
+ caproom top --json --park-min-mb 1024
67
+
68
+ # park / wake
69
+ caproom park <pid> # SIGSTOP — pages eligible for reclaim under pressure
70
+ caproom wake <pid> # SIGCONT
71
+ caproom status <pid> # ps stat/rss
72
+
73
+ # calibrate limit from current footprint (24GB->14G, 8GB->4G clamp)
74
+ caproom calibrate
75
+ caproom calibrate --duration 30
42
76
  ```
43
77
 
44
78
  ### Flags
45
79
 
46
80
  | Flag | Default | Meaning |
47
81
  |---|---|---|
48
- | `--limit <mb>` | `4096` | memory cap in MB |
82
+ | `--limit <mb>` | `4096` | memory cap in MB (phys_footprint) |
49
83
  | `--interval <sec>` | `0.2` | watchdog poll interval |
50
- | `--grace <sec>` | `5` | seconds to wait after `SIGTERM` before `SIGKILL`, watchdog backend only — gives the process a chance to flush/save state before a hard kill |
51
- | `--docker` | off | opt in to the Docker cgroup backend instead of the default host-native watchdog |
52
- | `--image <name>` | `node:22-slim` | docker image used by the `--docker` backend |
53
- | `--force-watchdog` | — | legacy no-op; the watchdog IS the default. Accepted so existing scripts and `init` snippets keep working |
54
- | `--no-intercept-tty` | off | bypass stdio interposition for TUI/pty apps — `exec` directly and monitor via detached `watch --auto-park` (preserves `OSC 10/11`, `DSR CPR`, mouse `DEC 1003`) |
55
- | `--pty` | off | allocate a real pty via `forkpty` (`scripts/pty_wrapper.py` python or `script`) and forward bytes verbatim — full terminal fidelity, watchdog enforces cap on pty tree |
56
- | `--no-pty` | — | disable pty allocation (fallback to bypass/watchdog) |
57
-
58
- Env var overrides: `CAPROOM_LIMIT_MB`, `CAPROOM_IMAGE`, `CAPROOM_INTERVAL`, `CAPROOM_GRACE`, `CAPROOM_BYPASS_TTY=1` (force bypass), `CAPROOM_PTY=1` (force pty, same as `--pty`).
59
-
60
- On cap breach, the watchdog backend sends `SIGTERM` first and waits `--grace` seconds before `SIGKILL`. If the process exits cleanly during the grace window, `caproom` propagates its real exit code; only a hard `SIGKILL` (process ignored `SIGTERM`, or grace ran out) reports `137` (same convention as Docker's own OOM-kill exit code, which the docker backend always uses on breach since Docker itself sends the kill).
61
-
62
- ## Backends compared
63
-
64
- The three enforcement mechanisms measure different quantities and cover children differently. Read this before reusing a `--limit` number across platforms or backends. The watchdog is the POSIX default; Docker is opt-in — caproom prefers to run your command unmodified in its real environment and *miss* an exotic memory spike over breaking a working workflow with container drift:
84
+ | `--grace <sec>` | `5` | seconds to wait after `SIGTERM` before `SIGKILL` |
65
85
 
66
- | | Docker cgroup | Windows Job Object | Watchdog (POSIX) | Watchdog (Windows) |
67
- |---|---|---|---|---|
68
- | Measures | cgroup memory | **committed virtual memory** | RSS of the process tree | working set of the process tree |
69
- | Children counted | yes — whole container | yes — auto-inherited at spawn | yes — tree walked each poll | yes — tree walked each poll |
70
- | Enforcement | kernel OOM-kill | allocation fails in-process | TERM → grace → KILL | hard kill (`taskkill /T /F`) |
71
- | Race window | none | none | bounded by `--interval` | bounded by `--interval` |
72
- | Interactive/streaming output | degraded — no TTY (`-i` only) | full — streamed live | full — child inherits the tty | streamed live via temp-file tail-follow (~50ms cadence) |
86
+ On breach: `SIGTERM` whole tree wait `grace` `SIGKILL` grace survivors from breach snapshot. Exit `143` if TERM honored, `137` if KILL (same as Docker OOM convention, but now native).
73
87
 
74
- **Committed vs RSS**: Node/V8 runtimes commit far more virtual memory than they touch, so a limit tuned against RSS on macOS will bite much earlier under the Job Object backend. Tune per platform.
88
+ ## Backends compared (POSIX)
75
89
 
76
- ### Docker backend caveats
90
+ | | Watchdog (bash 0.7.6) | Watchdog (Rust v1) | Watchdog (Windows) |
91
+ |---|---|---|---|
92
+ | Measures | RSS of tree | **phys_footprint** (`ri_phys_footprint`) | working set |
93
+ | Children | tree walked each poll | libproc tree walked each poll | tree walked |
94
+ | Enforcement | TERM → grace → KILL | TERM → grace → KILL | `taskkill /T /F` |
95
+ | Race window | bounded by `interval` | bounded by `interval` (polling-only; event-driven deferred) | bounded |
96
+ | Interactive | full tty | full tty | temp-file tail-follow (~50ms) |
77
97
 
78
- Opt in with `--docker`. The command then runs inside `node:22-slim` with `$PWD` mounted at `/work` — a Linux container, not your host shell:
98
+ Rust is a faster polling loop with better metrics, not yet event-driven.
79
99
 
80
- - Native modules built for macOS (`esbuild`, `swc`, `sharp`) fail with exec-format errors inside the container.
81
- - Host toolchain, env vars, git credentials, and `~/.ssh` are not present.
82
- - The image pins Node 22 regardless of your project's version (`--image` to override).
83
- - No TTY is allocated, so interactive/TUI programs degrade; Docker Desktop's file-share layer slows large builds on macOS.
100
+ ## `caproom top` process-tree inventory
84
101
 
85
- For capping an AI agent session you want to *interact* with, use the default watchdog: same host environment, streaming output, no container drift. Reach for `--docker` when you need the zero-race kernel guarantee and the command is container-safe.
86
-
87
- Orphan safety differs too. The watchdog TERMs the whole measured tree and SIGKILLs grace-period survivors from a breach-time snapshot — but a process that detaches before being observed escapes. Inside the Docker backend, the kernel's cgroup OOM handling acts on every task in the container: nothing outlives it, though the OOM killer picks victims by badness (it may kill your hog rather than the whole container — either way the capped workload ends and `caproom` exits non-zero). The Windows Job Object kills the whole job atomically on breach.
88
-
89
-
90
- ## init — auto-cap a command on every launch
91
-
92
- For a command you always want capped (e.g. an AI coding agent), don't type the wrapper every time — bake it into your shell so a new terminal tab is capped automatically:
93
-
94
- ```bash
95
- caproom init claude --limit 6144 --grace 10 >> ~/.zshrc && source ~/.zshrc
96
- ```
97
-
98
- This appends a shell function that wraps `claude` through the watchdog backend (host-native — no Docker isolation, so the wrapped command keeps its normal filesystem/auth/PATH access) and an alias so plain `claude` picks it up. Per-shell override without editing the rc file: `CAPROOM_LIMIT_MB=8192 claude ...`. Works for any command, not just `claude` — `caproom init npm --limit 2048` wraps `npm` the same way.
99
-
100
- **TUI / pty note:** `caproom -- <TUI>` (opencode, claude, vim, htop, …) would break the pty — `OSC 10/11` (`^[]10;rgb:...`), `DSR CPR` (`^[[5;1R`), and mouse `^[[<35;` reports leak as text. Since 0.7.3, `caproom` detects `[ -t 0 ] && [ -t 1 ]` for known TUIs and **bypasses stdio interposition**: the TUI `exec`s directly and a detached `caproom watch --auto-park` monitors its pid tree (same cap, no pty corruption). For full pty fidelity, use `--pty` / `CAPROOM_PTY=1` which allocates a real pty via `forkpty` (`python3` `scripts/pty_wrapper.py` or `script`) and forwards bytes verbatim while the watchdog still enforces the cap on the pty tree. Piped/batch `caproom -- opencode run "task"` stays fully capped via the watchdog. Override: `CAPROOM_BYPASS_TTY=1` / `--no-intercept-tty` forces bypass, `CAPROOM_PTY=1` / `--pty` forces pty for any command. `caproom init <TUI>` prints a warning and emits the tty-aware wrapper.
101
-
102
- ## setup / bind — shell integration for every terminal
103
-
104
- `caproom setup` binds headroom management to your shells at the **shell layer**, so it works in any terminal (Terminal.app, iTerm2, Ghostty, Windows Terminal) without touching terminal-specific config:
105
-
106
- ```bash
107
- caproom setup # bind zsh/bash + fish (Windows: PowerShell $PROFILE)
108
- caproom setup --uninstall # remove rc markers; backups and integration files stay
109
- ```
110
-
111
- What it does:
112
-
113
- - Writes one integration file per shell under `~/.caproom/` (`shell.sh`, `shell.fish`, `shell.ps1`) and adds a 3-line marker block (`# >>> caproom >>>`) to your rc files. Idempotent; timestamped `.bak` backups are made before first patch.
114
- - Adds a **headroom warning on every prompt** — once per minute, only when free memory drops below `CAPROOM_HEADROOM_WARN` percent (default 20). Never blocks, never modifies your prompt text.
115
- - Optional **auto-wrap**: `export CAPROOM_AUTO_WRAP="claude,codex,opencode"` in your rc creates `<cmd>_capped` twins that run through the watchdog with `CAPROOM_LIMIT_MB`. Bare command names are shadowed **only** if you also set `CAPROOM_AUTO_ALIAS=1` — caproom never hijacks an unwrapped command by default.
116
- - Optional login daemon: `caproom setup --guard --threshold 15` installs a LaunchAgent (macOS) or systemd user unit (Linux) running `caproom guard` across all terminals.
117
-
118
- npm install never touches your rc files — postinstall prints a hint, binding is always explicit. Undo any time: `caproom setup --uninstall`.
119
-
120
- ### `caproom top` — process-tree inventory for agents
121
-
122
- Read-only snapshot of every process tree you own, sorted by tree RSS. `--json` output is a **stable contract**: `schema` version field, additive changes only.
123
-
124
- ```bash
125
- caproom top # human table
126
- caproom top --json # machine output
127
- caproom top --json --pid 45057 # one subtree only
128
- caproom top --json --park-min-mb 1024 # park-candidate threshold (default 512MB)
129
- ```
102
+ Read-only snapshot of every tree you own, sorted by tree footprint.
130
103
 
131
104
  ```json
132
105
  { "schema": 1, "ts": 1755950000, "limit_mb_default": 4096,
133
106
  "processes": [
134
- { "pid": 45057,
135
- "cmd": "node /tmp/hog.mjs",
136
- "tree_rss_kb": 455136,
137
- "tree_pids": [45057, 45060],
138
- "state": "running" | "parked" | "zombie",
139
- "park_candidate": true,
107
+ { "pid": 45057, "cmd": "node /tmp/hog.mjs", "tree_rss_kb": 455136, "footprint_kb": 455136,
108
+ "tree_pids": [45057, 45060], "state": "running", "reason_code": "PARK_IDLE|GROWTH_RATE|PRESSURE|NONE",
109
+ "growth_kb_s": 0, "free_pct": 33, "park_candidate": true,
140
110
  "reason": "root sleeping + tree_rss 455136KB >= 524288KB park threshold" } ] }
141
111
  ```
142
112
 
143
- One row per **tree root**; members are listed in `tree_pids`. `park_candidate` is a heuristic (`state == running`, root sleeping/idle, tree RSS ≥ threshold) with the rule spelled out in `reason` so the agent never re-derives it — override freely using the raw fields. The intended loop: poll `top --json` → decide → `park <pid>` / `wake <pid>`. Note `park` makes pages *eligible* for reclaim; see the caveat under park/wake below before treating it as freed RAM.
113
+ One row per tree root; `park_candidate` is heuristic (`state running` + sleeping + footprint ≥ threshold) with `reason` spelled out.
144
114
 
145
115
  ## park / wake — reclaim idle memory without killing
146
116
 
147
- Long-running agent sessions accumulate subprocesses that go idle but stay resident — old file watchers, finished tool-call children, stale servers. Killing them loses state; leaving them wastes RAM. `caproom park` freezes instead:
148
-
149
- ```bash
150
- caproom park <pid> # SIGSTOP — process stays alive, keeps its PID and state,
151
- # just isn't scheduled. Its memory becomes eligible for
152
- # the kernel's own compressor once real system memory
153
- # pressure shows up.
154
- caproom wake <pid> # SIGCONT — resumes instantly, same state, no restart.
155
- caproom status <pid> # pid, state (T = parked, S = running), RSS, elapsed
156
117
  ```
157
-
158
- Verified empirically on macOS: a parked process's RSS dropped ~90% (345MB → 37MB) once real memory pressure hit, and resumed correctly and stayed responsive after `SIGCONT`. No reclaim happens while the system is idle/unpressured this rides the kernel's own compressor, it doesn't force anything.
159
-
160
- No daemon, no tracking file, no dependency — just `SIGSTOP`/`SIGCONT` wrapped in a CLI. Any script or agent can call `caproom park <pid>` / `caproom wake <pid>` directly.
161
-
162
- **Caveat**: a parked process does zero work while stopped — no CPU, no I/O, no timers firing. Only park something actually idle (a background watcher, a finished subprocess kept around for reuse) — never park the process an agent is actively waiting on a response from, or you'll hang the agent, not save it memory. Also: SIGSTOP only makes pages *eligible* for reclaim — the kernel compresses/evicts them lazily under real memory pressure. Park an idle 2GB agent on a quiet machine and it may stay ~2GB resident for hours. Park is insurance against OOM, not immediate RAM return.
163
-
164
- ### `caproom top` / `caproom watch` — agent interface
165
-
166
- `caproom top --json` (above) is read-only discovery with a stable schema. `caproom watch` turns it into a daemon:
167
-
168
- ```bash
169
- # observer: report tree-RSS breaches, touch nothing
170
- caproom watch --threshold-mb 2000 --json <pid>
171
-
172
- # arm auto-park: freeze breaching trees (SIGSTOP every pid in the snapshot)
173
- caproom watch --threshold-mb 2000 --auto-park --json <pid>
174
-
175
- # also restore automatically when system free memory recovers
176
- caproom watch --threshold-mb 2000 --auto-park --auto-wake-free-pct 15 <pid>
118
+ caproom park <pid> # SIGSTOP — single PID, pages eligible for kernel compressor
119
+ caproom park-tree <pid> # SIGSTOP whole tree (recursive Tree::build) — PID reuse guarded (pid,start)
120
+ caproom wake <pid> # SIGCONT single
121
+ caproom wake-tree <pid> # SIGCONT whole tree
177
122
  ```
178
123
 
179
- Naming the pid IS the per-process opt-in there is no system-wide mode, since stopping an unchosen process risks freezing it mid-write. Events are NDJSON on stdout (`started`, `breach`/`parked`, `recovered`, `woke`, `all-exited`). Auto-park freezes the whole measured tree, tracks exactly what *it* stopped, never re-parks within one breach episode (woken trees stay awake unless RSS drops back under threshold), and `--auto-wake-free-pct` undoes only watch's own parks.
124
+ Verified on macOS: parked RSS dropped `~90%` (345MB 37MB) once real pressure hit. No reclaim while system idle/unpressured rides kernel compressor, doesn't force. **Caveat:** parked process does zero work while stopped only park something actually idle (watcher, finished subprocess), never the process an agent is waiting on.
180
125
 
181
- Typical loop: `top --json` finds candidates `watch --auto-park` babysits them during heavy builds explicit or automatic wake restores them after.
126
+ Predicate `is_idle_subtree` protects: `cpu_delta <0.02` (`CpuRing` 500ms window, first sample assumes busy `1.0` so active watcher not parked on first sight) and `is_session_leader` (`pid == sid` primary, `pgid == pid` fallback) never parks foreground. Tested with real `python3 -c "while True: pass"` busy vs `sleep` idle — `park_does_not_hang_active_watcher` proves.
182
127
 
183
- ## MCP servernative agent access
128
+ ## `caproom calibrate`suggest limit
184
129
 
185
- `npm i -g caproom` also installs `caproom-mcp`, a zero-dependency MCP server (stdio) exposing the agent interface as tools:
130
+ Prints total RAM, free %, top 3 trees footprint, and `suggested --limit` (60% total, clamp 4G min / 80% max). Migration note `old RSS --limit 6144 ≈ footprint ~4.8G` (≈80% due shared overcount).
186
131
 
187
- ```json
188
- { "mcpServers": { "caproom": { "command": "caproom-mcp" } } }
189
- ```
190
-
191
- Tools: `top` (tree inventory, stable schema), `park`/`wake`, `watch_start`/`watch_events`/`watch_stop` (daemon lifecycle, NDJSON events), and `run` (execute a command under a cap, returns a KILLED-BY-CAP verdict at exit 137). Same gating as the CLI: watch requires explicit pids; auto-park is opt-in per watcher.
192
-
193
- ## What it never touches
132
+ ## MCP — typed surface
194
133
 
195
- caproom only watches OS-level RSS and sends signals (`SIGTERM`/`SIGKILL`/`SIGSTOP`/`SIGCONT`). The watchdog backend runs the wrapped command as a direct child with stdin/stdout/stderr passed straight through no pipe, no buffering, no interception. The Docker backend passes stdio through the same way (`docker run -i`). caproom never reads, modifies, or truncates anything the wrapped process reads or writes including an AI agent's own conversation/context stream. It manages RAM headroom only, nothing else.
134
+ `crates/caproom-mcp` is hand-rolled `serde` JSON with strictly typed enums (`state: parked|running|zombie`, `reason_code: PARK_IDLE|GROWTH_RATE|PRESSURE|NONE`), no `message` string. Tools v1 (8): `top`, `park`, `park_tree`, `wake`, `wake_tree`, `run`, `freemem`, `status`. Unknown tools return proper JSON-RPC error objects (`-32601`). `watch_*` + `rmcp` deferred to v1.1 with tokio isolated to MCP crate.
196
135
 
197
136
  ## Windows
198
137
 
199
- Windows uses a separate PowerShell backend, selected automatically. Same commands, but the semantics differ in three ways worth knowing before you reuse a `--limit` number across platforms.
200
-
201
- **The cap is a Job Object** (`JOB_OBJECT_LIMIT_PROCESS_MEMORY`), enforced by the kernel at allocation time. Two things it does better than the POSIX watchdog: there is no poll-interval race window, and child processes are covered automatically — a process associated with a job passes that association to anything it spawns, so the whole tree is capped, not just the direct child.
202
-
203
- **`--limit` means committed memory on Windows, RSS on macOS/Linux.** These are different quantities. The same number will bite at a different point, so tune it per platform rather than assuming it transfers.
204
-
205
- **No grace period.** Windows console apps have no `SIGTERM` equivalent. Under the Job Object backend nothing is killed at all — the allocation just fails inside the process. Under the watchdog fallback, a breach kills the whole tree (`taskkill /T /F`) with no chance to flush state. `--grace` is accepted and ignored.
138
+ Same `run`/`top`/`park`/`wake` via `cfg(unix)`/`cfg(windows)` guards (`cargo check --target x86_64-pc-windows-msvc` passes). `park`/`wake` are stubs (`park not implemented on Windows`) — `EmptyWorkingSet` not wired in Rust v1. Windows watchdog `taskkill /T /F` for TERM/KILL, no grace, `--grace` ignored. No Job Object hard cap — Rust is watchdog-only.
206
139
 
207
- **Watchdog output streams, but through temp files.** stdout/stderr are captured to files and tail-followed (~50ms cadence) so logs and CI steps show progress live. Full-screen TUI redraws are not pixel-perfect over this path; plain streaming output (agents in non-interactive mode, builds) works normally.
140
+ ## Rust port status (current `main` 0.9.0)
208
141
 
209
- **`park` does not suspend on Windows.** It calls `EmptyWorkingSet`, which trims the process's working set to the pagefile immediately and on demand — no waiting for system memory pressure, and **the process keeps running**. The macOS caveat about never parking a process an agent is waiting on does not apply here. `caproom wake` is therefore a no-op on Windows; trimmed pages fault back in on next access.
210
-
211
- `init` emits a PowerShell function plus `Set-Alias` for your `$PROFILE`:
212
-
213
- ```powershell
214
- caproom init claude --limit 6144 >> $PROFILE
215
- ```
216
-
217
- Docker backend is not wired up on Windows — the Job Object path already gives kernel enforcement, so there is nothing for it to add.
142
+ - `phys_footprint` via libproc, typed `top --json` with `growth_kb_s`/`reason_code`/`free_pct`
143
+ - ✅ `is_idle_subtree` ancestry walk + `cpu_delta` (`CpuRing` first-sample busy) + `is_session_leader` (`pid==sid` + `pid==pgid` fallback) wired, `S|I` gate dropped, `28` tests incl. `park_does_not_hang_active_watcher`
144
+ - R/S via TASKINFO `pti_numrunning` (`pbi_status` reports SRUN for sleepers); pid-list buffer grows past 8192 instead of silent truncation
145
+ - ✅ `growth` contextual `should_enforce_growth` (70% + pressure + <5min breach, 600s for >1MB/s) not bare `>200 KB/s`; `pressure` cached `hw.memsize OnceLock` + `free 800ms/200ms`
146
+ - ✅ `park-tree`/`wake-tree` tree-aware + `(pid,start_time)` reuse guard (`pbi_start` / `/proc/starttime` via `getsid`), `is_session_leader` fixes `Ghostty→tmux` conflation
147
+ - MCP: 8 tools, invalid-pid guards (kill(0)/kill(1) rejected), park_tree reuse guard, JSON-RPC error objects, EPIPE-safe writes
148
+ - ✅ `effective_limit = limit * (80 + 20*free_pct/15)/100` pressure-aware threshold
149
+ - ❌ Not yet: `dispatch_source_memorypressure` event-driven (polling-only v1), `rmcp` port, daemon+arbiter. See `docs/plan-caproom-rust.md` for reality vs pitch.
218
150
 
219
151
  ## Limitations
220
152
 
221
- - Docker backend mounts `$PWD` into the container at `/work` and runs there — paths outside `$PWD` aren't visible to the command.
222
- - Watchdog backends have a real (if small) race window; for a hard guarantee, opt into the Docker backend (`--docker`) on POSIX, or use the Job Object backend on Windows.
223
- - The watchdog's tree walk follows live parent→child edges. A child that *daemonizes* (double-fork, reparented to init/launchd) leaves the tree and escapes the cap — as does any process spawned after its parent chain broke, or during the kill grace window. On breach the watchdog signals every pid in the measured tree and SIGKILLs survivors of the grace period from a breach-time snapshot, so children cannot outlive the root — but processes that detach *before* being observed are missed by design. The Windows Job Object backend does not have this gap. This is a deliberate trade: caproom prefers to **miss** memory outside the tracked lineage rather than risk interfering with processes the user didn't ask it to manage.
224
- - On Windows, `Get-CimInstance` per poll makes the watchdog heavier than a plain RSS read; keep `--interval` at 0.2s or above there.
225
- - On Windows, the Job Object holds only the wrapped command and its descendants — never caproom itself — so the full `--limit` reaches your workload. (Cost: a millisecond-scale window after spawn before assignment lands, where the child is not yet counted.)
226
-
227
- ## Contributing
228
-
229
- Issues and PRs welcome at [github.com/intelogroup/caproom](https://github.com/intelogroup/caproom).
153
+ - Watchdog has bounded `interval` race window; for hard guarantee use cgroups outside caproom (Docker dropped in Rust v1).
154
+ - Tree walk follows live `ppid` edges. Daemonized double-fork reparented to 1 escapes by design prefer missing outside lineage over interfering with user processes.
155
+ - Parking only makes pages *eligible*; kernel compresses lazily under real pressure. Parking 2GB on idle machine may stay ~2GB for hours.
230
156
 
231
157
  ## License
232
158
 
Binary file
package/bin/caproom-rs ADDED
Binary file
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "caproom",
3
- "version": "0.7.5",
4
- "description": "Memory-cap any command (AI coding agents, builds, background jobs) on macOS, Linux, and Windows \u2014 real enforcement via Docker cgroups, Windows Job Objects, or a polling watchdog, plus park/wake to reclaim idle process memory without killing.",
3
+ "version": "0.9.2",
4
+ "description": "Memory-cap any command (AI coding agents, builds, background jobs) on macOS, Linux, and Windows real enforcement via Docker cgroups, Windows Job Objects, or a polling watchdog, plus park/wake to reclaim idle process memory without killing.",
5
5
  "bin": {
6
- "caproom": "bin/caproom.js",
7
- "caproom-mcp": "bin/caproom-mcp.js"
6
+ "caproom": "bin/caproom-rs",
7
+ "caproom-mcp": "bin/caproom-mcp"
8
8
  },
9
9
  "files": [
10
- "bin/caproom",
11
- "bin/caproom.js",
12
- "bin/caproom.ps1",
13
- "bin/caproom-mcp.js",
14
- "scripts/postinstall.js",
15
- "scripts/pty_wrapper.py"
10
+ "bin/caproom-rs",
11
+ "bin/caproom-rs.exe",
12
+ "prebuilt/*",
13
+ "scripts/pty_wrapper.py",
14
+ "bin/caproom-mcp",
15
+ "bin/caproom-mcp.exe"
16
16
  ],
17
17
  "keywords": [
18
18
  "memory",
@@ -30,9 +30,6 @@
30
30
  "linux",
31
31
  "win32"
32
32
  ],
33
- "scripts": {
34
- "postinstall": "node scripts/postinstall.js"
35
- },
36
33
  "license": "MIT",
37
34
  "repository": {
38
35
  "type": "git",
Binary file
Binary file
Binary file
Binary file