caproom 0.7.6 → 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 +84 -158
- package/bin/caproom-mcp +0 -0
- package/bin/caproom-rs +0 -0
- package/package.json +10 -13
- package/prebuilt/caproom-darwin-arm64 +0 -0
- package/prebuilt/caproom-darwin-x64 +0 -0
- package/prebuilt/caproom-linux-x64 +0 -0
- package/prebuilt/caproom-win32-x64.exe +0 -0
- package/bin/caproom +0 -1025
- package/bin/caproom-mcp.js +0 -267
- package/bin/caproom.js +0 -21
- package/bin/caproom.ps1 +0 -753
- package/scripts/postinstall.js +0 -7
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
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
41
|
-
caproom
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
88
|
+
## Backends compared (POSIX)
|
|
75
89
|
|
|
76
|
-
|
|
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
|
-
|
|
98
|
+
Rust is a faster polling loop with better metrics, not yet event-driven.
|
|
79
99
|
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
136
|
-
"
|
|
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
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
128
|
+
## `caproom calibrate` — suggest limit
|
|
184
129
|
|
|
185
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
140
|
+
## Rust port status (current `main` 0.9.0)
|
|
208
141
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
-
|
|
222
|
-
-
|
|
223
|
-
-
|
|
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
|
|
package/bin/caproom-mcp
ADDED
|
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.
|
|
4
|
-
"description": "Memory-cap any command (AI coding agents, builds, background jobs) on macOS, Linux, and Windows
|
|
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
|
|
7
|
-
"caproom-mcp": "bin/caproom-mcp
|
|
6
|
+
"caproom": "bin/caproom-rs",
|
|
7
|
+
"caproom-mcp": "bin/caproom-mcp"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"bin/caproom",
|
|
11
|
-
"bin/caproom.
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
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
|