From 5ca4981ea84da1980e6f42a7cab6e9299d938cef Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 26 Jan 2026 03:08:13 +0100 Subject: [PATCH] fix: add 2-minute grace period for respawn after server restart Prevents false idle detection when server is rebuilt/restarted. Previously, restored respawn controllers would start immediately and trigger after 30s of "no output" because the session wasn't fully connected yet. Now waits 2 minutes from server startup before activating restored respawn controllers, giving sessions time to reconnect and stabilize. Co-Authored-By: Claude Opus 4.5 --- src/web/server.ts | 49 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/src/web/server.ts b/src/web/server.ts index c26d4d99..fd51e0b2 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -273,6 +273,12 @@ export class WebServer extends EventEmitter { // Token recording for daily stats (track what's been recorded to avoid double-counting) private lastRecordedTokens: Map = new Map(); private tokenRecordingTimer: NodeJS.Timeout | null = null; + // Server startup time for respawn grace period calculation + private readonly serverStartTime: number = Date.now(); + // Pending respawn start timers (for cleanup on shutdown) + private pendingRespawnStarts: Map = new Map(); + // Grace period before starting restored respawn controllers (2 minutes) + private static readonly RESPAWN_RESTORE_GRACE_PERIOD_MS = 2 * 60 * 1000; constructor(port: number = 3000, https: boolean = false) { super(); @@ -2068,6 +2074,13 @@ export class WebServer extends EventEmitter { this.respawnTimers.delete(sessionId); } + // Clear pending respawn start timer (from restoration grace period) + const pendingStart = this.pendingRespawnStarts.get(sessionId); + if (pendingStart) { + clearTimeout(pendingStart); + this.pendingRespawnStarts.delete(sessionId); + } + // Stop transcript watcher this.stopTranscriptWatcher(sessionId); @@ -2497,9 +2510,10 @@ export class WebServer extends EventEmitter { this.broadcast('respawn:timerStarted', { sessionId, durationMinutes, endAt, startedAt: now }); } + /** * Restore a RespawnController from persisted configuration. - * Creates the controller, sets up listeners, starts it, and optionally sets up a timed respawn. + * Creates the controller, sets up listeners, but does NOT start it. * * @param session - The session to attach the controller to * @param config - The persisted respawn configuration @@ -2536,13 +2550,34 @@ export class WebServer extends EventEmitter { this.respawnControllers.set(session.id, controller); this.setupRespawnListeners(session.id, controller); - controller.start(); + + // Calculate delay: wait until 2 minutes after server start before starting respawn + // This prevents false idle detection immediately after a server restart/rebuild + const timeSinceStart = Date.now() - this.serverStartTime; + const delayMs = Math.max(0, WebServer.RESPAWN_RESTORE_GRACE_PERIOD_MS - timeSinceStart); + + if (delayMs > 0) { + console.log(`[Server] Restored respawn controller for session ${session.id} from ${source} (will start in ${Math.ceil(delayMs / 1000)}s)`); + const timer = setTimeout(() => { + this.pendingRespawnStarts.delete(session.id); + // Double-check controller still exists and is stopped + const ctrl = this.respawnControllers.get(session.id); + if (ctrl && ctrl.state === 'stopped') { + ctrl.start(); + this.broadcast('respawn:started', { sessionId: session.id }); + console.log(`[Server] Restored respawn controller started for session ${session.id}`); + } + }, delayMs); + this.pendingRespawnStarts.set(session.id, timer); + } else { + // Grace period has passed, start immediately + controller.start(); + console.log(`[Server] Restored respawn controller for session ${session.id} from ${source} (started immediately)`); + } if (config.durationMinutes && config.durationMinutes > 0) { this.setupTimedRespawn(session.id, config.durationMinutes); } - - console.log(`[Server] Restored respawn controller for session ${session.id} from ${source}`); } // Helper to get custom CLAUDE.md template path from settings @@ -3200,6 +3235,12 @@ export class WebServer extends EventEmitter { // Stop screen stats collection this.screenManager.stopStatsCollection(); + // Clear all pending respawn start timers (from restoration grace period) + for (const timer of this.pendingRespawnStarts.values()) { + clearTimeout(timer); + } + this.pendingRespawnStarts.clear(); + // Stop all respawn controllers and remove listeners for (const controller of this.respawnControllers.values()) { controller.stop();