fix: use state.json as single source of truth for session recovery

All session settings (auto-compact, auto-clear, ralph, respawn) are now
restored from state.json on server restart. Previously settings were
split across state.json, screens.json, and state-inner.json which was
fragile. state-inner.json is kept as a fallback for ralph state only.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-22 21:17:55 +01:00
co-authored by Claude Opus 4.5
parent 1c430e0cbd
commit 35c2f4fb61
+49 -45
View File
@@ -1971,9 +1971,10 @@ export class WebServer extends EventEmitter {
screenSession: screen // Pass the existing screen so startInteractive() can attach to it screenSession: screen // Pass the existing screen so startInteractive() can attach to it
}); });
// Restore auto-compact/auto-clear settings from state.json // Restore ALL session settings from state.json (single source of truth)
const savedState = this.store.getSession(screen.sessionId); const savedState = this.store.getSession(screen.sessionId);
if (savedState) { if (savedState) {
// Auto-compact
if (savedState.autoCompactEnabled !== undefined || savedState.autoCompactThreshold !== undefined) { if (savedState.autoCompactEnabled !== undefined || savedState.autoCompactThreshold !== undefined) {
session.setAutoCompact( session.setAutoCompact(
savedState.autoCompactEnabled ?? false, savedState.autoCompactEnabled ?? false,
@@ -1981,68 +1982,71 @@ export class WebServer extends EventEmitter {
savedState.autoCompactPrompt savedState.autoCompactPrompt
); );
} }
// Auto-clear
if (savedState.autoClearEnabled !== undefined || savedState.autoClearThreshold !== undefined) { if (savedState.autoClearEnabled !== undefined || savedState.autoClearThreshold !== undefined) {
session.setAutoClear( session.setAutoClear(
savedState.autoClearEnabled ?? false, savedState.autoClearEnabled ?? false,
savedState.autoClearThreshold savedState.autoClearThreshold
); );
} }
// Ralph / Todo tracker
if (savedState.ralphEnabled) {
session.ralphTracker.enable();
if (savedState.ralphCompletionPhrase) {
session.ralphTracker.startLoop(savedState.ralphCompletionPhrase);
}
console.log(`[Server] Restored Ralph tracker for session ${session.id} (phrase: ${savedState.ralphCompletionPhrase || 'none'})`);
}
// Respawn controller
if (savedState.respawnEnabled && savedState.respawnConfig) {
try {
const controller = new RespawnController(session, {
idleTimeoutMs: savedState.respawnConfig.idleTimeoutMs,
updatePrompt: savedState.respawnConfig.updatePrompt,
interStepDelayMs: savedState.respawnConfig.interStepDelayMs,
enabled: true,
sendClear: savedState.respawnConfig.sendClear,
sendInit: savedState.respawnConfig.sendInit,
kickstartPrompt: savedState.respawnConfig.kickstartPrompt,
completionConfirmMs: savedState.respawnConfig.completionConfirmMs,
noOutputTimeoutMs: savedState.respawnConfig.noOutputTimeoutMs,
});
this.respawnControllers.set(session.id, controller);
this.setupRespawnListeners(session.id, controller);
controller.start();
if (savedState.respawnConfig.durationMinutes && savedState.respawnConfig.durationMinutes > 0) {
this.setupTimedRespawn(session.id, savedState.respawnConfig.durationMinutes);
}
console.log(`[Server] Restored respawn controller for session ${session.id}`);
} catch (err) {
console.error(`[Server] Failed to restore respawn for session ${session.id}:`, err);
}
}
} }
this.sessions.set(session.id, session); // Fallback: restore Ralph state from state-inner.json if not already set
this.setupSessionListeners(session); if (!session.ralphTracker.enabled) {
const ralphState = this.store.getRalphState(screen.sessionId);
// Restore Ralph tracking state (Ralph Wiggum settings) if it was saved if (ralphState?.loop?.enabled) {
const ralphState = this.store.getRalphState(screen.sessionId); session.ralphTracker.restoreState(ralphState.loop, ralphState.todos);
if (ralphState) { console.log(`[Server] Restored Ralph state from inner store for session ${session.id}`);
session.ralphTracker.restoreState(ralphState.loop, ralphState.todos); }
console.log(`[Server] Restored Ralph state for session ${session.id} (enabled: ${ralphState.loop.enabled})`);
}
// Also check screen.ralphEnabled as a fallback
if (screen.ralphEnabled && !session.ralphTracker.enabled) {
session.ralphTracker.enable();
console.log(`[Server] Enabled Ralph tracker for session ${session.id} from screen config`);
} }
// Auto-detect completion phrase from CLAUDE.md if not already set // Fallback: auto-detect completion phrase from CLAUDE.md
if (!session.ralphTracker.loopState.completionPhrase) { if (session.ralphTracker.enabled && !session.ralphTracker.loopState.completionPhrase) {
const claudeMdPath = join(session.workingDir, 'CLAUDE.md'); const claudeMdPath = join(session.workingDir, 'CLAUDE.md');
const completionPhrase = extractCompletionPhrase(claudeMdPath); const completionPhrase = extractCompletionPhrase(claudeMdPath);
if (completionPhrase) { if (completionPhrase) {
session.ralphTracker.enable();
session.ralphTracker.startLoop(completionPhrase); session.ralphTracker.startLoop(completionPhrase);
console.log(`[Server] Auto-detected completion phrase for session ${session.id}: ${completionPhrase}`); console.log(`[Server] Auto-detected completion phrase for session ${session.id}: ${completionPhrase}`);
} }
} }
// Restore respawn controller if it was enabled this.sessions.set(session.id, session);
if (screen.respawnConfig?.enabled) { this.setupSessionListeners(session);
try {
const controller = new RespawnController(session, {
idleTimeoutMs: screen.respawnConfig.idleTimeoutMs,
updatePrompt: screen.respawnConfig.updatePrompt,
interStepDelayMs: screen.respawnConfig.interStepDelayMs,
enabled: screen.respawnConfig.enabled,
sendClear: screen.respawnConfig.sendClear,
sendInit: screen.respawnConfig.sendInit,
kickstartPrompt: screen.respawnConfig.kickstartPrompt,
});
this.respawnControllers.set(session.id, controller);
this.setupRespawnListeners(session.id, controller);
controller.start();
// Set up timed respawn if duration was configured
if (screen.respawnConfig.durationMinutes && screen.respawnConfig.durationMinutes > 0) {
this.setupTimedRespawn(session.id, screen.respawnConfig.durationMinutes);
}
console.log(`[Server] Restored respawn controller for session ${session.id}`);
} catch (err) {
console.error(`[Server] Failed to restore respawn for session ${session.id}:`, err);
}
}
// Persist full state after all restorations are applied
this.persistSessionState(session); this.persistSessionState(session);
// Mark it as restored (not started yet - user needs to attach) // Mark it as restored (not started yet - user needs to attach)