Overview
The evolution cycle is the core loop that drives continuous improvement. Each cycle consists of three phases:
Analysis Phase - Extract signals from runtime observations
Selection Phase - Match signals to genes and capsules
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 ) : [];
})();
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
Enable continuous loop mode
Max active user sessions before backing off
System load threshold (auto: CPU cores × 0.9)
Sleep duration when waiting for solidify (ms)
Minimum interval between cycles (ms)
Next Steps
Signals Learn how signals are extracted and de-duplicated
Mutations Understand mutation categories and risk levels