Skip to content

Commit 55c4316

Browse files
committed
Merge pull request #782 from web3dev1337/fix/recovery-resume
fix: restore recovery auto-resume
2 parents 2693a27 + 2e60a03 commit 55c4316

3 files changed

Lines changed: 85 additions & 27 deletions

File tree

client/app.js

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13759,15 +13759,24 @@ class ClaudeOrchestrator {
1375913759
console.log('Applying session recovery:', recovery);
1376013760
const recoverySettings = this.userSettings?.global?.sessionRecovery || {};
1376113761
const resumeConversation = recoverySettings.resumeConversation !== false;
13762+
const resumeCwd = recoverySettings.resumeCwd !== false;
13763+
const normalizeMode = (mode, fallback) => {
13764+
const normalized = String(mode || '').trim().toLowerCase();
13765+
if (normalized === 'fresh' || normalized === 'continue' || normalized === 'resume') {
13766+
return normalized;
13767+
}
13768+
return fallback;
13769+
};
1376213770

1376313771
// Track which sessions we're recovering so auto-start skips them
1376413772
this.recoveredSessions = new Set();
1376513773

1376613774
for (const session of recovery.sessions) {
13767-
const { sessionId, lastCwd, lastAgent, lastConversationId } = session;
13775+
const { sessionId, lastCwd, lastAgent, lastConversationId, lastMode } = session;
1376813776

1376913777
// Find the terminal for this session
13770-
if (!this.sessions.has(sessionId)) {
13778+
const sessionState = this.sessions.get(sessionId);
13779+
if (!sessionState) {
1377113780
console.log(`Session ${sessionId} not found, skipping recovery`);
1377213781
continue;
1377313782
}
@@ -13777,24 +13786,53 @@ class ClaudeOrchestrator {
1377713786

1377813787
console.log(`Recovering session ${sessionId}:`, { lastCwd, lastAgent, lastConversationId });
1377913788

13780-
// Start agent with resume if conversation available and it's a claude terminal
13781-
if (resumeConversation && lastConversationId && lastAgent === 'claude' && sessionId.includes('-claude')) {
13782-
console.log(`Resuming conversation: ${lastConversationId} in ${lastCwd}`);
13789+
const sessionType = String(sessionState?.type || '').trim().toLowerCase();
13790+
const recoveryCwd = resumeCwd ? lastCwd : null;
1378313791

13784-
// Use recovery-specific skipPermissions setting (defaults to true)
13792+
if (lastAgent === 'claude' && (sessionType === 'claude' || sessionId.includes('-claude'))) {
1378513793
const skipPermissions = recoverySettings.skipPermissions !== false;
13794+
if (resumeConversation && lastConversationId) {
13795+
console.log(`Resuming conversation: ${lastConversationId} in ${lastCwd}`);
13796+
this.socket.emit('start-claude', {
13797+
sessionId,
13798+
options: {
13799+
mode: 'resume',
13800+
resumeId: lastConversationId,
13801+
skipPermissions: skipPermissions,
13802+
cwd: recoveryCwd
13803+
}
13804+
});
13805+
} else {
13806+
let mode = normalizeMode(lastMode, 'continue');
13807+
if (mode === 'resume') mode = 'continue';
13808+
console.log(`Recovering Claude session ${sessionId} with mode: ${mode}`);
13809+
this.socket.emit('start-claude', {
13810+
sessionId,
13811+
options: {
13812+
mode,
13813+
skipPermissions: skipPermissions,
13814+
cwd: recoveryCwd
13815+
}
13816+
});
13817+
}
1378613818

13787-
this.socket.emit('start-claude', {
13788-
sessionId,
13789-
options: {
13790-
mode: 'resume',
13791-
resumeId: lastConversationId,
13792-
skipPermissions: skipPermissions,
13793-
cwd: lastCwd
13794-
}
13795-
});
13819+
await new Promise(resolve => setTimeout(resolve, 200));
13820+
continue;
13821+
}
1379613822

13797-
// Small delay between sessions
13823+
if (lastAgent === 'codex') {
13824+
let mode = normalizeMode(lastMode, 'continue');
13825+
if (mode === 'resume') mode = 'continue';
13826+
console.log(`Recovering Codex session ${sessionId} with mode: ${mode}`);
13827+
const config = {
13828+
agentId: 'codex',
13829+
mode,
13830+
flags: ['yolo']
13831+
};
13832+
if (recoveryCwd) {
13833+
config.cwd = recoveryCwd;
13834+
}
13835+
await this.startAgentWithConfig(sessionId, config);
1379813836
await new Promise(resolve => setTimeout(resolve, 200));
1379913837
}
1380013838
}

server/sessionManager.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -873,7 +873,7 @@ class SessionManager extends EventEmitter {
873873
// make the UI think an AI is still attached/running.
874874
if (workspaceId) {
875875
try {
876-
sessionRecoveryService.clearAgent(workspaceId, sessionId);
876+
sessionRecoveryService.markAgentInactive(workspaceId, sessionId);
877877
} catch {
878878
// best-effort
879879
}
@@ -1988,7 +1988,10 @@ class SessionManager extends EventEmitter {
19881988
const recovery = workspaceId
19891989
? sessionRecoveryService.getSession(workspaceId, sessionId)
19901990
: null;
1991-
const agent = recovery?.lastAgent || (type === 'codex' ? 'codex' : null);
1991+
const agentActive = recovery?.lastAgentActive !== false;
1992+
const agent = agentActive
1993+
? (recovery?.lastAgent || (type === 'codex' ? 'codex' : null))
1994+
: null;
19921995

19931996
const newStatus = this.statusDetector.detectStatus(sessionId, session.buffer || '', { agent });
19941997
if (newStatus === 'idle' && workspaceId && recovery?.lastAgent) {
@@ -1998,7 +2001,7 @@ class SessionManager extends EventEmitter {
19982001
const recentAll = this.statusDetector.getLastNonEmptyLines(recentLines, 6).join('\n');
19992002
if (this.statusDetector.hasExplicitShellIndicator(recentAll, lastNonEmptyLine)) {
20002003
try {
2001-
sessionRecoveryService.clearAgent(workspaceId, sessionId);
2004+
sessionRecoveryService.markAgentInactive(workspaceId, sessionId);
20022005
} catch {
20032006
// best-effort cleanup; status update still proceeds
20042007
}
@@ -2153,7 +2156,7 @@ class SessionManager extends EventEmitter {
21532156
worktreeId: session.worktreeId,
21542157
repositoryName: session.repositoryName, // For mixed-repo workspaces
21552158
repositoryType: session.repositoryType, // For dynamic launch options
2156-
agent: recovery?.lastAgent || null,
2159+
agent: recovery?.lastAgentActive === false ? null : (recovery?.lastAgent || null),
21572160
agentMode: recovery?.lastMode || null,
21582161
lastActivity: session.lastActivity
21592162
};
@@ -2670,7 +2673,7 @@ class SessionManager extends EventEmitter {
26702673
const workspaceId = session.workspace || null;
26712674
if (workspaceId) {
26722675
try {
2673-
sessionRecoveryService.clearAgent(workspaceId, sessionId);
2676+
sessionRecoveryService.markAgentInactive(workspaceId, sessionId);
26742677
} catch {
26752678
// best-effort
26762679
}

server/sessionRecoveryService.js

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,8 @@ class SessionRecoveryService {
198198
*/
199199
updateAgent(workspaceId, sessionId, agent, modeOrMeta) {
200200
const updates = {
201-
lastAgent: agent // 'claude', 'codex', 'opencode', etc.
201+
lastAgent: agent, // 'claude', 'codex', 'opencode', etc.
202+
lastAgentActive: true
202203
};
203204

204205
if (modeOrMeta && typeof modeOrMeta === 'object') {
@@ -216,6 +217,7 @@ class SessionRecoveryService {
216217
clearAgent(workspaceId, sessionId) {
217218
return this.updateSession(workspaceId, sessionId, {
218219
lastAgent: null,
220+
lastAgentActive: false,
219221
lastMode: null,
220222
lastAgentCommand: null,
221223
lastAgentCwd: null,
@@ -224,6 +226,20 @@ class SessionRecoveryService {
224226
});
225227
}
226228

229+
/**
230+
* Mark agent as inactive without losing the last agent metadata.
231+
* Keeps recovery info intact while allowing the UI to show "no agent".
232+
*/
233+
markAgentInactive(workspaceId, sessionId, meta = null) {
234+
const updates = {
235+
lastAgentActive: false
236+
};
237+
if (meta && typeof meta === 'object') {
238+
Object.assign(updates, meta);
239+
}
240+
return this.updateSession(workspaceId, sessionId, updates);
241+
}
242+
227243
/**
228244
* Mark session as running a server
229245
*/
@@ -333,21 +349,22 @@ class SessionRecoveryService {
333349
}
334350
}
335351

336-
// Only include Claude sessions with valid conversations
337-
// Always include server sessions
352+
const safeConversationId = (s.lastAgent === 'claude' && conversationValid)
353+
? s.lastConversationId
354+
: null;
338355
if (s.lastAgent === 'claude' && !conversationValid) {
339-
logger.debug('Skipping session with invalid/empty conversation', {
356+
logger.debug('Recovery session missing valid conversation, falling back to continue', {
340357
sessionId: s.sessionId,
341358
conversationId: s.lastConversationId
342359
});
343-
continue;
344360
}
345361

346362
recoveryData.push({
347363
sessionId: s.sessionId,
348364
lastCwd: conversationCwd || s.worktreePath,
349365
lastAgent: s.lastAgent,
350-
lastConversationId: s.lastConversationId,
366+
lastMode: s.lastMode,
367+
lastConversationId: safeConversationId,
351368
worktreePath: s.worktreePath,
352369
lastServerCommand: s.lastServerCommand,
353370
updatedAt: s.updatedAt

0 commit comments

Comments
 (0)