Summary
When EVOLVE_BRIDGE=true and a cycle completes without producing file changes (no solidification needed), the daemon enters a permanent Ralph-loop. The last_solidify.run_id never catches up to last_run.run_id, causing isPendingSolidify() to return true indefinitely. The daemon sleeps 60s and retries forever — no subsequent cycles ever run.
Environment
- Evolver version: v1.87.2
- Mode:
--loop daemon with EVOLVE_BRIDGE=true
- Cycle timeout: default
- Strategy: balanced
Root Cause Analysis
The gating loop in index.js (line 564-568):
const st0 = readJsonSafe(solidifyStatePath);
if (isPendingSolidify(st0)) {
await sleepMs(Math.max(pendingSleepMs, minSleepMs));
continue;
}
isPendingSolidify() (line 93-101):
function isPendingSolidify(state) {
const lastRun = state && state.last_run ? state.last_run : null;
const lastSolid = state && state.last_solidify ? state.last_solidify : null;
if (!lastRun || !lastRun.run_id) return false;
if (!lastSolid || !lastSolid.run_id) return true;
return String(lastSolid.run_id) !== String(lastRun.run_id);
}
The chain of events:
- Daemon starts a Bridge cycle → calls
evolve.run()
evolve.run() spawns a sub-agent via sessions_spawn and immediately writes a new last_run to evolution_solidify_state.json
- The sub-agent is expected to call
node index.js solidify on completion, which would update last_solidify
- Scenario A: Sub-agent produces "no changes detected" → does not call solidify →
last_solidify stays at old value → Ralph-loop
- Scenario B: Daemon restarts (e.g.,
maxCyclesPerProcess threshold, or crash) before sub-agent completes → new daemon finds stale last_solidify → Ralph-loop
In the non-Bridge path (line 614-623):
if (String(process.env.EVOLVE_BRIDGE || '').toLowerCase() === 'false') {
const stAfterRun = readJsonSafe(solidifyStatePath);
if (isPendingSolidify(stAfterRun)) {
const cleared = rejectPendingRun(solidifyStatePath);
...
}
}
The auto-rejection code only runs when BRIDGE is disabled (=== 'false'). When Bridge is enabled, there's no safety net.
Steps to Reproduce
- Start daemon:
node index.js --loop with EVOLVE_BRIDGE=true
- Let it run a Bridge cycle that spawns a sub-agent
- The sub-agent produces "no changes detected" or times out
- Observe: daemon enters Ralph-loop,
last_run.run_id != last_solidify.run_id
- Observe:
cycle_progress.json shows phase: sleep forever
cycleCount in evolution_state.json stays stagnant
Workaround
# Auto-fix: sync last_solidify to match last_run
python3 -c "
import json
with open('memory/evolution/evolution_solidify_state.json') as f:
d = json.load(f)
d['last_solidify'] = {'run_id': d['last_run']['run_id'], 'status': 'auto_solidified', 'reason': 'workaround'}
with open('memory/evolution/evolution_solidify_state.json', 'w') as f:
json.dump(d, f, indent=2, ensure_ascii=False)
f.write('\n')
"
Or use a cron guard: */2 * * * * <path>/evolver-ralph-guard.sh
Proposed Fix
In the Bridge-mode path, after evolve.run() completes (whether success, timeout, or error), the daemon should auto-solidify (or auto-reject) the pending run when no sub-agent result materializes. The existing rejectPendingRun() function (line 1062-1072) already handles this correctly — it just needs to be called regardless of the EVOLVE_BRIDGE setting.
Suggested fix at line 614 — remove the === 'false' guard so the auto-reject runs in all modes:
// Always auto-reject pending solidify after evolve.run() completes.
const stAfterRun = readJsonSafe(solidifyStatePath);
if (isPendingSolidify(stAfterRun)) {
const cleared = rejectPendingRun(solidifyStatePath);
if (cleared) {
console.warn('[Loop] Auto-rejected pending run (sub-agent did not solidify).');
}
}
Summary
When
EVOLVE_BRIDGE=trueand a cycle completes without producing file changes (no solidification needed), the daemon enters a permanent Ralph-loop. Thelast_solidify.run_idnever catches up tolast_run.run_id, causingisPendingSolidify()to returntrueindefinitely. The daemon sleeps 60s and retries forever — no subsequent cycles ever run.Environment
--loopdaemon withEVOLVE_BRIDGE=trueRoot Cause Analysis
The gating loop in
index.js(line 564-568):isPendingSolidify()(line 93-101):The chain of events:
evolve.run()evolve.run()spawns a sub-agent viasessions_spawnand immediately writes a newlast_runtoevolution_solidify_state.jsonnode index.js solidifyon completion, which would updatelast_solidifylast_solidifystays at old value → Ralph-loopmaxCyclesPerProcessthreshold, or crash) before sub-agent completes → new daemon finds stalelast_solidify→ Ralph-loopIn the non-Bridge path (line 614-623):
The auto-rejection code only runs when BRIDGE is disabled (
=== 'false'). When Bridge is enabled, there's no safety net.Steps to Reproduce
node index.js --loopwithEVOLVE_BRIDGE=truelast_run.run_id != last_solidify.run_idcycle_progress.jsonshowsphase: sleepforevercycleCountinevolution_state.jsonstays stagnantWorkaround
Or use a cron guard:
*/2 * * * * <path>/evolver-ralph-guard.shProposed Fix
In the Bridge-mode path, after
evolve.run()completes (whether success, timeout, or error), the daemon should auto-solidify (or auto-reject) the pending run when no sub-agent result materializes. The existingrejectPendingRun()function (line 1062-1072) already handles this correctly — it just needs to be called regardless of theEVOLVE_BRIDGEsetting.Suggested fix at line 614 — remove the
=== 'false'guard so the auto-reject runs in all modes: