claude-evolve 1.5.3 → 1.5.6

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.
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Memory-limited execution wrapper for claude-evolve evaluations.
4
+
5
+ This script runs a command with memory limits to prevent runaway algorithms
6
+ from consuming all system memory and crashing the machine.
7
+ """
8
+ import sys
9
+ import os
10
+ import subprocess
11
+ import signal
12
+ import time
13
+ import resource
14
+ from typing import Optional
15
+
16
+ def set_memory_limit(limit_mb: int) -> None:
17
+ """Set memory limit in MB using resource module."""
18
+ try:
19
+ # Convert MB to bytes
20
+ limit_bytes = limit_mb * 1024 * 1024
21
+
22
+ # Set virtual memory limit (address space)
23
+ # On macOS this is the most reliable way to limit memory
24
+ resource.setrlimit(resource.RLIMIT_AS, (limit_bytes, limit_bytes))
25
+
26
+ # Also try to set data segment limit if available
27
+ try:
28
+ resource.setrlimit(resource.RLIMIT_DATA, (limit_bytes, limit_bytes))
29
+ except (OSError, ValueError):
30
+ # Not available on all systems
31
+ pass
32
+
33
+ print(f"[MEMORY] Set memory limit to {limit_mb}MB", file=sys.stderr)
34
+
35
+ except (OSError, ValueError) as e:
36
+ print(f"[MEMORY] Warning: Could not set memory limit: {e}", file=sys.stderr)
37
+
38
+ 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
+
42
+ while process.poll() is None:
43
+ 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
76
+ return f"Memory limit exceeded: {memory_mb:.1f}MB > {limit_mb}MB"
77
+
78
+ time.sleep(0.5) # Check every 500ms
79
+
80
+ except (subprocess.TimeoutExpired, ValueError, ProcessLookupError):
81
+ # Process might have terminated or ps command failed
82
+ time.sleep(0.5)
83
+ continue
84
+
85
+ # print(f"[MEMORY] Monitoring stopped for PID {process.pid}", file=sys.stderr)
86
+ return None
87
+
88
+ def monitor_memory_usage(process: subprocess.Popen, limit_mb: int) -> Optional[str]:
89
+ """Monitor process memory usage and kill if it exceeds limits."""
90
+ try:
91
+ import psutil
92
+ ps_process = psutil.Process(process.pid)
93
+
94
+ while process.poll() is None:
95
+ try:
96
+ # Get memory usage in MB
97
+ memory_info = ps_process.memory_info()
98
+ memory_mb = memory_info.rss / (1024 * 1024)
99
+
100
+ 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
103
+ try:
104
+ pgid = os.getpgid(process.pid)
105
+ os.killpg(pgid, signal.SIGTERM)
106
+ except ProcessLookupError:
107
+ return f"Memory limit exceeded: {memory_mb:.1f}MB > {limit_mb}MB"
108
+
109
+ time.sleep(2) # Give it time to cleanup
110
+
111
+ try:
112
+ if process.poll() is None:
113
+ pgid = os.getpgid(process.pid)
114
+ os.killpg(pgid, signal.SIGKILL)
115
+ except ProcessLookupError:
116
+ pass
117
+ return f"Memory limit exceeded: {memory_mb:.1f}MB > {limit_mb}MB"
118
+
119
+ time.sleep(0.5) # Check every 500ms
120
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
121
+ # Process already terminated
122
+ break
123
+ except ImportError:
124
+ # psutil not available, use native monitoring
125
+ return monitor_memory_usage_native(process, limit_mb)
126
+
127
+ return None
128
+
129
+ def validate_memory_limit(limit_mb: int) -> bool:
130
+ """Validate memory limit against system resources."""
131
+ if limit_mb <= 0:
132
+ return True # 0 or negative means disabled
133
+
134
+ # Basic sanity checks
135
+ if limit_mb < 10:
136
+ print(f"[MEMORY] Warning: Memory limit {limit_mb}MB is very small", file=sys.stderr)
137
+ elif limit_mb > 64000:
138
+ print(f"[MEMORY] Warning: Memory limit {limit_mb}MB is very large", file=sys.stderr)
139
+
140
+ return True
141
+
142
+ def main():
143
+ if len(sys.argv) < 3:
144
+ print("Usage: memory_limit_wrapper.py <memory_limit_mb> <command> [args...]", file=sys.stderr)
145
+ sys.exit(1)
146
+
147
+ try:
148
+ memory_limit_mb = int(sys.argv[1])
149
+ except ValueError:
150
+ print(f"Error: Invalid memory limit '{sys.argv[1]}' - must be integer MB", file=sys.stderr)
151
+ sys.exit(1)
152
+
153
+ if not validate_memory_limit(memory_limit_mb):
154
+ sys.exit(1)
155
+
156
+ command = sys.argv[2:]
157
+
158
+ if memory_limit_mb <= 0:
159
+ print("[MEMORY] No memory limit set (0 or negative value)", file=sys.stderr)
160
+ # Just exec the command directly without limits
161
+ os.execvp(command[0], command)
162
+
163
+ # Set memory limits for this process (inherited by subprocess)
164
+ set_memory_limit(memory_limit_mb)
165
+
166
+ try:
167
+ # Start process in new process group for easier cleanup
168
+ process = subprocess.Popen(
169
+ command,
170
+ preexec_fn=os.setsid, # Create new process group
171
+ stdout=subprocess.PIPE,
172
+ stderr=subprocess.STDOUT,
173
+ universal_newlines=True,
174
+ bufsize=1 # Line buffered
175
+ )
176
+
177
+ # Monitor memory usage in background
178
+ memory_error = None
179
+ import threading
180
+
181
+ def memory_monitor():
182
+ nonlocal memory_error
183
+ memory_error = monitor_memory_usage(process, memory_limit_mb)
184
+
185
+ monitor_thread = threading.Thread(target=memory_monitor, daemon=True)
186
+ monitor_thread.start()
187
+
188
+ # Stream output in real-time
189
+ while True:
190
+ output = process.stdout.readline()
191
+ if output == '' and process.poll() is not None:
192
+ break
193
+ if output:
194
+ print(output.rstrip())
195
+ sys.stdout.flush()
196
+
197
+ # Wait for completion
198
+ return_code = process.wait()
199
+
200
+ # Check if we killed it due to memory
201
+ if memory_error:
202
+ print(f"[MEMORY] {memory_error}", file=sys.stderr)
203
+ sys.exit(137) # 128 + SIGKILL
204
+
205
+ sys.exit(return_code)
206
+
207
+ except FileNotFoundError:
208
+ print(f"Error: Command not found: {command[0]}", file=sys.stderr)
209
+ sys.exit(127)
210
+ except KeyboardInterrupt:
211
+ print("[MEMORY] Interrupted by user", file=sys.stderr)
212
+ try:
213
+ os.killpg(os.getpgid(process.pid), signal.SIGTERM)
214
+ except:
215
+ pass
216
+ sys.exit(130)
217
+ except Exception as e:
218
+ print(f"[MEMORY] Unexpected error: {e}", file=sys.stderr)
219
+ sys.exit(1)
220
+
221
+ if __name__ == "__main__":
222
+ main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-evolve",
3
- "version": "1.5.3",
3
+ "version": "1.5.6",
4
4
  "bin": {
5
5
  "claude-evolve": "./bin/claude-evolve",
6
6
  "claude-evolve-main": "./bin/claude-evolve-main",
@@ -46,6 +46,13 @@ auto_ideate: true
46
46
  # Maximum number of retries for failed candidates before marking as permanently failed
47
47
  max_retries: 3
48
48
 
49
+ # Memory protection configuration
50
+ # Memory limit in MB for evaluation processes (0 = no limit)
51
+ # This prevents runaway algorithms from consuming all system memory
52
+ # Default: 12GB (reasonable for ML workloads, adjust based on your system RAM)
53
+ # Recommendation: Set to ~50-75% of available system RAM
54
+ memory_limit_mb: 12288
55
+
49
56
  # Parallel execution configuration
50
57
  parallel:
51
58
  # Enable parallel execution of evolution candidates
@@ -59,16 +66,20 @@ parallel:
59
66
 
60
67
  # LLM/AI CLI configuration
61
68
  llm_cli:
62
- # How to run each CLI for each LLM option
63
- # {{PROMPT}} will be replaced with the actual prompt text
64
- o3: 'codex exec -m o3 --dangerously-bypass-approvals-and-sandbox "{{PROMPT}}"'
65
- codex: 'codex exec --dangerously-bypass-approvals-and-sandbox "{{PROMPT}}"'
66
- gemini: 'gemini -y -p "{{PROMPT}}"'
67
- opus: 'claude --dangerously-skip-permissions --model opus -p "{{PROMPT}}"'
68
- sonnet: 'claude --dangerously-skip-permissions --model sonnet -p "{{PROMPT}}"'
69
-
70
69
  # What to run for each sub-command
71
70
  # Models are tried in order, with round-robin distribution across candidates
72
71
  # You can repeat models for weighted selection (e.g., "sonnet sonnet gemini" for 2:1 ratio)
73
- run: sonnet
74
- ideate: gemini o3 opus
72
+
73
+ # commented out because these change over time; if you want to fix them in a particular
74
+ # configuration, uncomment them and set them
75
+ #run: sonnet cursor-sonnet
76
+ #ideate: gemini opus gpt5high o3high cursor-opus
77
+
78
+ # Available models:
79
+ # - sonnet: Claude 3.5 Sonnet via Claude CLI
80
+ # - opus: Claude 3 Opus via Claude CLI
81
+ # - gemini: Gemini via Gemini CLI
82
+ # - gpt5high: GPT-5 via Codex CLI (high reasoning)
83
+ # - o3high: O3 via Codex CLI (high reasoning)
84
+ # - cursor-sonnet: Claude 3.5 Sonnet via Cursor Agent CLI
85
+ # - cursor-opus: Claude 3 Opus via Cursor Agent CLI