caproom 0.1.0 → 0.3.1
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 +55 -12
- package/bin/caproom +103 -7
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
# caproom
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/caproom)
|
|
4
|
+
[](LICENSE)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
macOS has no working per-process memory limit in userspace. Verified empirically:
|
|
6
|
+
Prevent RAM OOM for long-running terminal coding agents, builds, and background jobs — memory caps plus idle-process parking, for macOS/Linux.
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
- launchd's own `HardResourceLimits.ResidentSetSize` is a no-op — a job capped at 100MB was observed running past 590MB, still `state = active`.
|
|
8
|
+
## Why
|
|
11
9
|
|
|
12
|
-
|
|
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.
|
|
13
11
|
|
|
14
12
|
## What it does
|
|
15
13
|
|
|
@@ -22,11 +20,9 @@ Two backends, auto-selected:
|
|
|
22
20
|
1. **Docker cgroup** (`--memory`) — used when Docker is installed and running. Hard cap, real kernel enforcement, zero race window.
|
|
23
21
|
2. **Polling watchdog** (`ps` RSS + `SIGKILL`) — fallback when Docker isn't available. No dependencies, works anywhere `ps` exists. Has a small race window bounded by `--interval` (default 200ms) — a process can spike briefly past the cap between polls before being killed.
|
|
24
22
|
|
|
25
|
-
Both are honest about their mechanism: neither claims kernel-enforced rlimit, because that doesn't exist on macOS for arbitrary processes.
|
|
26
|
-
|
|
27
23
|
## Install
|
|
28
24
|
|
|
29
|
-
```
|
|
25
|
+
```bash
|
|
30
26
|
npm install -g caproom
|
|
31
27
|
```
|
|
32
28
|
|
|
@@ -48,9 +44,52 @@ caproom --limit 1024 --force-watchdog -- ./some-script.sh
|
|
|
48
44
|
caproom --limit 4096 --image python:3.12-slim -- python train.py
|
|
49
45
|
```
|
|
50
46
|
|
|
51
|
-
|
|
47
|
+
### Flags
|
|
48
|
+
|
|
49
|
+
| Flag | Default | Meaning |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `--limit <mb>` | `4096` | memory cap in MB |
|
|
52
|
+
| `--image <name>` | `node:22-slim` | docker image used by the docker backend |
|
|
53
|
+
| `--interval <sec>` | `0.2` | watchdog poll interval |
|
|
54
|
+
| `--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 |
|
|
55
|
+
| `--force-watchdog` | off | use the polling watchdog even if Docker is available |
|
|
56
|
+
|
|
57
|
+
Env var overrides: `CAPROOM_LIMIT_MB`, `CAPROOM_IMAGE`, `CAPROOM_INTERVAL`, `CAPROOM_GRACE`.
|
|
58
|
+
|
|
59
|
+
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).
|
|
60
|
+
|
|
61
|
+
## init — auto-cap a command on every launch
|
|
62
|
+
|
|
63
|
+
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:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
caproom init claude --limit 6144 --grace 10 >> ~/.zshrc && source ~/.zshrc
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
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.
|
|
70
|
+
|
|
71
|
+
## park / wake — reclaim idle memory without killing
|
|
72
|
+
|
|
73
|
+
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:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
caproom park <pid> # SIGSTOP — process stays alive, keeps its PID and state,
|
|
77
|
+
# just isn't scheduled. Its memory becomes eligible for
|
|
78
|
+
# the kernel's own compressor once real system memory
|
|
79
|
+
# pressure shows up.
|
|
80
|
+
caproom wake <pid> # SIGCONT — resumes instantly, same state, no restart.
|
|
81
|
+
caproom status <pid> # pid, state (T = parked, S = running), RSS, elapsed
|
|
82
|
+
```
|
|
52
83
|
|
|
53
|
-
|
|
84
|
+
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.
|
|
85
|
+
|
|
86
|
+
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.
|
|
87
|
+
|
|
88
|
+
**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.
|
|
89
|
+
|
|
90
|
+
## What it never touches
|
|
91
|
+
|
|
92
|
+
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.
|
|
54
93
|
|
|
55
94
|
## Limitations
|
|
56
95
|
|
|
@@ -58,6 +97,10 @@ On cap breach, `caproom` kills the process and exits `137` (same convention as D
|
|
|
58
97
|
- Watchdog backend has a real (if small) race window; for a hard guarantee, use the Docker backend.
|
|
59
98
|
- Neither backend can cap a process that immediately forks and hides children under a different watched PID tree in unusual ways — the watchdog only tracks the direct child.
|
|
60
99
|
|
|
100
|
+
## Contributing
|
|
101
|
+
|
|
102
|
+
Issues and PRs welcome at [github.com/intelogroup/caproom](https://github.com/intelogroup/caproom).
|
|
103
|
+
|
|
61
104
|
## License
|
|
62
105
|
|
|
63
106
|
MIT
|
package/bin/caproom
CHANGED
|
@@ -10,28 +10,111 @@
|
|
|
10
10
|
set -euo pipefail
|
|
11
11
|
|
|
12
12
|
usage() {
|
|
13
|
-
|
|
13
|
+
local stream=/dev/stderr
|
|
14
|
+
local code=1
|
|
15
|
+
if [[ "${1:-}" == "help" ]]; then stream=/dev/stdout; code=0; fi
|
|
16
|
+
cat >"$stream" << 'EOF'
|
|
14
17
|
usage: caproom [--limit <mb>] [--image <docker-image>] [--interval <sec>] -- <command> [args...]
|
|
18
|
+
caproom park <pid>
|
|
19
|
+
caproom wake <pid>
|
|
20
|
+
caproom status <pid>
|
|
21
|
+
caproom init <command> [--limit <mb>] [--grace <sec>]
|
|
15
22
|
|
|
16
23
|
--limit <mb> memory cap in MB (default: 4096)
|
|
17
24
|
--image <name> docker image to run the command in, when using the docker
|
|
18
25
|
backend (default: node:22-slim)
|
|
19
26
|
--interval <sec> watchdog poll interval in seconds, fallback backend only
|
|
20
27
|
(default: 0.2)
|
|
28
|
+
--grace <sec> seconds to wait after SIGTERM before SIGKILL, watchdog
|
|
29
|
+
backend only (default: 5) — gives the process a chance
|
|
30
|
+
to flush/save state before a hard kill
|
|
21
31
|
--force-watchdog force the polling watchdog even if Docker is available
|
|
22
32
|
|
|
23
|
-
|
|
33
|
+
park / wake — freeze an idle process so the kernel can reclaim/compress its
|
|
34
|
+
memory without killing it. For a long-running agent sitting on stale
|
|
35
|
+
subprocesses: `caproom park <pid>` (SIGSTOP) instead of killing it. It stays
|
|
36
|
+
alive, keeps its PID, keeps its state — just isn't scheduled and its memory
|
|
37
|
+
becomes eligible for compression under system memory pressure. `caproom wake
|
|
38
|
+
<pid>` (SIGCONT) brings it back instantly, same state, no restart needed.
|
|
39
|
+
Any agent can call these directly — they're just SIGSTOP/SIGCONT, no daemon,
|
|
40
|
+
no tracking file required.
|
|
41
|
+
|
|
42
|
+
init <command> — print a shell snippet that auto-caps <command> on every
|
|
43
|
+
invocation, so a new terminal tab is capped with no extra typing. Append the
|
|
44
|
+
output to your shell rc (~/.zshrc, ~/.bashrc):
|
|
45
|
+
|
|
46
|
+
caproom init claude >> ~/.zshrc && source ~/.zshrc
|
|
47
|
+
|
|
48
|
+
env vars (override flags): CAPROOM_LIMIT_MB, CAPROOM_IMAGE, CAPROOM_INTERVAL, CAPROOM_GRACE
|
|
24
49
|
|
|
25
50
|
examples:
|
|
26
51
|
caproom --limit 2048 -- npm run build
|
|
27
52
|
caproom --limit 512 -- claude --dangerously-skip-permissions -p "task"
|
|
53
|
+
caproom park 12345
|
|
54
|
+
caproom wake 12345
|
|
55
|
+
caproom init claude --limit 6144 --grace 10
|
|
56
|
+
EOF
|
|
57
|
+
exit "$code"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
cmd_init() {
|
|
61
|
+
local target="${1:-}"
|
|
62
|
+
[[ -z "$target" ]] && { echo "usage: caproom init <command> [--limit <mb>] [--grace <sec>]" >&2; exit 1; }
|
|
63
|
+
shift
|
|
64
|
+
local limit=4096
|
|
65
|
+
local grace=5
|
|
66
|
+
while [[ $# -gt 0 ]]; do
|
|
67
|
+
case "$1" in
|
|
68
|
+
--limit) limit="$2"; shift 2 ;;
|
|
69
|
+
--grace) grace="$2"; shift 2 ;;
|
|
70
|
+
*) echo "caproom: unknown init flag $1" >&2; exit 1 ;;
|
|
71
|
+
esac
|
|
72
|
+
done
|
|
73
|
+
local fn="${target}_capped"
|
|
74
|
+
cat << EOF
|
|
75
|
+
# caproom: auto-cap '$target' — added by 'caproom init $target'
|
|
76
|
+
# override per-shell: CAPROOM_LIMIT_MB=8192 $target ...
|
|
77
|
+
$fn() {
|
|
78
|
+
command caproom --limit "\${CAPROOM_LIMIT_MB:-$limit}" --force-watchdog --grace "\${CAPROOM_GRACE:-$grace}" -- command $target "\$@"
|
|
79
|
+
}
|
|
80
|
+
alias $target=$fn
|
|
28
81
|
EOF
|
|
29
|
-
exit 1
|
|
30
82
|
}
|
|
31
83
|
|
|
84
|
+
cmd_park() {
|
|
85
|
+
local pid="${1:-}"
|
|
86
|
+
[[ -z "$pid" ]] && { echo "usage: caproom park <pid>" >&2; exit 1; }
|
|
87
|
+
kill -0 "$pid" 2>/dev/null || { echo "caproom: no such pid $pid" >&2; exit 1; }
|
|
88
|
+
kill -STOP "$pid"
|
|
89
|
+
echo "caproom: pid $pid parked (SIGSTOP) — memory now eligible for kernel reclaim under pressure. wake with: caproom wake $pid" >&2
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
cmd_wake() {
|
|
93
|
+
local pid="${1:-}"
|
|
94
|
+
[[ -z "$pid" ]] && { echo "usage: caproom wake <pid>" >&2; exit 1; }
|
|
95
|
+
kill -0 "$pid" 2>/dev/null || { echo "caproom: no such pid $pid" >&2; exit 1; }
|
|
96
|
+
kill -CONT "$pid"
|
|
97
|
+
echo "caproom: pid $pid woken (SIGCONT)" >&2
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
cmd_status() {
|
|
101
|
+
local pid="${1:-}"
|
|
102
|
+
[[ -z "$pid" ]] && { echo "usage: caproom status <pid>" >&2; exit 1; }
|
|
103
|
+
ps -o pid,stat,rss,etime,command -p "$pid" 2>/dev/null || { echo "caproom: no such pid $pid" >&2; exit 1; }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case "${1:-}" in
|
|
107
|
+
park) shift; cmd_park "$@"; exit 0 ;;
|
|
108
|
+
wake) shift; cmd_wake "$@"; exit 0 ;;
|
|
109
|
+
status) shift; cmd_status "$@"; exit 0 ;;
|
|
110
|
+
init) shift; cmd_init "$@"; exit 0 ;;
|
|
111
|
+
help|-h|--help) usage help ;;
|
|
112
|
+
esac
|
|
113
|
+
|
|
32
114
|
LIMIT_MB="${CAPROOM_LIMIT_MB:-4096}"
|
|
33
115
|
IMAGE="${CAPROOM_IMAGE:-node:22-slim}"
|
|
34
116
|
INTERVAL="${CAPROOM_INTERVAL:-0.2}"
|
|
117
|
+
GRACE="${CAPROOM_GRACE:-5}"
|
|
35
118
|
FORCE_WATCHDOG=0
|
|
36
119
|
|
|
37
120
|
while [[ $# -gt 0 ]]; do
|
|
@@ -39,9 +122,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
39
122
|
--limit) LIMIT_MB="$2"; shift 2 ;;
|
|
40
123
|
--image) IMAGE="$2"; shift 2 ;;
|
|
41
124
|
--interval) INTERVAL="$2"; shift 2 ;;
|
|
125
|
+
--grace) GRACE="$2"; shift 2 ;;
|
|
42
126
|
--force-watchdog) FORCE_WATCHDOG=1; shift ;;
|
|
43
127
|
--) shift; break ;;
|
|
44
|
-
-h|--help) usage ;;
|
|
128
|
+
-h|--help) usage help ;;
|
|
45
129
|
*) break ;;
|
|
46
130
|
esac
|
|
47
131
|
done
|
|
@@ -69,9 +153,21 @@ run_watchdog() {
|
|
|
69
153
|
local rss_kb
|
|
70
154
|
rss_kb=$(ps -o rss= -p "$pid" 2>/dev/null | tr -d ' ')
|
|
71
155
|
if [[ -n "$rss_kb" && "$rss_kb" -gt "$limit_kb" ]]; then
|
|
72
|
-
echo "caproom: pid $pid RSS ${rss_kb}KB exceeded ${limit_kb}KB cap —
|
|
73
|
-
kill -
|
|
74
|
-
|
|
156
|
+
echo "caproom: pid $pid RSS ${rss_kb}KB exceeded ${limit_kb}KB cap — sending SIGTERM (grace ${GRACE}s)" >&2
|
|
157
|
+
kill -TERM "$pid" 2>/dev/null || true
|
|
158
|
+
local waited=0
|
|
159
|
+
while kill -0 "$pid" 2>/dev/null && [[ "$waited" -lt "$GRACE" ]]; do
|
|
160
|
+
sleep 1
|
|
161
|
+
waited=$(( waited + 1 ))
|
|
162
|
+
done
|
|
163
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
164
|
+
echo "caproom: pid $pid still alive after ${GRACE}s grace — SIGKILL" >&2
|
|
165
|
+
kill -9 "$pid" 2>/dev/null || true
|
|
166
|
+
exit 137
|
|
167
|
+
fi
|
|
168
|
+
wait "$pid" 2>/dev/null || exit_code=$?
|
|
169
|
+
echo "caproom: pid $pid exited cleanly (code $exit_code) during grace period" >&2
|
|
170
|
+
exit "$exit_code"
|
|
75
171
|
fi
|
|
76
172
|
sleep "$INTERVAL"
|
|
77
173
|
done
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "caproom",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Memory-cap any command (AI coding agents, builds, background jobs) on macOS/Linux — real enforcement via Docker cgroups or a polling watchdog,
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Memory-cap any command (AI coding agents, builds, background jobs) on macOS/Linux — real enforcement via Docker cgroups or a polling watchdog, plus park/wake to reclaim idle process memory without killing.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"caproom": "bin/caproom"
|
|
7
7
|
},
|
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
"memory",
|
|
13
13
|
"oom",
|
|
14
14
|
"cgroup",
|
|
15
|
-
"rlimit",
|
|
16
15
|
"macos",
|
|
17
16
|
"watchdog",
|
|
18
17
|
"ai-agent",
|
|
19
|
-
"process-limit"
|
|
18
|
+
"process-limit",
|
|
19
|
+
"park",
|
|
20
|
+
"idle-memory"
|
|
20
21
|
],
|
|
21
22
|
"os": [
|
|
22
23
|
"darwin",
|