claude-evolve 1.8.12 → 1.8.13

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.
@@ -407,19 +407,41 @@ with EvolutionCSV('$FULL_CSV_PATH') as csv:
407
407
  eval_arg=""
408
408
  fi
409
409
  local eval_cmd=("$PYTHON_CMD" "$FULL_EVALUATOR_PATH" "$eval_arg")
410
-
410
+
411
411
  # Add memory limiting if configured
412
+ # CRITICAL: Use multiple layers of protection (ulimit + Python wrapper + monitoring)
413
+ local memory_protection=""
412
414
  if [[ -n "$MEMORY_LIMIT_MB" ]] && [[ "$MEMORY_LIMIT_MB" -gt 0 ]]; then
415
+ # Layer 1: ulimit for hard memory limit (kernel-enforced)
416
+ # IMPORTANT: Use -m (RSS) not -v (virtual memory) because:
417
+ # - Neural networks use mmap() which bypasses RLIMIT_AS (-v)
418
+ # - RSS limit is more reliable for actual memory consumption
419
+ # Convert MB to KB for ulimit
420
+ local memory_limit_kb=$((MEMORY_LIMIT_MB * 1024))
421
+
422
+ # Try -m first (RSS limit), fall back to -v if not supported
423
+ if ulimit -m $memory_limit_kb 2>/dev/null; then
424
+ memory_protection="ulimit -m $memory_limit_kb 2>/dev/null; "
425
+ echo "[MEMORY] Layer 1: ulimit -m ${memory_limit_kb}KB (RSS limit - catches neural networks)" >&2
426
+ else
427
+ memory_protection="ulimit -v $memory_limit_kb 2>/dev/null; "
428
+ echo "[MEMORY] Layer 1: ulimit -v ${memory_limit_kb}KB (fallback - may not catch neural networks)" >&2
429
+ fi
430
+
431
+ # Layer 2: Python wrapper with PROCESS TREE monitoring (backup protection)
413
432
  eval_cmd=("$PYTHON_CMD" "$SCRIPT_DIR/../lib/memory_limit_wrapper.py" "$MEMORY_LIMIT_MB" "${eval_cmd[@]}")
433
+
434
+ echo "[MEMORY] Layer 2: Python process tree monitoring (kills entire subprocess tree)" >&2
414
435
  fi
415
-
436
+
416
437
  # Add timeout if configured
417
438
  [[ -n "$timeout_seconds" ]] && eval_cmd=(timeout "$timeout_seconds" "${eval_cmd[@]}")
418
-
419
- # Run evaluation with tee to both display and capture output
439
+
440
+ # Run evaluation with memory protection, tee to both display and capture output
420
441
  # Use stdbuf to disable buffering for real-time output
421
442
  # IMPORTANT: Use PIPESTATUS to get the exit code of the evaluation command, not tee
422
- stdbuf -o0 -e0 "${eval_cmd[@]}" 2>&1 | tee "$eval_output_file" >&2
443
+ # The subshell ensures ulimit is applied before the command runs
444
+ stdbuf -o0 -e0 bash -c "${memory_protection}$(printf '%q ' "${eval_cmd[@]}")" 2>&1 | tee "$eval_output_file" >&2
423
445
  local eval_exit_code=${PIPESTATUS[0]} # Get exit code of first command in pipe
424
446
 
425
447
  if [[ $eval_exit_code -eq 0 ]]; then
@@ -4,6 +4,16 @@ Memory-limited execution wrapper for claude-evolve evaluations.
4
4
 
5
5
  This script runs a command with memory limits to prevent runaway algorithms
6
6
  from consuming all system memory and crashing the machine.
7
+
8
+ CRITICAL: Multi-layer protection approach (both must work together):
9
+ 1. ulimit -m (RSS limit) set by calling shell script - kernel-enforced, catches neural networks
10
+ 2. This Python wrapper monitors ENTIRE PROCESS TREE every 0.1s and kills if limit exceeded
11
+
12
+ AIDEV-NOTE: Previous bugs fixed:
13
+ - ulimit -v (virtual memory) doesn't catch neural networks that use mmap()
14
+ - Was only monitoring direct child, not entire process tree (missed grandchildren)
15
+ - Monitoring interval was 0.5s - too slow for fast memory allocations
16
+ - Resource limit failures were silently ignored instead of failing fast
7
17
  """
8
18
  import sys
9
19
  import os
@@ -11,119 +21,197 @@ import subprocess
11
21
  import signal
12
22
  import time
13
23
  import resource
14
- from typing import Optional
24
+ from typing import Optional, Tuple
15
25
 
16
- def set_memory_limit(limit_mb: int) -> None:
17
- """Set memory limit in MB using resource module."""
26
+ def verify_memory_limit_set(limit_mb: int) -> Tuple[bool, str]:
27
+ """Verify that memory limits are actually enforced."""
28
+ try:
29
+ limit_bytes = limit_mb * 1024 * 1024
30
+
31
+ # Check RLIMIT_AS (virtual memory)
32
+ soft_as, hard_as = resource.getrlimit(resource.RLIMIT_AS)
33
+ if soft_as != resource.RLIM_INFINITY and soft_as <= limit_bytes * 1.1:
34
+ return True, f"RLIMIT_AS set to {soft_as / (1024*1024):.0f}MB"
35
+
36
+ # Check RLIMIT_DATA (data segment)
37
+ try:
38
+ soft_data, hard_data = resource.getrlimit(resource.RLIMIT_DATA)
39
+ if soft_data != resource.RLIM_INFINITY and soft_data <= limit_bytes * 1.1:
40
+ return True, f"RLIMIT_DATA set to {soft_data / (1024*1024):.0f}MB"
41
+ except (OSError, ValueError):
42
+ pass
43
+
44
+ return False, "No hard memory limits detected"
45
+ except Exception as e:
46
+ return False, f"Error checking limits: {e}"
47
+
48
+ def set_memory_limit(limit_mb: int) -> bool:
49
+ """
50
+ Set memory limit in MB using resource module.
51
+ Returns True if successful, False otherwise.
52
+ """
18
53
  try:
19
54
  # Convert MB to bytes
20
55
  limit_bytes = limit_mb * 1024 * 1024
21
-
56
+
22
57
  # Set virtual memory limit (address space)
23
58
  # On macOS this is the most reliable way to limit memory
24
59
  resource.setrlimit(resource.RLIMIT_AS, (limit_bytes, limit_bytes))
25
-
60
+
26
61
  # Also try to set data segment limit if available
27
62
  try:
28
63
  resource.setrlimit(resource.RLIMIT_DATA, (limit_bytes, limit_bytes))
29
64
  except (OSError, ValueError):
30
65
  # Not available on all systems
31
66
  pass
32
-
33
- print(f"[MEMORY] Set memory limit to {limit_mb}MB", file=sys.stderr)
34
-
67
+
68
+ # Verify it was actually set
69
+ is_set, msg = verify_memory_limit_set(limit_mb)
70
+ if is_set:
71
+ print(f"[MEMORY] ✓ Hard limit enforced: {msg}", file=sys.stderr)
72
+ return True
73
+ else:
74
+ print(f"[MEMORY] ✗ Hard limit NOT enforced: {msg}", file=sys.stderr)
75
+ return False
76
+
35
77
  except (OSError, ValueError) as e:
36
- print(f"[MEMORY] Warning: Could not set memory limit: {e}", file=sys.stderr)
78
+ print(f"[MEMORY] Could not set memory limit: {e}", file=sys.stderr)
79
+ return False
80
+
81
+ def get_process_tree_memory_native(pid: int) -> float:
82
+ """Get total memory usage of entire process group using native ps command."""
83
+ try:
84
+ # Get the process group ID
85
+ pgid = os.getpgid(pid)
86
+
87
+ # Get all processes in the process group
88
+ ps_result = subprocess.run(
89
+ ["ps", "-o", "rss=", "-g", str(pgid)],
90
+ capture_output=True,
91
+ text=True,
92
+ timeout=1
93
+ )
94
+
95
+ if ps_result.returncode != 0:
96
+ return 0.0
97
+
98
+ # Sum all RSS values from the process group
99
+ total_rss_kb = 0
100
+ for line in ps_result.stdout.strip().split('\n'):
101
+ line = line.strip()
102
+ if line:
103
+ try:
104
+ total_rss_kb += int(line)
105
+ except ValueError:
106
+ continue
107
+
108
+ # Convert KB to MB
109
+ return total_rss_kb / 1024.0
110
+ except Exception:
111
+ return 0.0
37
112
 
38
113
  def monitor_memory_usage_native(process: subprocess.Popen, limit_mb: int) -> Optional[str]:
39
- """Monitor process memory usage using native tools and kill if it exceeds limits."""
40
- # print(f"[MEMORY] Starting native monitoring for PID {process.pid} with limit {limit_mb}MB", file=sys.stderr)
41
-
114
+ """Monitor ENTIRE PROCESS TREE memory usage using native tools and kill if it exceeds limits."""
115
+ print(f"[MEMORY] Monitoring process tree from root PID {process.pid} (limit: {limit_mb}MB)", file=sys.stderr)
116
+
42
117
  while process.poll() is None:
43
118
  try:
44
- # Use ps command to get memory usage
45
- ps_result = subprocess.run(
46
- ["ps", "-o", "rss=", "-p", str(process.pid)],
47
- capture_output=True,
48
- text=True,
49
- timeout=1
50
- )
51
-
52
- if ps_result.returncode == 0 and ps_result.stdout.strip():
53
- # ps returns RSS in KB, convert to MB
54
- memory_kb = int(ps_result.stdout.strip())
55
- memory_mb = memory_kb / 1024
56
-
57
- # print(f"[MEMORY] PID {process.pid} using {memory_mb:.1f}MB (limit: {limit_mb}MB)", file=sys.stderr)
58
-
59
- if memory_mb > limit_mb:
60
- print(f"[MEMORY] Process exceeded {limit_mb}MB limit (using {memory_mb:.1f}MB), terminating", file=sys.stderr)
61
- # Kill the entire process group - fix race condition
62
- try:
63
- pgid = os.getpgid(process.pid)
64
- os.killpg(pgid, signal.SIGTERM)
65
- except ProcessLookupError:
66
- return f"Memory limit exceeded: {memory_mb:.1f}MB > {limit_mb}MB"
67
-
68
- time.sleep(2) # Give it time to cleanup
69
-
70
- try:
71
- if process.poll() is None:
72
- pgid = os.getpgid(process.pid)
73
- os.killpg(pgid, signal.SIGKILL)
74
- except ProcessLookupError:
75
- pass
119
+ # Get total memory for entire process tree
120
+ memory_mb = get_process_tree_memory_native(process.pid)
121
+
122
+ if memory_mb > limit_mb:
123
+ print(f"[MEMORY] Process tree exceeded {limit_mb}MB limit (using {memory_mb:.1f}MB), terminating entire tree", file=sys.stderr)
124
+ # Kill the entire process group
125
+ try:
126
+ pgid = os.getpgid(process.pid)
127
+ os.killpg(pgid, signal.SIGTERM)
128
+ except ProcessLookupError:
76
129
  return f"Memory limit exceeded: {memory_mb:.1f}MB > {limit_mb}MB"
77
-
78
- time.sleep(0.5) # Check every 500ms
79
-
130
+
131
+ time.sleep(2) # Give it time to cleanup
132
+
133
+ try:
134
+ if process.poll() is None:
135
+ pgid = os.getpgid(process.pid)
136
+ os.killpg(pgid, signal.SIGKILL)
137
+ except ProcessLookupError:
138
+ pass
139
+ return f"Memory limit exceeded: {memory_mb:.1f}MB > {limit_mb}MB"
140
+
141
+ time.sleep(0.1) # Check every 100ms for faster response
142
+
80
143
  except (subprocess.TimeoutExpired, ValueError, ProcessLookupError):
81
144
  # Process might have terminated or ps command failed
82
- time.sleep(0.5)
145
+ time.sleep(0.1)
83
146
  continue
84
-
85
- # print(f"[MEMORY] Monitoring stopped for PID {process.pid}", file=sys.stderr)
147
+
86
148
  return None
87
149
 
150
+ def get_process_tree_memory_psutil(ps_process) -> float:
151
+ """Get total memory usage of entire process tree using psutil."""
152
+ try:
153
+ import psutil
154
+ total_mb = 0.0
155
+
156
+ # Get memory of root process
157
+ try:
158
+ total_mb += ps_process.memory_info().rss / (1024 * 1024)
159
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
160
+ return 0.0
161
+
162
+ # Get memory of all children (recursive)
163
+ try:
164
+ for child in ps_process.children(recursive=True):
165
+ try:
166
+ total_mb += child.memory_info().rss / (1024 * 1024)
167
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
168
+ continue
169
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
170
+ pass
171
+
172
+ return total_mb
173
+ except ImportError:
174
+ return 0.0
175
+
88
176
  def monitor_memory_usage(process: subprocess.Popen, limit_mb: int) -> Optional[str]:
89
- """Monitor process memory usage and kill if it exceeds limits."""
177
+ """Monitor ENTIRE PROCESS TREE memory usage and kill if it exceeds limits."""
90
178
  try:
91
179
  import psutil
92
180
  ps_process = psutil.Process(process.pid)
93
-
181
+ print(f"[MEMORY] Monitoring process tree from root PID {process.pid} (limit: {limit_mb}MB, using psutil)", file=sys.stderr)
182
+
94
183
  while process.poll() is None:
95
184
  try:
96
- # Get memory usage in MB
97
- memory_info = ps_process.memory_info()
98
- memory_mb = memory_info.rss / (1024 * 1024)
99
-
185
+ # Get total memory for entire process tree
186
+ memory_mb = get_process_tree_memory_psutil(ps_process)
187
+
100
188
  if memory_mb > limit_mb:
101
- print(f"[MEMORY] Process exceeded {limit_mb}MB limit (using {memory_mb:.1f}MB), terminating", file=sys.stderr)
102
- # Kill the entire process group - fix race condition
189
+ print(f"[MEMORY] Process tree exceeded {limit_mb}MB limit (using {memory_mb:.1f}MB), terminating entire tree", file=sys.stderr)
190
+ # Kill the entire process group
103
191
  try:
104
192
  pgid = os.getpgid(process.pid)
105
193
  os.killpg(pgid, signal.SIGTERM)
106
194
  except ProcessLookupError:
107
195
  return f"Memory limit exceeded: {memory_mb:.1f}MB > {limit_mb}MB"
108
-
196
+
109
197
  time.sleep(2) # Give it time to cleanup
110
-
198
+
111
199
  try:
112
200
  if process.poll() is None:
113
- pgid = os.getpgid(process.pid)
201
+ pgid = os.getpgid(process.pid)
114
202
  os.killpg(pgid, signal.SIGKILL)
115
203
  except ProcessLookupError:
116
204
  pass
117
205
  return f"Memory limit exceeded: {memory_mb:.1f}MB > {limit_mb}MB"
118
-
119
- time.sleep(0.5) # Check every 500ms
206
+
207
+ time.sleep(0.1) # Check every 100ms for faster response
120
208
  except (psutil.NoSuchProcess, psutil.AccessDenied):
121
209
  # Process already terminated
122
210
  break
123
211
  except ImportError:
124
212
  # psutil not available, use native monitoring
125
213
  return monitor_memory_usage_native(process, limit_mb)
126
-
214
+
127
215
  return None
128
216
 
129
217
  def validate_memory_limit(limit_mb: int) -> bool:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-evolve",
3
- "version": "1.8.12",
3
+ "version": "1.8.13",
4
4
  "bin": {
5
5
  "claude-evolve": "./bin/claude-evolve",
6
6
  "claude-evolve-main": "./bin/claude-evolve-main",