Skip to main content

Overview

The evolution cycle is the core loop that drives continuous improvement. Each cycle consists of three phases:
  1. Analysis Phase - Extract signals from runtime observations
  2. Selection Phase - Match signals to genes and capsules
  3. Execution Phase - Execute the mutation and record the outcome

Phase 1: Analysis

The analysis phase scans multiple data sources to build a comprehensive picture of the system state.

Data Sources

// From src/evolve.js:813
const recentMasterLog = readRealSessionLog();
const todayLog = readRecentLog(TODAY_LOG);
const memorySnippet = readMemorySnippet();
const userSnippet = readUserSnippet();
Active session transcripts from ~/.openclaw/agents/{AGENT_NAME}/sessions/*.jsonl.The system reads up to 6 active sessions (modified in last 24 hours) to get a full picture:
// From src/evolve.js:161
function readRealSessionLog() {
  const ACTIVE_WINDOW_MS = 24 * 60 * 60 * 1000; // 24 hours
  const TARGET_BYTES = 120000;
  const PER_SESSION_BYTES = 20000;
  
  // Find ALL active sessions (modified in last 24h), sorted newest first
  let files = fs.readdirSync(AGENT_SESSIONS_DIR)
    .filter(f => f.endsWith('.jsonl') && !f.includes('.lock'))
    .filter(f => (now - f.time) < ACTIVE_WINDOW_MS)
    .sort((a, b) => b.time - a.time);
}
  • MEMORY.md: Persistent agent memory (up to 50KB)
  • USER.md: User preferences and context
Both files support session scoping to isolate evolution state across channels/projects.
Last 80 evolution events from assets/gep/events.jsonl:
// From src/evolve.js:953
const recentEvents = (() => {
  const all = readAllEvents();
  return Array.isArray(all) ? all.filter(e => e.type === 'EvolutionEvent').slice(-80) : [];
})();

Signal Extraction

The extractSignals() function analyzes the corpus to identify actionable patterns:
// From src/evolve.js:961
const signals = extractSignals({
  recentSessionTranscript: recentMasterLog,
  todayLog,
  memorySnippet,
  userSnippet,
  recentEvents,
});
See Signals for detailed extraction logic.

Phase 2: Selection

The selection phase matches signals to the best Gene (strategy template) and Capsule (validated solution).

Gene Selection

Genes are scored by how well their signals_match patterns align with current signals:
// From src/gep/selector.js:30
function scoreGene(gene, signals) {
  const patterns = Array.isArray(gene.signals_match) ? gene.signals_match : [];
  let score = 0;
  for (const pat of patterns) {
    if (matchPatternToSignals(pat, signals)) score += 1;
  }
  return score;
}
Memory Graph Preference: If the memory graph has identified a high-confidence path, the selector will prefer that gene over raw signal matching.

Capsule Selection

Capsules are pre-validated solutions that match trigger patterns:
// From src/gep/selector.js:138
function selectCapsule(capsules, signals) {
  const scored = (capsules || [])
    .map(c => {
      const triggers = Array.isArray(c.trigger) ? c.trigger : [];
      const score = triggers.reduce((acc, t) => 
        matchPatternToSignals(t, signals) ? acc + 1 : acc, 0);
      return { capsule: c, score };
    })
    .filter(x => x.score > 0)
    .sort((a, b) => b.score - a.score);
  return scored.length ? scored[0].capsule : null;
}

Failed Capsule Ban

To prevent repeating known failures, genes from recently failed capsules with similar signals are temporarily banned:
// From src/gep/selector.js:164
function banGenesFromFailedCapsules(failedCapsules, signals, existingBans) {
  var bans = existingBans instanceof Set ? new Set(existingBans) : new Set();
  var geneFailCounts = {};
  
  for (var i = 0; i < failedCapsules.length; i++) {
    var fc = failedCapsules[i];
    var overlap = computeSignalOverlap(signals, fc.trigger || []);
    if (overlap < FAILED_CAPSULE_OVERLAP_MIN) continue;
    
    var gid = String(fc.gene);
    geneFailCounts[gid] = (geneFailCounts[gid] || 0) + 1;
  }
  
  // Ban genes that failed 2+ times with similar signals
  for (var j = 0; j < keys.length; j++) {
    if (geneFailCounts[keys[j]] >= FAILED_CAPSULE_BAN_THRESHOLD) {
      bans.add(keys[j]);
    }
  }
  return bans;
}

Drift Intensity

In evolutionary biology, genetic drift is stronger in small populations. Evolver models this:
// From src/gep/selector.js:46
function computeDriftIntensity(opts) {
  var ne = effectivePopulationSize || genePoolSize || null;
  
  if (driftEnabled) {
    // Explicit drift: use moderate-to-high intensity
    return ne && ne > 1 ? Math.min(1, 1 / Math.sqrt(ne) + 0.3) : 0.7;
  }
  
  if (ne != null && ne > 0) {
    // Population-dependent drift: small population = more drift
    // Ne=1: intensity=1.0, Ne=25: intensity=0.2, Ne=100: intensity=0.1
    return Math.min(1, 1 / Math.sqrt(ne));
  }
  
  return 0; // No drift info, pure selection
}
Drift allows the evolver to escape local optima by occasionally selecting sub-optimal genes.

Phase 3: Execution

The execution phase constructs a Mutation, selects a Personality, and executes the evolution.

Mutation Construction

// From src/evolve.js:1315 (truncated output)
const mutation = buildMutation({
  signals,
  selectedGene,
  driftEnabled: IS_RANDOM_DRIFT,
  personalityState,
  target: `gene:${selectedGene.id}`,
  expected_effect: 'reduce runtime errors and increase stability',
});
See Mutations for details on categories and risk levels.

Personality Selection

const personalityResult = selectPersonalityForRun({
  driftEnabled: IS_RANDOM_DRIFT,
  signals,
  recentEvents,
});
See Personality for natural selection mechanics.

Memory Graph Recording

Every cycle records three nodes to the causal memory graph:
// From src/evolve.js:1151
try {
  recordOutcomeFromState({ signals, observations });
  recordSignalSnapshot({ signals, observations });
} catch (e) {
  console.error(`[MemoryGraph] Write failed: ${e.message}`);
  throw new Error(`MemoryGraph write failed: ${e.message}`);
}
If memory graph writes fail, the cycle is aborted. Evolution without causal memory leads to thrashing.

Loop Gating and Backoff

To prevent resource exhaustion and ensure quality, the evolution loop includes multiple guard conditions.

Pending Solidify Gate

Do not start a new cycle until the previous one is solidified:
// From src/evolve.js:701
if (bridgeEnabled && loopMode) {
  const st = readStateForSolidify();
  const lastRun = st && st.last_run ? st.last_run : null;
  const lastSolid = st && st.last_solidify ? st.last_solidify : null;
  
  if (lastRun && lastRun.run_id) {
    const pending = !lastSolid || lastSolid.run_id !== lastRun.run_id;
    if (pending) {
      writeDormantHypothesis({
        backoff_reason: 'loop_gating_pending_solidify',
        signals: lastRun.signals,
        mutation: lastRun.mutation,
        run_id: lastRun.run_id,
      });
      await sleepMs(waitMs);
      return;
    }
  }
}

Active Sessions Backoff

If the agent has too many active user sessions, back off to avoid starving user conversations:
// From src/evolve.js:669
const QUEUE_MAX = Number.parseInt(process.env.EVOLVE_AGENT_QUEUE_MAX || '10', 10);
const activeUserSessions = getRecentActiveSessionCount(10 * 60 * 1000);

if (activeUserSessions > QUEUE_MAX) {
  writeDormantHypothesis({
    backoff_reason: 'active_sessions_exceeded',
    active_sessions: activeUserSessions,
    queue_max: QUEUE_MAX,
  });
  await sleepMs(QUEUE_BACKOFF_MS);
  return;
}

System Load Backoff

When system load is too high, back off to prevent contributing to load spikes:
// From src/evolve.js:686
const LOAD_MAX = parseFloat(process.env.EVOLVE_LOAD_MAX || String(getDefaultLoadMax()));
const sysLoad = getSystemLoad();

if (sysLoad.load1m > LOAD_MAX) {
  writeDormantHypothesis({
    backoff_reason: 'system_load_exceeded',
    system_load: { load1m: sysLoad.load1m, load5m: sysLoad.load5m },
    load_max: LOAD_MAX,
    cpu_cores: os.cpus().length,
  });
  await sleepMs(QUEUE_BACKOFF_MS);
  return;
}
The default load threshold is CPU cores × 0.9, automatically adapting to system capacity.

Repair Loop Circuit Breaker

If the last N events are all failed repairs with the same gene, force innovation intent:
// From src/evolve.js:791
const REPAIR_LOOP_THRESHOLD = 3;
const recent = allEvents.slice(-REPAIR_LOOP_THRESHOLD);

const allRepairFailed = recent.every(e =>
  e.intent === 'repair' && e.outcome.status === 'failed'
);

if (allRepairFailed) {
  const geneIds = recent.map(e => e.genes_used[0] || 'unknown');
  const sameGene = geneIds.every(id => id === geneIds[0]);
  
  console.warn(`[CircuitBreaker] Detected ${REPAIR_LOOP_THRESHOLD} consecutive failed repairs. Forcing innovation.`);
  process.env.FORCE_INNOVATION = 'true';
}

Cycle Lifecycle

Each evolution cycle follows this sequence:

Configuration

Environment Variables

EVOLVE_LOOP
boolean
default:"false"
Enable continuous loop mode
EVOLVE_AGENT_QUEUE_MAX
number
default:"10"
Max active user sessions before backing off
EVOLVE_LOAD_MAX
number
default:"auto"
System load threshold (auto: CPU cores × 0.9)
EVOLVE_PENDING_SLEEP_MS
number
default:"120000"
Sleep duration when waiting for solidify (ms)
EVOLVE_MIN_INTERVAL
number
default:"120000"
Minimum interval between cycles (ms)

Next Steps

Signals

Learn how signals are extracted and de-duplicated

Mutations

Understand mutation categories and risk levels

Build docs developers (and LLMs) love