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
+43 -39
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,59 +1982,41 @@ 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) {
this.sessions.set(session.id, session);
this.setupSessionListeners(session);
// Restore Ralph tracking state (Ralph Wiggum settings) if it was saved
const ralphState = this.store.getRalphState(screen.sessionId);
if (ralphState) {
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(); session.ralphTracker.enable();
console.log(`[Server] Enabled Ralph tracker for session ${session.id} from screen config`); if (savedState.ralphCompletionPhrase) {
session.ralphTracker.startLoop(savedState.ralphCompletionPhrase);
} }
console.log(`[Server] Restored Ralph tracker for session ${session.id} (phrase: ${savedState.ralphCompletionPhrase || 'none'})`);
// Auto-detect completion phrase from CLAUDE.md if not already set
if (!session.ralphTracker.loopState.completionPhrase) {
const claudeMdPath = join(session.workingDir, 'CLAUDE.md');
const completionPhrase = extractCompletionPhrase(claudeMdPath);
if (completionPhrase) {
session.ralphTracker.enable();
session.ralphTracker.startLoop(completionPhrase);
console.log(`[Server] Auto-detected completion phrase for session ${session.id}: ${completionPhrase}`);
} }
} // Respawn controller
if (savedState.respawnEnabled && savedState.respawnConfig) {
// Restore respawn controller if it was enabled
if (screen.respawnConfig?.enabled) {
try { try {
const controller = new RespawnController(session, { const controller = new RespawnController(session, {
idleTimeoutMs: screen.respawnConfig.idleTimeoutMs, idleTimeoutMs: savedState.respawnConfig.idleTimeoutMs,
updatePrompt: screen.respawnConfig.updatePrompt, updatePrompt: savedState.respawnConfig.updatePrompt,
interStepDelayMs: screen.respawnConfig.interStepDelayMs, interStepDelayMs: savedState.respawnConfig.interStepDelayMs,
enabled: screen.respawnConfig.enabled, enabled: true,
sendClear: screen.respawnConfig.sendClear, sendClear: savedState.respawnConfig.sendClear,
sendInit: screen.respawnConfig.sendInit, sendInit: savedState.respawnConfig.sendInit,
kickstartPrompt: screen.respawnConfig.kickstartPrompt, kickstartPrompt: savedState.respawnConfig.kickstartPrompt,
completionConfirmMs: savedState.respawnConfig.completionConfirmMs,
noOutputTimeoutMs: savedState.respawnConfig.noOutputTimeoutMs,
}); });
this.respawnControllers.set(session.id, controller); this.respawnControllers.set(session.id, controller);
this.setupRespawnListeners(session.id, controller); this.setupRespawnListeners(session.id, controller);
controller.start(); controller.start();
// Set up timed respawn if duration was configured if (savedState.respawnConfig.durationMinutes && savedState.respawnConfig.durationMinutes > 0) {
if (screen.respawnConfig.durationMinutes && screen.respawnConfig.durationMinutes > 0) { this.setupTimedRespawn(session.id, savedState.respawnConfig.durationMinutes);
this.setupTimedRespawn(session.id, screen.respawnConfig.durationMinutes);
} }
console.log(`[Server] Restored respawn controller for session ${session.id}`); console.log(`[Server] Restored respawn controller for session ${session.id}`);
@@ -2041,8 +2024,29 @@ export class WebServer extends EventEmitter {
console.error(`[Server] Failed to restore respawn for session ${session.id}:`, err); console.error(`[Server] Failed to restore respawn for session ${session.id}:`, err);
} }
} }
}
// Persist full state after all restorations are applied // Fallback: restore Ralph state from state-inner.json if not already set
if (!session.ralphTracker.enabled) {
const ralphState = this.store.getRalphState(screen.sessionId);
if (ralphState?.loop?.enabled) {
session.ralphTracker.restoreState(ralphState.loop, ralphState.todos);
console.log(`[Server] Restored Ralph state from inner store for session ${session.id}`);
}
}
// Fallback: auto-detect completion phrase from CLAUDE.md
if (session.ralphTracker.enabled && !session.ralphTracker.loopState.completionPhrase) {
const claudeMdPath = join(session.workingDir, 'CLAUDE.md');
const completionPhrase = extractCompletionPhrase(claudeMdPath);
if (completionPhrase) {
session.ralphTracker.startLoop(completionPhrase);
console.log(`[Server] Auto-detected completion phrase for session ${session.id}: ${completionPhrase}`);
}
}
this.sessions.set(session.id, session);
this.setupSessionListeners(session);
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)