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.
- package/bin/claude-evolve-analyze +36 -12
- package/bin/claude-evolve-autostatus +35 -12
- package/bin/claude-evolve-edit +182 -17
- package/bin/claude-evolve-ideate +130 -40
- package/bin/claude-evolve-migrate-llm-columns +120 -0
- package/bin/claude-evolve-run +71 -1
- package/bin/claude-evolve-setup +1 -1
- package/bin/claude-evolve-status +35 -7
- package/bin/claude-evolve-worker +151 -42
- package/lib/__pycache__/evolution_csv.cpython-311.pyc +0 -0
- package/lib/__pycache__/evolution_csv.cpython-313.pyc +0 -0
- package/lib/ai-cli.sh +45 -14
- package/lib/config.sh +27 -18
- package/lib/csv_fixer.py +35 -0
- package/lib/evolution_csv.py +28 -7
- package/lib/memory_limit_wrapper.py +222 -0
- package/package.json +1 -1
- package/templates/config.yaml +21 -10
|
@@ -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
package/templates/config.yaml
CHANGED
|
@@ -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
|
-
|
|
74
|
-
|
|
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
|