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
This commit is contained in:
Roo Code 2025-07-26 23:34:44 +00:00
parent 91c21a1bba
commit 6fd883d81d
5 changed files with 403 additions and 23 deletions

View file

@ -707,6 +707,10 @@ export const webviewMessageHandler = async (
vscode.env.openExternal(vscode.Uri.parse(message.url))
}
break
case "reloadWindow":
// Reload the VS Code window to recover from crash
vscode.commands.executeCommand("workbench.action.reloadWindow")
break
case "checkpointDiff":
const result = checkoutDiffPayloadSchema.safeParse(message.payload)

View file

@ -59,6 +59,12 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(outputChannel)
outputChannel.appendLine(`${Package.name} extension activated - ${JSON.stringify(Package)}`)
// Set up global error handlers for crash recovery
setupGlobalErrorHandlers(context, outputChannel)
// Check for crash recovery
await checkForCrashRecovery(context, outputChannel)
// Migrate old settings to new
await migrateSettings(context, outputChannel)
@ -218,3 +224,267 @@ export async function deactivate() {
TelemetryService.instance.shutdown()
TerminalRegistry.cleanup()
}
// Global error handlers for crash recovery
function setupGlobalErrorHandlers(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel) {
// Handle uncaught exceptions
process.on("uncaughtException", async (error: Error) => {
const errorMessage = `[CRASH] Uncaught Exception: ${error.message}\nStack: ${error.stack}`
outputChannel.appendLine(errorMessage)
console.error(errorMessage)
// Save crash information
await saveCrashInfo(context, error, "uncaughtException")
// Attempt to save current task state
await saveTaskStateOnCrash(context)
// Log telemetry
// Log crash telemetry
try {
if (TelemetryService.hasInstance()) {
TelemetryService.instance.captureEvent("extension_crash" as any, {
type: "uncaughtException",
error: error.message,
stack: error.stack,
platform: process.platform,
})
}
} catch (e) {
console.error("Failed to log telemetry:", e)
}
// Show user-friendly error message
vscode.window
.showErrorMessage(
"Roo Code encountered an unexpected error. Your work has been saved. Please restart VS Code.",
"Restart VS Code",
)
.then((selection) => {
if (selection === "Restart VS Code") {
vscode.commands.executeCommand("workbench.action.reloadWindow")
}
})
})
// Handle unhandled promise rejections
process.on("unhandledRejection", async (reason: any, promise: Promise<any>) => {
const errorMessage = `[CRASH] Unhandled Promise Rejection: ${reason}\nPromise: ${promise}`
outputChannel.appendLine(errorMessage)
console.error(errorMessage)
// Save crash information
await saveCrashInfo(context, reason, "unhandledRejection")
// Attempt to save current task state
await saveTaskStateOnCrash(context)
// Log telemetry
// Log crash telemetry
try {
if (TelemetryService.hasInstance()) {
TelemetryService.instance.captureEvent("extension_crash" as any, {
type: "unhandledRejection",
reason: String(reason),
platform: process.platform,
})
}
} catch (e) {
console.error("Failed to log telemetry:", e)
}
})
// Windows-specific error handling
if (process.platform === "win32") {
// Handle Windows-specific errors
process.on("SIGTERM", async () => {
outputChannel.appendLine("[CRASH] Received SIGTERM signal (Windows termination)")
await saveCrashInfo(context, new Error("SIGTERM received"), "SIGTERM")
await saveTaskStateOnCrash(context)
})
process.on("SIGINT", async () => {
outputChannel.appendLine("[CRASH] Received SIGINT signal (Windows interruption)")
await saveCrashInfo(context, new Error("SIGINT received"), "SIGINT")
await saveTaskStateOnCrash(context)
})
// Handle Windows-specific exit events
process.on("exit", async (code) => {
if (code !== 0) {
outputChannel.appendLine(`[CRASH] Process exiting with code ${code}`)
await saveCrashInfo(context, new Error(`Process exit with code ${code}`), "exit")
await saveTaskStateOnCrash(context)
}
})
// Handle Windows-specific errors that might cause crashes
process.on("uncaughtExceptionMonitor", (error: Error, origin: string) => {
// This event is emitted before uncaughtException, useful for logging
outputChannel.appendLine(`[CRASH] Uncaught exception monitor: ${error.message} from ${origin}`)
// Check for Windows-specific error patterns
if (error.message.includes("EPERM") || error.message.includes("EACCES")) {
outputChannel.appendLine("[CRASH] Windows permission error detected")
} else if (error.message.includes("ENOENT")) {
outputChannel.appendLine("[CRASH] Windows file not found error detected")
} else if (error.message.includes("spawn") || error.message.includes("ENOBUFS")) {
outputChannel.appendLine("[CRASH] Windows process spawn error detected")
}
})
}
}
// Save crash information for recovery
async function saveCrashInfo(context: vscode.ExtensionContext, error: any, type: string) {
try {
const crashInfo = {
timestamp: new Date().toISOString(),
type,
error:
error instanceof Error
? {
message: error.message,
stack: error.stack,
name: error.name,
}
: String(error),
platform: process.platform,
vscodeVersion: vscode.version,
extensionVersion: context.extension?.packageJSON?.version,
}
await context.globalState.update("lastCrashInfo", crashInfo)
await context.globalState.update("hasCrashRecovery", true)
} catch (e) {
console.error("Failed to save crash info:", e)
}
}
// Save current task state on crash
async function saveTaskStateOnCrash(context: vscode.ExtensionContext) {
try {
const provider = ClineProvider.getVisibleInstance()
if (provider) {
const currentTask = provider.getCurrentCline()
if (currentTask) {
// Save current task state
// Force save current state
await provider.postStateToWebview()
// Save task recovery info
const recoveryInfo = {
taskId: currentTask.taskId,
parentTaskId: currentTask.parentTask?.taskId,
taskStack: provider.getCurrentTaskStack(),
timestamp: new Date().toISOString(),
}
await context.globalState.update("taskRecoveryInfo", recoveryInfo)
outputChannel.appendLine(`[CRASH] Saved task recovery info for task ${currentTask.taskId}`)
}
}
} catch (e) {
console.error("Failed to save task state on crash:", e)
}
}
// Check for crash recovery on startup
async function checkForCrashRecovery(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel) {
try {
const hasCrashRecovery = context.globalState.get<boolean>("hasCrashRecovery")
const lastCrashInfo = context.globalState.get<any>("lastCrashInfo")
const taskRecoveryInfo = context.globalState.get<any>("taskRecoveryInfo")
if (hasCrashRecovery && lastCrashInfo) {
outputChannel.appendLine(
`[RECOVERY] Detected previous crash: ${lastCrashInfo.type} at ${lastCrashInfo.timestamp}`,
)
// Clear the crash flag
await context.globalState.update("hasCrashRecovery", false)
// Show recovery notification with Windows-specific messaging if applicable
const isWindows = process.platform === "win32"
const crashMessage =
isWindows && (lastCrashInfo.type === "SIGTERM" || lastCrashInfo.type === "SIGINT")
? "Roo Code was terminated unexpectedly on Windows. Would you like to restore your last session?"
: "Roo Code recovered from a previous crash. Would you like to restore your last session?"
const selection = await vscode.window.showInformationMessage(crashMessage, "Restore Session", "Start Fresh")
if (selection === "Restore Session" && taskRecoveryInfo) {
outputChannel.appendLine(`[RECOVERY] Attempting to restore task ${taskRecoveryInfo.taskId}`)
// Delay to ensure extension is fully initialized
setTimeout(async () => {
try {
const provider = ClineProvider.getVisibleInstance()
if (provider && taskRecoveryInfo.taskId) {
// Check if this was a subtask
if (taskRecoveryInfo.parentTaskId) {
outputChannel.appendLine(
`[RECOVERY] Detected subtask recovery. Parent task: ${taskRecoveryInfo.parentTaskId}`,
)
// First, try to restore the parent task
try {
await provider.showTaskWithId(taskRecoveryInfo.parentTaskId)
outputChannel.appendLine(
`[RECOVERY] Restored parent task ${taskRecoveryInfo.parentTaskId}`,
)
// Then show information about the subtask that was interrupted
vscode.window
.showInformationMessage(
`Restored to parent task. The subtask that was running during the crash has been saved and can be resumed.`,
"View Subtask",
)
.then(async (selection) => {
if (selection === "View Subtask") {
// Show the subtask that was interrupted
await provider.showTaskWithId(taskRecoveryInfo.taskId)
}
})
} catch (parentError) {
// If parent task can't be restored, just restore the subtask
outputChannel.appendLine(
`[RECOVERY] Failed to restore parent task, restoring subtask instead`,
)
await provider.showTaskWithId(taskRecoveryInfo.taskId)
vscode.window.showInformationMessage(
`Restored subtask from before the crash. The parent task context may need to be re-established.`,
)
}
} else {
// Regular task recovery
await provider.showTaskWithId(taskRecoveryInfo.taskId)
vscode.window.showInformationMessage(`Restored task from before the crash.`)
}
// If there was a task stack, log it for debugging
if (taskRecoveryInfo.taskStack && taskRecoveryInfo.taskStack.length > 1) {
outputChannel.appendLine(
`[RECOVERY] Task stack at crash: ${taskRecoveryInfo.taskStack.join(" -> ")}`,
)
}
}
} catch (e) {
console.error("Failed to restore task:", e)
vscode.window.showErrorMessage(
"Could not restore the previous task, but your work has been saved.",
)
}
}, 2000)
}
// Clear recovery info
await context.globalState.update("taskRecoveryInfo", undefined)
await context.globalState.update("lastCrashInfo", undefined)
}
} catch (e) {
console.error("Error checking for crash recovery:", e)
}
}

View file

@ -168,5 +168,20 @@
"preventCompletionWithOpenTodos": {
"description": "Prevent task completion when there are incomplete todos in the todo list"
}
},
"errorBoundary": {
"title": "Something went wrong",
"reportText": "Please help us improve by reporting this error on",
"githubText": "GitHub",
"copyInstructions": "Please copy and paste the following error message:",
"errorStack": "Error Stack",
"componentStack": "Component Stack",
"windowsNote": "This crash occurred on Windows. Your work has been automatically saved.",
"crashRecoveryText": "Don't worry! Your work has been saved and can be recovered.",
"restarting": "Restarting...",
"restartVSCode": "Restart VS Code",
"reportIssue": "Report Issue",
"technicalDetails": "Technical Details",
"helpText": "If the problem persists, please"
}
}

View file

@ -182,6 +182,7 @@ export interface WebviewMessage {
| "profileThresholds"
| "setHistoryPreviewCollapsed"
| "openExternal"
| "reloadWindow"
| "filterMarketplaceItems"
| "marketplaceButtonClicked"
| "installMarketplaceItem"

View file

@ -2,6 +2,7 @@ import React, { Component } from "react"
import { telemetryClient } from "@src/utils/TelemetryClient"
import { withTranslation, WithTranslation } from "react-i18next"
import { enhanceErrorWithSourceMaps } from "@src/utils/sourceMapUtils"
import { vscode } from "@src/utils/vscode"
type ErrorProps = {
children: React.ReactNode
@ -11,14 +12,15 @@ type ErrorState = {
error?: string
componentStack?: string | null
timestamp?: number
isRecovering?: boolean
}
class ErrorBoundary extends Component<ErrorProps, ErrorState> {
constructor(props: ErrorProps) {
super(props)
this.state = {}
this.state = {}
this.state = {
isRecovering: false,
}
}
static getDerivedStateFromError(error: unknown) {
@ -54,6 +56,31 @@ class ErrorBoundary extends Component<ErrorProps, ErrorState> {
})
}
handleRestart = () => {
this.setState({ isRecovering: true })
vscode.postMessage({ type: "reloadWindow" })
}
handleReportIssue = () => {
const errorInfo = encodeURIComponent(
`
**Error:** ${this.state.error || "Unknown error"}
**Timestamp:** ${new Date(this.state.timestamp || Date.now()).toISOString()}
**Version:** ${process.env.PKG_VERSION || "unknown"}
**Platform:** ${navigator.platform}
**User Agent:** ${navigator.userAgent}
**Component Stack:**
\`\`\`
${this.state.componentStack || "Not available"}
\`\`\`
`.trim(),
)
const issueUrl = `https://github.com/RooCodeInc/Roo-Code/issues/new?title=Crash%20Report&body=${errorInfo}`
window.open(issueUrl, "_blank")
}
render() {
const { t } = this.props
@ -63,33 +90,96 @@ class ErrorBoundary extends Component<ErrorProps, ErrorState> {
const errorDisplay = this.state.error
const componentStackDisplay = this.state.componentStack
const version = process.env.PKG_VERSION || "unknown"
const isWindows = navigator.platform.toLowerCase().includes("win")
return (
<div>
<h2 className="text-lg font-bold mt-0 mb-2">
{t("errorBoundary.title")} (v{version})
</h2>
<p className="mb-4">
{t("errorBoundary.reportText")}{" "}
<a href="https://github.com/RooCodeInc/Roo-Code/issues" target="_blank" rel="noreferrer">
{t("errorBoundary.githubText")}
</a>
</p>
<p className="mb-2">{t("errorBoundary.copyInstructions")}</p>
<div className="p-4">
<div className="mb-4 p-4 bg-vscode-editorWidget-background border border-vscode-editorWidget-border rounded">
<h2 className="text-lg font-bold mt-0 mb-2 text-vscode-errorForeground">
{t("errorBoundary.title")} (v{version})
</h2>
<div className="mb-4">
<h3 className="text-md font-bold mb-1">{t("errorBoundary.errorStack")}</h3>
<pre className="p-2 border rounded text-sm overflow-auto">{errorDisplay}</pre>
{isWindows && (
<div className="mb-3 p-2 bg-vscode-inputValidation-warningBackground border border-vscode-inputValidation-warningBorder rounded text-sm">
<span className="codicon codicon-warning mr-2"></span>
{t(
"errorBoundary.windowsNote",
"This crash occurred on Windows. Your work has been automatically saved.",
)}
</div>
)}
<p className="mb-4">
{t(
"errorBoundary.crashRecoveryText",
"Don't worry! Your work has been saved and can be recovered.",
)}
</p>
<div className="flex gap-2 mb-4">
<button
className="px-4 py-2 bg-vscode-button-background text-vscode-button-foreground hover:bg-vscode-button-hoverBackground rounded"
onClick={this.handleRestart}
disabled={this.state.isRecovering}>
{this.state.isRecovering ? (
<>
<span className="codicon codicon-loading codicon-modifier-spin mr-2"></span>
{t("errorBoundary.restarting", "Restarting...")}
</>
) : (
<>
<span className="codicon codicon-debug-restart mr-2"></span>
{t("errorBoundary.restartVSCode", "Restart VS Code")}
</>
)}
</button>
<button
className="px-4 py-2 bg-vscode-button-secondaryBackground text-vscode-button-secondaryForeground hover:bg-vscode-button-secondaryHoverBackground rounded"
onClick={this.handleReportIssue}>
<span className="codicon codicon-github mr-2"></span>
{t("errorBoundary.reportIssue", "Report Issue")}
</button>
</div>
</div>
{componentStackDisplay && (
<div>
<h3 className="text-md font-bold mb-1">{t("errorBoundary.componentStack")}</h3>
<pre className="p-2 border rounded text-sm overflow-auto">{componentStackDisplay}</pre>
<details className="mb-4">
<summary className="cursor-pointer font-bold mb-2">
{t("errorBoundary.technicalDetails", "Technical Details")}
</summary>
<div className="mt-2">
<p className="mb-2">{t("errorBoundary.copyInstructions")}</p>
<div className="mb-4">
<h3 className="text-md font-bold mb-1">{t("errorBoundary.errorStack")}</h3>
<pre className="p-2 border rounded text-sm overflow-auto max-h-64">{errorDisplay}</pre>
</div>
{componentStackDisplay && (
<div>
<h3 className="text-md font-bold mb-1">{t("errorBoundary.componentStack")}</h3>
<pre className="p-2 border rounded text-sm overflow-auto max-h-64">
{componentStackDisplay}
</pre>
</div>
)}
</div>
)}
</details>
<div className="text-sm text-vscode-descriptionForeground">
<p>
{t("errorBoundary.helpText", "If the problem persists, please")}{" "}
<a
href="https://github.com/RooCodeInc/Roo-Code/issues"
target="_blank"
rel="noreferrer"
className="text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground underline">
{t("errorBoundary.githubText")}
</a>
</p>
</div>
</div>
)
}