caproom 0.1.0 → 0.2.0

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 +41 -12
  2. package/bin/caproom +63 -4
  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,38 @@ 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
+ ## park / wake — reclaim idle memory without killing
62
+
63
+ 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:
64
+
65
+ ```bash
66
+ caproom park <pid> # SIGSTOP — process stays alive, keeps its PID and state,
67
+ # just isn't scheduled. Its memory becomes eligible for
68
+ # the kernel's own compressor once real system memory
69
+ # pressure shows up.
70
+ caproom wake <pid> # SIGCONT — resumes instantly, same state, no restart.
71
+ caproom status <pid> # pid, state (T = parked, S = running), RSS, elapsed
72
+ ```
73
+
74
+ 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.
75
+
76
+ 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.
52
77
 
53
- On cap breach, `caproom` kills the process and exits `137` (same convention as Docker's own OOM-kill exit code).
78
+ **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.
54
79
 
55
80
  ## Limitations
56
81
 
@@ -58,6 +83,10 @@ On cap breach, `caproom` kills the process and exits `137` (same convention as D
58
83
  - Watchdog backend has a real (if small) race window; for a hard guarantee, use the Docker backend.
59
84
  - 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
85
 
86
+ ## Contributing
87
+
88
+ Issues and PRs welcome at [github.com/intelogroup/caproom](https://github.com/intelogroup/caproom).
89
+
61
90
  ## License
62
91
 
63
92
  MIT
package/bin/caproom CHANGED
@@ -12,26 +12,72 @@ set -euo pipefail
12
12
  usage() {
13
13
  cat >&2 << 'EOF'
14
14
  usage: caproom [--limit <mb>] [--image <docker-image>] [--interval <sec>] -- <command> [args...]
15
+ caproom park <pid>
16
+ caproom wake <pid>
17
+ caproom status <pid>
15
18
 
16
19
  --limit <mb> memory cap in MB (default: 4096)
17
20
  --image <name> docker image to run the command in, when using the docker
18
21
  backend (default: node:22-slim)
19
22
  --interval <sec> watchdog poll interval in seconds, fallback backend only
20
23
  (default: 0.2)
24
+ --grace <sec> seconds to wait after SIGTERM before SIGKILL, watchdog
25
+ backend only (default: 5) — gives the process a chance
26
+ to flush/save state before a hard kill
21
27
  --force-watchdog force the polling watchdog even if Docker is available
22
28
 
23
- env vars (override flags): CAPROOM_LIMIT_MB, CAPROOM_IMAGE, CAPROOM_INTERVAL
29
+ park / wake freeze an idle process so the kernel can reclaim/compress its
30
+ memory without killing it. For a long-running agent sitting on stale
31
+ subprocesses: `caproom park <pid>` (SIGSTOP) instead of killing it. It stays
32
+ alive, keeps its PID, keeps its state — just isn't scheduled and its memory
33
+ becomes eligible for compression under system memory pressure. `caproom wake
34
+ <pid>` (SIGCONT) brings it back instantly, same state, no restart needed.
35
+ Any agent can call these directly — they're just SIGSTOP/SIGCONT, no daemon,
36
+ no tracking file required.
37
+
38
+ env vars (override flags): CAPROOM_LIMIT_MB, CAPROOM_IMAGE, CAPROOM_INTERVAL, CAPROOM_GRACE
24
39
 
25
40
  examples:
26
41
  caproom --limit 2048 -- npm run build
27
42
  caproom --limit 512 -- claude --dangerously-skip-permissions -p "task"
43
+ caproom park 12345
44
+ caproom wake 12345
28
45
  EOF
29
46
  exit 1
30
47
  }
31
48
 
49
+ cmd_park() {
50
+ local pid="${1:-}"
51
+ [[ -z "$pid" ]] && { echo "usage: caproom park <pid>" >&2; exit 1; }
52
+ kill -0 "$pid" 2>/dev/null || { echo "caproom: no such pid $pid" >&2; exit 1; }
53
+ kill -STOP "$pid"
54
+ echo "caproom: pid $pid parked (SIGSTOP) — memory now eligible for kernel reclaim under pressure. wake with: caproom wake $pid" >&2
55
+ }
56
+
57
+ cmd_wake() {
58
+ local pid="${1:-}"
59
+ [[ -z "$pid" ]] && { echo "usage: caproom wake <pid>" >&2; exit 1; }
60
+ kill -0 "$pid" 2>/dev/null || { echo "caproom: no such pid $pid" >&2; exit 1; }
61
+ kill -CONT "$pid"
62
+ echo "caproom: pid $pid woken (SIGCONT)" >&2
63
+ }
64
+
65
+ cmd_status() {
66
+ local pid="${1:-}"
67
+ [[ -z "$pid" ]] && { echo "usage: caproom status <pid>" >&2; exit 1; }
68
+ ps -o pid,stat,rss,etime,command -p "$pid" 2>/dev/null || { echo "caproom: no such pid $pid" >&2; exit 1; }
69
+ }
70
+
71
+ case "${1:-}" in
72
+ park) shift; cmd_park "$@"; exit 0 ;;
73
+ wake) shift; cmd_wake "$@"; exit 0 ;;
74
+ status) shift; cmd_status "$@"; exit 0 ;;
75
+ esac
76
+
32
77
  LIMIT_MB="${CAPROOM_LIMIT_MB:-4096}"
33
78
  IMAGE="${CAPROOM_IMAGE:-node:22-slim}"
34
79
  INTERVAL="${CAPROOM_INTERVAL:-0.2}"
80
+ GRACE="${CAPROOM_GRACE:-5}"
35
81
  FORCE_WATCHDOG=0
36
82
 
37
83
  while [[ $# -gt 0 ]]; do
@@ -39,6 +85,7 @@ while [[ $# -gt 0 ]]; do
39
85
  --limit) LIMIT_MB="$2"; shift 2 ;;
40
86
  --image) IMAGE="$2"; shift 2 ;;
41
87
  --interval) INTERVAL="$2"; shift 2 ;;
88
+ --grace) GRACE="$2"; shift 2 ;;
42
89
  --force-watchdog) FORCE_WATCHDOG=1; shift ;;
43
90
  --) shift; break ;;
44
91
  -h|--help) usage ;;
@@ -69,9 +116,21 @@ run_watchdog() {
69
116
  local rss_kb
70
117
  rss_kb=$(ps -o rss= -p "$pid" 2>/dev/null | tr -d ' ')
71
118
  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
119
+ echo "caproom: pid $pid RSS ${rss_kb}KB exceeded ${limit_kb}KB cap — sending SIGTERM (grace ${GRACE}s)" >&2
120
+ kill -TERM "$pid" 2>/dev/null || true
121
+ local waited=0
122
+ while kill -0 "$pid" 2>/dev/null && [[ "$waited" -lt "$GRACE" ]]; do
123
+ sleep 1
124
+ waited=$(( waited + 1 ))
125
+ done
126
+ if kill -0 "$pid" 2>/dev/null; then
127
+ echo "caproom: pid $pid still alive after ${GRACE}s grace — SIGKILL" >&2
128
+ kill -9 "$pid" 2>/dev/null || true
129
+ exit 137
130
+ fi
131
+ wait "$pid" 2>/dev/null || exit_code=$?
132
+ echo "caproom: pid $pid exited cleanly (code $exit_code) during grace period" >&2
133
+ exit "$exit_code"
75
134
  fi
76
135
  sleep "$INTERVAL"
77
136
  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.2.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, 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",