Skip to content

Commit 6fd883d

Browse files
committed
feat: add crash recovery and Windows-specific error handling
- Add global error handlers for uncaught exceptions and promise rejections - Implement crash information persistence for recovery on restart - Add Windows-specific signal handlers (SIGTERM, SIGINT, exit) - Enhance ErrorBoundary component with user-friendly crash recovery UI - Add restart functionality from ErrorBoundary - Implement task state persistence during crashes - Add subtask recovery mechanism to restore parent task context - Integrate crash telemetry logging - Add Windows-specific crash detection and messaging Fixes #6257
1 parent 91c21a1 commit 6fd883d

File tree

5 files changed

+404
-24
lines changed

5 files changed

+404
-24
lines changed

src/core/webview/webviewMessageHandler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,10 @@ export const webviewMessageHandler = async (
707707
vscode.env.openExternal(vscode.Uri.parse(message.url))
708708
}
709709
break
710+
case "reloadWindow":
711+
// Reload the VS Code window to recover from crash
712+
vscode.commands.executeCommand("workbench.action.reloadWindow")
713+
break
710714
case "checkpointDiff":
711715
const result = checkoutDiffPayloadSchema.safeParse(message.payload)
712716

src/extension.ts

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ export async function activate(context: vscode.ExtensionContext) {
5959
context.subscriptions.push(outputChannel)
6060
outputChannel.appendLine(`${Package.name} extension activated - ${JSON.stringify(Package)}`)
6161

62+
// Set up global error handlers for crash recovery
63+
setupGlobalErrorHandlers(context, outputChannel)
64+
65+
// Check for crash recovery
66+
await checkForCrashRecovery(context, outputChannel)
67+
6268
// Migrate old settings to new
6369
await migrateSettings(context, outputChannel)
6470

@@ -218,3 +224,267 @@ export async function deactivate() {
218224
TelemetryService.instance.shutdown()
219225
TerminalRegistry.cleanup()
220226
}
227+
228+
// Global error handlers for crash recovery
229+
function setupGlobalErrorHandlers(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel) {
230+
// Handle uncaught exceptions
231+
process.on("uncaughtException", async (error: Error) => {
232+
const errorMessage = `[CRASH] Uncaught Exception: ${error.message}\nStack: ${error.stack}`
233+
outputChannel.appendLine(errorMessage)
234+
console.error(errorMessage)
235+
236+
// Save crash information
237+
await saveCrashInfo(context, error, "uncaughtException")
238+
239+
// Attempt to save current task state
240+
await saveTaskStateOnCrash(context)
241+
242+
// Log telemetry
243+
// Log crash telemetry
244+
try {
245+
if (TelemetryService.hasInstance()) {
246+
TelemetryService.instance.captureEvent("extension_crash" as any, {
247+
type: "uncaughtException",
248+
error: error.message,
249+
stack: error.stack,
250+
platform: process.platform,
251+
})
252+
}
253+
} catch (e) {
254+
console.error("Failed to log telemetry:", e)
255+
}
256+
257+
// Show user-friendly error message
258+
vscode.window
259+
.showErrorMessage(
260+
"Roo Code encountered an unexpected error. Your work has been saved. Please restart VS Code.",
261+
"Restart VS Code",
262+
)
263+
.then((selection) => {
264+
if (selection === "Restart VS Code") {
265+
vscode.commands.executeCommand("workbench.action.reloadWindow")
266+
}
267+
})
268+
})
269+
270+
// Handle unhandled promise rejections
271+
process.on("unhandledRejection", async (reason: any, promise: Promise<any>) => {
272+
const errorMessage = `[CRASH] Unhandled Promise Rejection: ${reason}\nPromise: ${promise}`
273+
outputChannel.appendLine(errorMessage)
274+
console.error(errorMessage)
275+
276+
// Save crash information
277+
await saveCrashInfo(context, reason, "unhandledRejection")
278+
279+
// Attempt to save current task state
280+
await saveTaskStateOnCrash(context)
281+
282+
// Log telemetry
283+
// Log crash telemetry
284+
try {
285+
if (TelemetryService.hasInstance()) {
286+
TelemetryService.instance.captureEvent("extension_crash" as any, {
287+
type: "unhandledRejection",
288+
reason: String(reason),
289+
platform: process.platform,
290+
})
291+
}
292+
} catch (e) {
293+
console.error("Failed to log telemetry:", e)
294+
}
295+
})
296+
297+
// Windows-specific error handling
298+
if (process.platform === "win32") {
299+
// Handle Windows-specific errors
300+
process.on("SIGTERM", async () => {
301+
outputChannel.appendLine("[CRASH] Received SIGTERM signal (Windows termination)")
302+
await saveCrashInfo(context, new Error("SIGTERM received"), "SIGTERM")
303+
await saveTaskStateOnCrash(context)
304+
})
305+
306+
process.on("SIGINT", async () => {
307+
outputChannel.appendLine("[CRASH] Received SIGINT signal (Windows interruption)")
308+
await saveCrashInfo(context, new Error("SIGINT received"), "SIGINT")
309+
await saveTaskStateOnCrash(context)
310+
})
311+
312+
// Handle Windows-specific exit events
313+
process.on("exit", async (code) => {
314+
if (code !== 0) {
315+
outputChannel.appendLine(`[CRASH] Process exiting with code ${code}`)
316+
await saveCrashInfo(context, new Error(`Process exit with code ${code}`), "exit")
317+
await saveTaskStateOnCrash(context)
318+
}
319+
})
320+
321+
// Handle Windows-specific errors that might cause crashes
322+
process.on("uncaughtExceptionMonitor", (error: Error, origin: string) => {
323+
// This event is emitted before uncaughtException, useful for logging
324+
outputChannel.appendLine(`[CRASH] Uncaught exception monitor: ${error.message} from ${origin}`)
325+
326+
// Check for Windows-specific error patterns
327+
if (error.message.includes("EPERM") || error.message.includes("EACCES")) {
328+
outputChannel.appendLine("[CRASH] Windows permission error detected")
329+
} else if (error.message.includes("ENOENT")) {
330+
outputChannel.appendLine("[CRASH] Windows file not found error detected")
331+
} else if (error.message.includes("spawn") || error.message.includes("ENOBUFS")) {
332+
outputChannel.appendLine("[CRASH] Windows process spawn error detected")
333+
}
334+
})
335+
}
336+
}
337+
338+
// Save crash information for recovery
339+
async function saveCrashInfo(context: vscode.ExtensionContext, error: any, type: string) {
340+
try {
341+
const crashInfo = {
342+
timestamp: new Date().toISOString(),
343+
type,
344+
error:
345+
error instanceof Error
346+
? {
347+
message: error.message,
348+
stack: error.stack,
349+
name: error.name,
350+
}
351+
: String(error),
352+
platform: process.platform,
353+
vscodeVersion: vscode.version,
354+
extensionVersion: context.extension?.packageJSON?.version,
355+
}
356+
357+
await context.globalState.update("lastCrashInfo", crashInfo)
358+
await context.globalState.update("hasCrashRecovery", true)
359+
} catch (e) {
360+
console.error("Failed to save crash info:", e)
361+
}
362+
}
363+
364+
// Save current task state on crash
365+
async function saveTaskStateOnCrash(context: vscode.ExtensionContext) {
366+
try {
367+
const provider = ClineProvider.getVisibleInstance()
368+
if (provider) {
369+
const currentTask = provider.getCurrentCline()
370+
if (currentTask) {
371+
// Save current task state
372+
// Force save current state
373+
await provider.postStateToWebview()
374+
375+
// Save task recovery info
376+
const recoveryInfo = {
377+
taskId: currentTask.taskId,
378+
parentTaskId: currentTask.parentTask?.taskId,
379+
taskStack: provider.getCurrentTaskStack(),
380+
timestamp: new Date().toISOString(),
381+
}
382+
383+
await context.globalState.update("taskRecoveryInfo", recoveryInfo)
384+
outputChannel.appendLine(`[CRASH] Saved task recovery info for task ${currentTask.taskId}`)
385+
}
386+
}
387+
} catch (e) {
388+
console.error("Failed to save task state on crash:", e)
389+
}
390+
}
391+
392+
// Check for crash recovery on startup
393+
async function checkForCrashRecovery(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel) {
394+
try {
395+
const hasCrashRecovery = context.globalState.get<boolean>("hasCrashRecovery")
396+
const lastCrashInfo = context.globalState.get<any>("lastCrashInfo")
397+
const taskRecoveryInfo = context.globalState.get<any>("taskRecoveryInfo")
398+
399+
if (hasCrashRecovery && lastCrashInfo) {
400+
outputChannel.appendLine(
401+
`[RECOVERY] Detected previous crash: ${lastCrashInfo.type} at ${lastCrashInfo.timestamp}`,
402+
)
403+
404+
// Clear the crash flag
405+
await context.globalState.update("hasCrashRecovery", false)
406+
407+
// Show recovery notification with Windows-specific messaging if applicable
408+
const isWindows = process.platform === "win32"
409+
const crashMessage =
410+
isWindows && (lastCrashInfo.type === "SIGTERM" || lastCrashInfo.type === "SIGINT")
411+
? "Roo Code was terminated unexpectedly on Windows. Would you like to restore your last session?"
412+
: "Roo Code recovered from a previous crash. Would you like to restore your last session?"
413+
414+
const selection = await vscode.window.showInformationMessage(crashMessage, "Restore Session", "Start Fresh")
415+
416+
if (selection === "Restore Session" && taskRecoveryInfo) {
417+
outputChannel.appendLine(`[RECOVERY] Attempting to restore task ${taskRecoveryInfo.taskId}`)
418+
419+
// Delay to ensure extension is fully initialized
420+
setTimeout(async () => {
421+
try {
422+
const provider = ClineProvider.getVisibleInstance()
423+
if (provider && taskRecoveryInfo.taskId) {
424+
// Check if this was a subtask
425+
if (taskRecoveryInfo.parentTaskId) {
426+
outputChannel.appendLine(
427+
`[RECOVERY] Detected subtask recovery. Parent task: ${taskRecoveryInfo.parentTaskId}`,
428+
)
429+
430+
// First, try to restore the parent task
431+
try {
432+
await provider.showTaskWithId(taskRecoveryInfo.parentTaskId)
433+
outputChannel.appendLine(
434+
`[RECOVERY] Restored parent task ${taskRecoveryInfo.parentTaskId}`,
435+
)
436+
437+
// Then show information about the subtask that was interrupted
438+
vscode.window
439+
.showInformationMessage(
440+
`Restored to parent task. The subtask that was running during the crash has been saved and can be resumed.`,
441+
"View Subtask",
442+
)
443+
.then(async (selection) => {
444+
if (selection === "View Subtask") {
445+
// Show the subtask that was interrupted
446+
await provider.showTaskWithId(taskRecoveryInfo.taskId)
447+
}
448+
})
449+
} catch (parentError) {
450+
// If parent task can't be restored, just restore the subtask
451+
outputChannel.appendLine(
452+
`[RECOVERY] Failed to restore parent task, restoring subtask instead`,
453+
)
454+
await provider.showTaskWithId(taskRecoveryInfo.taskId)
455+
456+
vscode.window.showInformationMessage(
457+
`Restored subtask from before the crash. The parent task context may need to be re-established.`,
458+
)
459+
}
460+
} else {
461+
// Regular task recovery
462+
await provider.showTaskWithId(taskRecoveryInfo.taskId)
463+
464+
vscode.window.showInformationMessage(`Restored task from before the crash.`)
465+
}
466+
467+
// If there was a task stack, log it for debugging
468+
if (taskRecoveryInfo.taskStack && taskRecoveryInfo.taskStack.length > 1) {
469+
outputChannel.appendLine(
470+
`[RECOVERY] Task stack at crash: ${taskRecoveryInfo.taskStack.join(" -> ")}`,
471+
)
472+
}
473+
}
474+
} catch (e) {
475+
console.error("Failed to restore task:", e)
476+
vscode.window.showErrorMessage(
477+
"Could not restore the previous task, but your work has been saved.",
478+
)
479+
}
480+
}, 2000)
481+
}
482+
483+
// Clear recovery info
484+
await context.globalState.update("taskRecoveryInfo", undefined)
485+
await context.globalState.update("lastCrashInfo", undefined)
486+
}
487+
} catch (e) {
488+
console.error("Error checking for crash recovery:", e)
489+
}
490+
}

src/i18n/locales/en/common.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,5 +168,20 @@
168168
"preventCompletionWithOpenTodos": {
169169
"description": "Prevent task completion when there are incomplete todos in the todo list"
170170
}
171+
},
172+
"errorBoundary": {
173+
"title": "Something went wrong",
174+
"reportText": "Please help us improve by reporting this error on",
175+
"githubText": "GitHub",
176+
"copyInstructions": "Please copy and paste the following error message:",
177+
"errorStack": "Error Stack",
178+
"componentStack": "Component Stack",
179+
"windowsNote": "This crash occurred on Windows. Your work has been automatically saved.",
180+
"crashRecoveryText": "Don't worry! Your work has been saved and can be recovered.",
181+
"restarting": "Restarting...",
182+
"restartVSCode": "Restart VS Code",
183+
"reportIssue": "Report Issue",
184+
"technicalDetails": "Technical Details",
185+
"helpText": "If the problem persists, please"
171186
}
172187
}

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export interface WebviewMessage {
182182
| "profileThresholds"
183183
| "setHistoryPreviewCollapsed"
184184
| "openExternal"
185+
| "reloadWindow"
185186
| "filterMarketplaceItems"
186187
| "marketplaceButtonClicked"
187188
| "installMarketplaceItem"

0 commit comments

Comments
 (0)