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.
Files changed (3) hide show
  1. package/README.md +55 -12
  2. package/bin/caproom +103 -7
  3. package/package.json +5 -4
package/README.md CHANGED
@@ -1,15 +1,13 @@
1
1
  # caproom
2
2
 
3
- Memory-cap any command — AI coding agents, builds, background jobs — on macOS/Linux.
3
+ [![npm](https://img.shields.io/npm/v/caproom.svg)](https://www.npmjs.com/package/caproom)
4
+ [![license](https://img.shields.io/npm/l/caproom.svg)](LICENSE)
4
5
 
5
- ## Why
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
- - `setrlimit(RLIMIT_AS, ...)`, `RLIMIT_DATA`, `RLIMIT_RSS` all return `EINVAL` on modern macOS — the kernel rejects them outright, `ulimit -v` included.
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
- So a runaway process — a leaking build tool, an AI agent that gets stuck in a loop, a stray `find /` — has nothing standing between it and system OOM on macOS. `caproom` fills that gap with the two mechanisms that actually work.
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
- Env var overrides: `CAPROOM_LIMIT_MB`, `CAPROOM_IMAGE`, `CAPROOM_INTERVAL`.
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
- On cap breach, `caproom` kills the process and exits `137` (same convention as Docker's own OOM-kill exit code).
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
- cat >&2 << 'EOF'
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
- env vars (override flags): CAPROOM_LIMIT_MB, CAPROOM_IMAGE, CAPROOM_INTERVAL
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 — killing" >&2
73
- kill -9 "$pid" 2>/dev/null || true
74
- exit 137
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.0",
4
- "description": "Memory-cap any command (AI coding agents, builds, background jobs) on macOS/Linux — real enforcement via Docker cgroups or a polling watchdog, since macOS has no working per-process memory rlimit.",
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",