fix: session restoration now properly attaches to existing screens

- Pass existing screenSession to Session constructor for restored sessions
- Skip terminal buffer clearing when attaching to restored (existing) screens
- Frontend auto-calls /interactive endpoint when selecting a restored session
- Fixes issue where restored sessions appeared broken after server restart

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-19 19:56:28 +01:00
co-authored by Claude Opus 4.5
parent 611815628e
commit 3e26a6a18e
3 changed files with 60 additions and 26 deletions
+44 -24
View File
@@ -129,6 +129,7 @@ export class Session extends EventEmitter {
name?: string;
screenManager?: ScreenManager;
useScreen?: boolean;
screenSession?: ScreenSession; // For restored sessions - pass the existing screen
}) {
super();
this.id = config.id || uuidv4();
@@ -139,6 +140,7 @@ export class Session extends EventEmitter {
this._lastActivityAt = this.createdAt;
this._screenManager = config.screenManager || null;
this._useScreen = config.useScreen ?? (this._screenManager !== null && ScreenManager.isScreenAvailable());
this._screenSession = config.screenSession || null; // Use existing screen if provided
// Initialize task tracker and forward events
this._taskTracker = new TaskTracker();
@@ -379,14 +381,21 @@ export class Session extends EventEmitter {
console.log('[Session] Starting interactive Claude session' + (this._useScreen ? ' (with screen)' : ''));
// If screen wrapping is enabled, create a screen session first
// If screen wrapping is enabled, create or attach to a screen session
if (this._useScreen && this._screenManager) {
try {
this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'claude', this._name);
console.log('[Session] Created screen session:', this._screenSession.screenName);
// Check if we already have a screen session (restored session)
const isRestoredSession = this._screenSession !== null;
if (isRestoredSession) {
console.log('[Session] Attaching to existing screen session:', this._screenSession.screenName);
} else {
// Create a new screen session
this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'claude', this._name);
console.log('[Session] Created screen session:', this._screenSession.screenName);
// Wait a moment for screen to fully start
await new Promise(resolve => setTimeout(resolve, 300));
// Wait a moment for screen to fully start
await new Promise(resolve => setTimeout(resolve, 300));
}
// Attach to the screen session via PTY
this.ptyProcess = pty.spawn('screen', [
@@ -399,12 +408,14 @@ export class Session extends EventEmitter {
env: { ...process.env, TERM: 'xterm-256color' },
});
// Screen creates blank space when initializing. After attaching, wait for
// the initial burst then clear the buffer and tell clients to clear their terminal.
setTimeout(() => {
this._terminalBuffer = '';
this.emit('clearTerminal');
}, 100);
// For NEW screens: clear buffer after initial burst (screen initialization noise)
// For RESTORED screens: don't clear - we want to see the existing output
if (!isRestoredSession) {
setTimeout(() => {
this._terminalBuffer = '';
this.emit('clearTerminal');
}, 100);
}
} catch (err) {
console.error('[Session] Failed to create screen session, falling back to direct PTY:', err);
this._useScreen = false;
@@ -510,14 +521,21 @@ export class Session extends EventEmitter {
const shell = process.env.SHELL || '/bin/bash';
console.log('[Session] Starting shell session with:', shell + (this._useScreen ? ' (with screen)' : ''));
// If screen wrapping is enabled, create a screen session first
// If screen wrapping is enabled, create or attach to a screen session
if (this._useScreen && this._screenManager) {
try {
this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'shell', this._name);
console.log('[Session] Created screen session:', this._screenSession.screenName);
// Check if we already have a screen session (restored session)
const isRestoredSession = this._screenSession !== null;
if (isRestoredSession) {
console.log('[Session] Attaching to existing screen session:', this._screenSession.screenName);
} else {
// Create a new screen session
this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'shell', this._name);
console.log('[Session] Created screen session:', this._screenSession.screenName);
// Wait a moment for screen to fully start
await new Promise(resolve => setTimeout(resolve, 300));
// Wait a moment for screen to fully start
await new Promise(resolve => setTimeout(resolve, 300));
}
// Attach to the screen session via PTY
this.ptyProcess = pty.spawn('screen', [
@@ -530,14 +548,16 @@ export class Session extends EventEmitter {
env: { ...process.env, TERM: 'xterm-256color' },
});
// Screen creates blank space when initializing. After attaching, wait for
// the initial burst then clear by sending 'clear' command to the shell.
setTimeout(() => {
if (this.ptyProcess) {
this._terminalBuffer = '';
this.ptyProcess.write('clear\n');
}
}, 100);
// For NEW screens: clear by sending 'clear' command to the shell
// For RESTORED screens: don't clear - we want to see the existing output
if (!isRestoredSession) {
setTimeout(() => {
if (this.ptyProcess) {
this._terminalBuffer = '';
this.ptyProcess.write('clear\n');
}
}, 100);
}
} catch (err) {
console.error('[Session] Failed to create screen session, falling back to direct PTY:', err);
this._useScreen = false;
+13
View File
@@ -633,6 +633,19 @@ class ClaudemanApp {
this.activeSessionId = sessionId;
this.renderSessionTabs();
// Check if this is a restored session that needs to be attached
const session = this.sessions.get(sessionId);
if (session && session.pid === null && session.status === 'idle') {
// This is a restored session - attach to the existing screen
try {
await fetch(`/api/sessions/${sessionId}/interactive`, { method: 'POST' });
// Update local session state
session.status = 'busy';
} catch (err) {
console.error('Failed to attach to restored session:', err);
}
}
// Load terminal buffer for this session
try {
const res = await fetch(`/api/sessions/${sessionId}/terminal`);
+3 -2
View File
@@ -1393,14 +1393,15 @@ export class WebServer extends EventEmitter {
const screens = this.screenManager.getScreens();
for (const screen of screens) {
if (!this.sessions.has(screen.sessionId)) {
// Create a session object for this screen
// Create a session object for this screen with the existing screenSession
const session = new Session({
id: screen.sessionId, // Preserve the original session ID
workingDir: screen.workingDir,
mode: screen.mode,
name: screen.name || screen.screenName,
screenManager: this.screenManager,
useScreen: true
useScreen: true,
screenSession: screen // Pass the existing screen so startInteractive() can attach to it
});
this.sessions.set(session.id, session);