mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
feat: persist respawn config and inner loop state across server restarts
- Add PersistedRespawnConfig type and respawnConfig field to ScreenSession - Add innerLoopEnabled field to ScreenSession for Ralph Wiggum tracking - Add updateRespawnConfig, clearRespawnConfig, updateInnerLoopEnabled methods to ScreenManager - Save respawn config when enabled/updated via API endpoints - Save inner loop enabled state when changed via API - Restore respawn controllers and inner loop state on server startup This ensures respawn continues working after claudeman restarts. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+28
-1
@@ -19,7 +19,7 @@ import { spawn, execSync } from 'node:child_process';
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import { ScreenSession, ProcessStats, ScreenSessionWithStats, getErrorMessage } from './types.js';
|
||||
import { ScreenSession, ProcessStats, ScreenSessionWithStats, PersistedRespawnConfig, getErrorMessage } from './types.js';
|
||||
|
||||
/** Path to persisted screen session metadata */
|
||||
const SCREENS_FILE = join(homedir(), '.claudeman', 'screens.json');
|
||||
@@ -570,6 +570,33 @@ export class ScreenManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// Update respawn config for a screen session (persisted across restarts)
|
||||
updateRespawnConfig(sessionId: string, config: PersistedRespawnConfig | undefined): void {
|
||||
const screen = this.screens.get(sessionId);
|
||||
if (screen) {
|
||||
screen.respawnConfig = config;
|
||||
this.saveScreens();
|
||||
}
|
||||
}
|
||||
|
||||
// Clear respawn config when respawn is stopped
|
||||
clearRespawnConfig(sessionId: string): void {
|
||||
const screen = this.screens.get(sessionId);
|
||||
if (screen && screen.respawnConfig) {
|
||||
delete screen.respawnConfig;
|
||||
this.saveScreens();
|
||||
}
|
||||
}
|
||||
|
||||
// Update inner loop (Ralph Wiggum) enabled state
|
||||
updateInnerLoopEnabled(sessionId: string, enabled: boolean): void {
|
||||
const screen = this.screens.get(sessionId);
|
||||
if (screen) {
|
||||
screen.innerLoopEnabled = enabled;
|
||||
this.saveScreens();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if screen is available on the system
|
||||
static isScreenAvailable(): boolean {
|
||||
try {
|
||||
|
||||
@@ -433,6 +433,29 @@ export interface CaseInfo {
|
||||
*
|
||||
* Claudeman uses GNU screen for session persistence across server restarts.
|
||||
*/
|
||||
/**
|
||||
* Persisted respawn configuration for screen sessions.
|
||||
* Subset of RespawnConfig that gets saved to disk.
|
||||
*/
|
||||
export interface PersistedRespawnConfig {
|
||||
/** Whether respawn was enabled */
|
||||
enabled: boolean;
|
||||
/** How long to wait after seeing prompt before considering truly idle (ms) */
|
||||
idleTimeoutMs: number;
|
||||
/** The prompt to send for updating docs */
|
||||
updatePrompt: string;
|
||||
/** Delay between sending steps (ms) */
|
||||
interStepDelayMs: number;
|
||||
/** Whether to send /clear after update prompt */
|
||||
sendClear: boolean;
|
||||
/** Whether to send /init after /clear */
|
||||
sendInit: boolean;
|
||||
/** Optional prompt to send if /init doesn't trigger work */
|
||||
kickstartPrompt?: string;
|
||||
/** Duration in minutes if timed respawn was set */
|
||||
durationMinutes?: number;
|
||||
}
|
||||
|
||||
export interface ScreenSession {
|
||||
/** Claudeman session ID */
|
||||
sessionId: string;
|
||||
@@ -450,6 +473,10 @@ export interface ScreenSession {
|
||||
attached: boolean;
|
||||
/** Session display name (tab name) */
|
||||
name?: string;
|
||||
/** Persisted respawn controller configuration (restored on server restart) */
|
||||
respawnConfig?: PersistedRespawnConfig;
|
||||
/** Whether inner loop (Ralph Wiggum) tracking is enabled */
|
||||
innerLoopEnabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
type SessionResponse,
|
||||
type QuickStartResponse,
|
||||
type CaseInfo,
|
||||
type PersistedRespawnConfig,
|
||||
} from '../types.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
@@ -305,6 +306,8 @@ export class WebServer extends EventEmitter {
|
||||
} else {
|
||||
session.innerLoopTracker.disable();
|
||||
}
|
||||
// Persist inner loop enabled state
|
||||
this.screenManager.updateInnerLoopEnabled(id, enabled);
|
||||
}
|
||||
|
||||
// Configure the inner loop tracker
|
||||
@@ -516,6 +519,10 @@ export class WebServer extends EventEmitter {
|
||||
}
|
||||
|
||||
controller.start();
|
||||
|
||||
// Persist respawn config to screen session
|
||||
this.saveRespawnConfig(id, controller.getConfig());
|
||||
|
||||
this.broadcast('respawn:started', { sessionId: id, status: controller.getStatus() });
|
||||
|
||||
return { success: true, status: controller.getStatus() };
|
||||
@@ -531,6 +538,10 @@ export class WebServer extends EventEmitter {
|
||||
}
|
||||
|
||||
controller.stop();
|
||||
|
||||
// Clear persisted respawn config
|
||||
this.screenManager.clearRespawnConfig(id);
|
||||
|
||||
this.broadcast('respawn:stopped', { sessionId: id });
|
||||
|
||||
return { success: true };
|
||||
@@ -547,6 +558,10 @@ export class WebServer extends EventEmitter {
|
||||
}
|
||||
|
||||
controller.updateConfig(config);
|
||||
|
||||
// Persist updated config
|
||||
this.saveRespawnConfig(id, controller.getConfig());
|
||||
|
||||
this.broadcast('respawn:configUpdated', { sessionId: id, config: controller.getConfig() });
|
||||
|
||||
return { success: true, config: controller.getConfig() };
|
||||
@@ -627,6 +642,9 @@ export class WebServer extends EventEmitter {
|
||||
this.setupTimedRespawn(id, body.durationMinutes);
|
||||
}
|
||||
|
||||
// Persist respawn config to screen session
|
||||
this.saveRespawnConfig(id, controller.getConfig(), body?.durationMinutes);
|
||||
|
||||
this.broadcast('respawn:started', { sessionId: id, status: controller.getStatus() });
|
||||
|
||||
return {
|
||||
@@ -1083,6 +1101,21 @@ export class WebServer extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to save respawn config to screen session for persistence
|
||||
private saveRespawnConfig(sessionId: string, config: RespawnConfig, durationMinutes?: number): void {
|
||||
const persistedConfig: PersistedRespawnConfig = {
|
||||
enabled: config.enabled,
|
||||
idleTimeoutMs: config.idleTimeoutMs,
|
||||
updatePrompt: config.updatePrompt,
|
||||
interStepDelayMs: config.interStepDelayMs,
|
||||
sendClear: config.sendClear,
|
||||
sendInit: config.sendInit,
|
||||
kickstartPrompt: config.kickstartPrompt,
|
||||
durationMinutes,
|
||||
};
|
||||
this.screenManager.updateRespawnConfig(sessionId, persistedConfig);
|
||||
}
|
||||
|
||||
// Get system CPU and memory usage
|
||||
private getSystemStats(): { cpu: number; memory: { usedMB: number; totalMB: number; percent: number } } {
|
||||
try {
|
||||
@@ -1690,6 +1723,38 @@ export class WebServer extends EventEmitter {
|
||||
session.innerLoopTracker.restoreState(innerState.loop, innerState.todos);
|
||||
console.log(`[Server] Restored inner loop state for session ${session.id} (enabled: ${innerState.loop.enabled})`);
|
||||
}
|
||||
// Also check screen.innerLoopEnabled as a fallback
|
||||
if (screen.innerLoopEnabled && !session.innerLoopTracker.enabled) {
|
||||
session.innerLoopTracker.enable();
|
||||
console.log(`[Server] Enabled inner loop tracker for session ${session.id} from screen config`);
|
||||
}
|
||||
|
||||
// Restore respawn controller if it was enabled
|
||||
if (screen.respawnConfig?.enabled) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark it as restored (not started yet - user needs to attach)
|
||||
console.log(`[Server] Restored session ${session.id} from screen ${screen.screenName}`);
|
||||
|
||||
Reference in New Issue
Block a user