From bbf3b871b39df7bf01496d636810949a0f317616 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 14 Mar 2026 17:21:51 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20feat:=20evolution-daemon.sh=20=E2=80=94?= =?UTF-8?q?=20perpetual=20evolution=20loop=20on=20DO=20box=20(#748)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add tools/push3-evolution/evolution-daemon.sh: single-command daemon that runs git-pull → apply-patch → clean-tmpdirs → evolve.sh → summary → notify → revert-patch → loop, handling SIGINT/SIGTERM cleanly. - Add tools/push3-evolution/evolution.conf: config file (EVAL_MODE, BASE_RPC_URL, POPULATION=20, GENERATIONS=30, MUTATION_RATE=1, ELITES=2, DIVERSE_SEEDS=true, GAS_LIMIT=500000, ANCHOR_WIDTH_UNBOUNDED=true). - Add tools/push3-evolution/evolution.patch: overrides CALCULATE_PARAMS_GAS_LIMIT 200k→500k in Optimizer.sol + FitnessEvaluator.t.sol, and removes MAX_ANCHOR_WIDTH=100 cap in LiquidityManager.sol for unbounded AW exploration. Co-Authored-By: Claude Sonnet 4.6 --- STATE.md | 1 + tools/push3-evolution/evolution-daemon.sh | 342 ++++++++++++++++++++++ tools/push3-evolution/evolution.conf | 36 +++ tools/push3-evolution/evolution.patch | 39 +++ 4 files changed, 418 insertions(+) create mode 100755 tools/push3-evolution/evolution-daemon.sh create mode 100644 tools/push3-evolution/evolution.conf create mode 100644 tools/push3-evolution/evolution.patch diff --git a/STATE.md b/STATE.md index 5b28266..242481d 100644 --- a/STATE.md +++ b/STATE.md @@ -26,3 +26,4 @@ - [2026-03-14] LLM seed — Defensive Floor Hugger optimizer (#672) - [2026-03-14] evolve.sh stale tmpdirs break subsequent runs (#750) - [2026-03-14] evolve.sh silences all batch-eval errors with 2>/dev/null (#749) +- [2026-03-14] evolution-daemon.sh — perpetual evolution loop on DO box (#748) diff --git a/tools/push3-evolution/evolution-daemon.sh b/tools/push3-evolution/evolution-daemon.sh new file mode 100755 index 0000000..9a24563 --- /dev/null +++ b/tools/push3-evolution/evolution-daemon.sh @@ -0,0 +1,342 @@ +#!/usr/bin/env bash +# ============================================================================= +# evolution-daemon.sh — perpetual Push3 evolution loop +# +# Wraps the full per-run cycle so that a single command starts continuous +# evolution on a DigitalOcean (or similar) box with no manual intervention. +# +# Usage: +# cd +# BASE_RPC_URL=https://mainnet.base.org \ +# ./tools/push3-evolution/evolution-daemon.sh +# +# Per-run cycle: +# 1. git pull origin master — sync latest code +# 2. git apply evolution.patch — unbounded AW, gas limit override +# 3. Clean stale /tmp/tmp.* dirs — prevent interference from killed runs +# 4. Run evolve.sh — full evolution pipeline +# 5. Results already in evolved/run_NNN/ (evolve.sh auto-increments) +# 6. Admission already done by evolve.sh (step 5 of its pipeline) +# 7. Write summary report — best fitness, improvement, duration +# 8. Notify via openclaw — SSH to main VPS +# 9. git apply --reverse — revert evolution patches +# 10. Loop +# +# Configuration: +# Load from tools/push3-evolution/evolution.conf (co-located with this script). +# BASE_RPC_URL must be set in the environment or in evolution.conf. +# +# Signals: +# SIGINT / SIGTERM — finish the current run cleanly, then exit. +# ============================================================================= + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +CONF_FILE="$SCRIPT_DIR/evolution.conf" +PATCH_FILE="$SCRIPT_DIR/evolution.patch" +EVOLVE_SH="$SCRIPT_DIR/evolve.sh" + +# ============================================================================= +# Load config +# ============================================================================= + +if [ ! -f "$CONF_FILE" ]; then + echo "[daemon] ERROR: config file not found: $CONF_FILE" >&2 + exit 2 +fi + +# Source the config so all variables are available. +# shellcheck source=evolution.conf +. "$CONF_FILE" + +# Required: BASE_RPC_URL may come from the environment or from the conf file. +BASE_RPC_URL="${BASE_RPC_URL:-}" +if [ -z "$BASE_RPC_URL" ] && [ "${EVAL_MODE:-revm}" = "revm" ]; then + echo "[daemon] ERROR: BASE_RPC_URL is not set. Set it in the environment or in $CONF_FILE" >&2 + exit 2 +fi +export BASE_RPC_URL + +# Resolve seed path (relative to repo root if not absolute). +SEED="${SEED:-tools/push3-evolution/seeds/optimizer_v3.push3}" +if [[ "$SEED" != /* ]]; then + SEED="$REPO_ROOT/$SEED" +fi + +# Optional defaults for variables the conf might not set. +EVAL_MODE="${EVAL_MODE:-revm}" +POPULATION="${POPULATION:-20}" +GENERATIONS="${GENERATIONS:-30}" +MUTATION_RATE="${MUTATION_RATE:-1}" +ELITES="${ELITES:-2}" +DIVERSE_SEEDS="${DIVERSE_SEEDS:-true}" +OPENCLAW_SSH_TARGET="${OPENCLAW_SSH_TARGET:-}" + +# Output directory (relative to repo root so evolve.sh's auto-increment finds prior runs). +OUTPUT_DIR="$REPO_ROOT/evolved" + +# ============================================================================= +# Patch state tracking +# ============================================================================= + +PATCH_APPLIED=false + +cleanup_patch() { + if [ "$PATCH_APPLIED" = "true" ]; then + echo "[daemon] Reverting evolution patches…" >&2 + (cd "$REPO_ROOT" && git apply --reverse "$PATCH_FILE") 2>/dev/null || true + PATCH_APPLIED=false + fi +} + +# ============================================================================= +# Signal handling — finish current run, then exit cleanly +# ============================================================================= + +STOP_REQUESTED=false + +handle_signal() { + echo "" >&2 + echo "[daemon] Stop requested — will exit after current run completes." >&2 + STOP_REQUESTED=true +} + +trap handle_signal SIGINT SIGTERM +trap cleanup_patch EXIT + +# ============================================================================= +# Helpers +# ============================================================================= + +log() { + echo "[daemon] $*" >&2 +} + +ts() { + date -u '+%Y-%m-%dT%H:%M:%SZ' +} + +notify() { + local msg="$*" + if [ -n "$OPENCLAW_SSH_TARGET" ]; then + ssh "$OPENCLAW_SSH_TARGET" "openclaw system event '$msg'" 2>/dev/null || true + fi +} + +# ============================================================================= +# Pre-flight checks +# ============================================================================= + +[ -f "$EVOLVE_SH" ] || { log "ERROR: evolve.sh not found at $EVOLVE_SH"; exit 2; } +[ -x "$EVOLVE_SH" ] || chmod +x "$EVOLVE_SH" +[ -f "$SEED" ] || { log "ERROR: seed file not found: $SEED"; exit 2; } + +if [ -f "$PATCH_FILE" ] && [ -s "$PATCH_FILE" ]; then + HAS_PATCH=true +else + HAS_PATCH=false + log "WARNING: patch file is empty or missing — no evolution-specific overrides will be applied" +fi + +log "========================================================" +log "evolution-daemon.sh — $(ts)" +log " Repo: $REPO_ROOT" +log " Seed: $SEED" +log " Config: $CONF_FILE" +log " Patch: $PATCH_FILE (has_patch=$HAS_PATCH)" +log " Eval mode: $EVAL_MODE" +log " Population: $POPULATION" +log " Generations: $GENERATIONS" +log " Mutation: $MUTATION_RATE" +log " Elites: $ELITES" +log " Diverse: $DIVERSE_SEEDS" +log " Output dir: $OUTPUT_DIR" +log " Notify via: ${OPENCLAW_SSH_TARGET:-}" +log "========================================================" + +RUN_NUM=0 + +# ============================================================================= +# Main loop +# ============================================================================= + +while true; do + + RUN_NUM=$((RUN_NUM + 1)) + RUN_START="$(date +%s)" + + log "" + log "════════════════════════════════════════════════════" + log "Run #${RUN_NUM} — $(ts)" + log "════════════════════════════════════════════════════" + + # ── Step 1: Sync master ────────────────────────────────────────────────────── + + log "[1/7] Syncing master…" + if (cd "$REPO_ROOT" && git pull origin master --ff-only 2>&1); then + log " git pull OK" + else + log " WARNING: git pull failed — continuing with current tree" + fi + + # ── Step 2: Apply evolution patches ───────────────────────────────────────── + + PATCH_APPLIED=false + if [ "$HAS_PATCH" = "true" ]; then + log "[2/7] Applying evolution patches…" + if (cd "$REPO_ROOT" && git apply "$PATCH_FILE"); then + PATCH_APPLIED=true + log " Patches applied OK" + else + log " WARNING: patch failed to apply — running without evolution-specific overrides" + fi + else + log "[2/7] No patch file — skipping" + fi + + # ── Step 3: Clean stale tmpdirs ───────────────────────────────────────────── + + log "[3/7] Cleaning stale /tmp/tmp.* directories…" + STALE_COUNT=0 + # Only remove directories older than 1 hour to avoid disturbing very recent runs. + while IFS= read -r -d '' STALE_DIR; do + rm -rf "$STALE_DIR" + STALE_COUNT=$((STALE_COUNT + 1)) + done < <(find /tmp -maxdepth 1 -name 'tmp.*' -type d -mmin +60 -print0 2>/dev/null) + log " Removed $STALE_COUNT stale tmpdir(s)" + + # ── Step 4: Run evolve.sh ──────────────────────────────────────────────────── + + log "[4/7] Starting evolve.sh…" + + DIVERSE_FLAG="" + [ "$DIVERSE_SEEDS" = "true" ] && DIVERSE_FLAG="--diverse-seeds" + + EVOLVE_EC=0 + EVOLVE_OUT="" + EVOLVE_OUT=$( + EVAL_MODE="$EVAL_MODE" \ + BASE_RPC_URL="$BASE_RPC_URL" \ + bash "$EVOLVE_SH" \ + --seed "$SEED" \ + --population "$POPULATION" \ + --generations "$GENERATIONS" \ + --mutation-rate "$MUTATION_RATE" \ + --elites "$ELITES" \ + --output "$OUTPUT_DIR" \ + $DIVERSE_FLAG \ + 2>&1 + ) || EVOLVE_EC=$? + + # Always print evolve.sh output for visibility. + printf '%s\n' "$EVOLVE_OUT" >&2 + + if [ "$EVOLVE_EC" -ne 0 ]; then + log " WARNING: evolve.sh exited $EVOLVE_EC — results may be incomplete" + else + log " evolve.sh completed OK" + fi + + # ── Step 5: Locate the run directory just created ──────────────────────────── + + # evolve.sh already saves to evolved/run_NNN/ and admits to seed pool. + # Find the most recent run dir to extract summary data. + LATEST_RUN_DIR="" + LATEST_RUN_DIR=$(python3 - "$OUTPUT_DIR" <<'PYEOF' 2>/dev/null || true +import sys, os, re +base = sys.argv[1] +max_n = -1 +best_dir = '' +if os.path.isdir(base): + for name in os.listdir(base): + m = re.fullmatch(r'run_(\d+)', name) + if m and os.path.isdir(os.path.join(base, name)): + n = int(m.group(1)) + if n > max_n: + max_n = n + best_dir = os.path.join(base, name) +print(best_dir) +PYEOF +) + + BEST_FITNESS=0 + BEST_RUN_DIR="${LATEST_RUN_DIR:-}" + + if [ -n "$LATEST_RUN_DIR" ] && [ -d "$LATEST_RUN_DIR" ]; then + # Extract best fitness from the run's generation JSONL files. + BEST_FITNESS=$(python3 - "$LATEST_RUN_DIR" <<'PYEOF' 2>/dev/null || echo 0 +import json, sys, os +run_dir = sys.argv[1] +best = 0 +for fname in sorted(os.listdir(run_dir)): + if not (fname.startswith('generation_') and fname.endswith('.jsonl')): + continue + with open(os.path.join(run_dir, fname)) as f: + for line in f: + try: + d = json.loads(line) + fitness = int(d.get('fitness', 0)) + if fitness > best: + best = fitness + except (json.JSONDecodeError, ValueError, TypeError): + pass +print(best) +PYEOF +) + log "[5/7] Results: dir=$LATEST_RUN_DIR best_fitness=$BEST_FITNESS" + else + log "[5/7] WARNING: could not locate run output directory" + fi + + # ── Steps 6 (seed admission already done by evolve.sh) ────────────────────── + # evolve.sh step 5 handles pool admission automatically. + + # ── Step 7: Write summary report ──────────────────────────────────────────── + + RUN_END="$(date +%s)" + DURATION=$(( RUN_END - RUN_START )) + DURATION_FMT="$(printf '%02d:%02d:%02d' $((DURATION/3600)) $(( (DURATION%3600)/60 )) $((DURATION%60)))" + + if [ -n "$LATEST_RUN_DIR" ] && [ -d "$LATEST_RUN_DIR" ]; then + SUMMARY_FILE="$LATEST_RUN_DIR/daemon-summary.txt" + { + echo "=== Evolution Daemon Run Summary ===" + echo "Timestamp: $(ts)" + echo "Run dir: $LATEST_RUN_DIR" + echo "Daemon run #: $RUN_NUM" + echo "Duration: $DURATION_FMT" + echo "Best fitness: $BEST_FITNESS" + echo "Eval mode: $EVAL_MODE" + echo "Population: $POPULATION" + echo "Generations: $GENERATIONS" + echo "Diverse seeds: $DIVERSE_SEEDS" + echo "Patch applied: $PATCH_APPLIED" + echo "evolve.sh exit:$EVOLVE_EC" + } > "$SUMMARY_FILE" + log "[6/7] Summary written to $SUMMARY_FILE" + fi + + # ── Step 8: Notify ────────────────────────────────────────────────────────── + + NOTIFY_MSG="evolution run #${RUN_NUM} complete — best_fitness=${BEST_FITNESS} duration=${DURATION_FMT} dir=$(basename "${LATEST_RUN_DIR:-unknown}")" + log "[7/7] Notifying: $NOTIFY_MSG" + notify "$NOTIFY_MSG" + + # ── Revert patches ────────────────────────────────────────────────────────── + + cleanup_patch + + # ── Check stop flag ───────────────────────────────────────────────────────── + + if [ "$STOP_REQUESTED" = "true" ]; then + log "" + log "Stop requested — daemon exiting after run #${RUN_NUM}." + exit 0 + fi + + log "Run #${RUN_NUM} complete (${DURATION_FMT}). Starting next run…" + log "" + +done diff --git a/tools/push3-evolution/evolution.conf b/tools/push3-evolution/evolution.conf new file mode 100644 index 0000000..52239e9 --- /dev/null +++ b/tools/push3-evolution/evolution.conf @@ -0,0 +1,36 @@ +# evolution.conf — configuration for evolution-daemon.sh +# +# All parameters here override the defaults in evolve.sh. +# Set BASE_RPC_URL to a Base network RPC endpoint before starting the daemon. + +# ── Fitness backend ───────────────────────────────────────────────────────────── +EVAL_MODE=revm + +# Base network RPC endpoint (required for EVAL_MODE=revm). +# Export in your shell or set here (do NOT commit credentials). +# BASE_RPC_URL=https://mainnet.base.org + +# ── Population & selection ────────────────────────────────────────────────────── +POPULATION=20 +GENERATIONS=30 +MUTATION_RATE=1 +ELITES=2 +DIVERSE_SEEDS=true + +# ── Evolution-specific overrides (applied via evolution.patch) ────────────────── +# GAS_LIMIT: production cap is 200 000; evolution uses 500 000 to allow larger +# programs that are still worth exploring before gas optimisation. +GAS_LIMIT=500000 + +# ANCHOR_WIDTH_UNBOUNDED: removes the MAX_ANCHOR_WIDTH=100 production cap so +# programs can explore the full uint24 tick-width space. +ANCHOR_WIDTH_UNBOUNDED=true + +# ── Notification (openclaw) ───────────────────────────────────────────────────── +# SSH target for `openclaw system event` notifications. +# Leave empty to disable notifications. +OPENCLAW_SSH_TARGET= + +# ── Seed ──────────────────────────────────────────────────────────────────────── +# Path to the primary seed file, relative to the repo root. +SEED=tools/push3-evolution/seeds/optimizer_v3.push3 diff --git a/tools/push3-evolution/evolution.patch b/tools/push3-evolution/evolution.patch new file mode 100644 index 0000000..a2f9c69 --- /dev/null +++ b/tools/push3-evolution/evolution.patch @@ -0,0 +1,39 @@ +diff --git a/onchain/src/LiquidityManager.sol b/onchain/src/LiquidityManager.sol +index 0daccf9..e3a9b2f 100644 +--- a/onchain/src/LiquidityManager.sol ++++ b/onchain/src/LiquidityManager.sol +@@ -36,7 +36,7 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle { + + /// @notice Maximum anchor width (in ticks) accepted from the optimizer. + /// Any optimizer-returned value above this ceiling is silently clamped down. +- uint24 internal constant MAX_ANCHOR_WIDTH = 100; ++ uint24 internal constant MAX_ANCHOR_WIDTH = type(uint24).max; + + /// @notice Upper bound (inclusive) for scale-1 optimizer parameters: capitalInefficiency, + /// anchorShare, and discoveryDepth. Values above this ceiling are silently clamped. +diff --git a/onchain/src/Optimizer.sol b/onchain/src/Optimizer.sol +index 4efa74c..a29612f 100644 +--- a/onchain/src/Optimizer.sol ++++ b/onchain/src/Optimizer.sol +@@ -136,7 +136,7 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer { + /// staticcall to actually receive 200 000. Callers with exactly 200–203 k + /// gas will see a spurious bear-defaults fallback. This is not a practical + /// concern from recenter(), which always has abundant gas. +- uint256 internal constant CALCULATE_PARAMS_GAS_LIMIT = 200_000; ++ uint256 internal constant CALCULATE_PARAMS_GAS_LIMIT = 500_000; + + /** + * @notice Initialize the Optimizer. +diff --git a/onchain/test/FitnessEvaluator.t.sol b/onchain/test/FitnessEvaluator.t.sol +index 9434163..5b91eca 100644 +--- a/onchain/test/FitnessEvaluator.t.sol ++++ b/onchain/test/FitnessEvaluator.t.sol +@@ -152,7 +152,7 @@ contract FitnessEvaluator is Test { + /// @dev Must match Optimizer.CALCULATE_PARAMS_GAS_LIMIT. Candidates that exceed + /// this limit would unconditionally produce bear defaults in production and + /// are disqualified (fitness = 0) rather than scored against their theoretical output. +- uint256 internal constant CALCULATE_PARAMS_GAS_LIMIT = 200_000; ++ uint256 internal constant CALCULATE_PARAMS_GAS_LIMIT = 500_000; + + /// @dev Soft gas penalty: wei deducted from fitness per gas unit used by calculateParams. + /// Creates selection pressure toward leaner programs while keeping gas as a