diff --git a/src/main/bootstrap-application.ts b/src/main/bootstrap-application.ts index f711cb7a..b75a2696 100644 --- a/src/main/bootstrap-application.ts +++ b/src/main/bootstrap-application.ts @@ -199,6 +199,24 @@ export async function bootstrapApplication({ } serviceGraph.getOrchestrationService()?.startHeartbeatScheduler(); + + // Check for crash recovery -- emit recovery notification before resuming queued runs + const interruptedCount = db.lastInterruptedCount; + if (interruptedCount > 0) { + log.warn(`[bootstrap] Detected ${interruptedCount} interrupted runs from previous session. Sending recovery notification.`); + const interruptedRuns = db.getInterruptedRunsForRecovery(); + if (interruptedRuns.length > 0) { + emitEvent({ + type: "recovery-needed", + interruptedRuns, + } as any); // DesktopEvent will need extending + notify( + "Interrupted runs detected", + `${interruptedRuns.length} task(s) were interrupted when the app last closed. Check the dashboard for recovery options.` + ); + } + } + serviceGraph.getOrchestrationService()?.resumeQueuedRuns(); return { diff --git a/src/main/ipc-domain-handlers.ts b/src/main/ipc-domain-handlers.ts index a49f99b7..37441e2b 100644 --- a/src/main/ipc-domain-handlers.ts +++ b/src/main/ipc-domain-handlers.ts @@ -4,6 +4,7 @@ import { registerAutomationOperationsHandlers } from "./ipc-automation-operation import { registerBrowserHandlers } from "./ipc-browser-handlers"; import { registerContentOperationsHandlers } from "./ipc-content-operations-handlers"; import { registerOnboardingHandlers } from "./ipc-onboarding-handlers"; +import { registerRecoveryHandlers } from "./ipc-recovery-handlers"; export function registerDomainHandlers(dependencies: RegisterDomainHandlersDependencies) { registerOnboardingHandlers(dependencies); @@ -11,4 +12,5 @@ export function registerDomainHandlers(dependencies: RegisterDomainHandlersDepen registerContentOperationsHandlers(dependencies); registerAutomationOperationsHandlers(dependencies); registerAgentCoordinationHandlers(dependencies); + registerRecoveryHandlers(dependencies); } diff --git a/src/preload/index.ts b/src/preload/index.ts index 5f9d7a79..043e1b6e 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -75,6 +75,12 @@ const api: DesktopApi = { deleteWorkflow: (input) => ipcRenderer.invoke(IPC_CHANNELS.deleteWorkflow, input), runWorkflow: (input) => ipcRenderer.invoke(IPC_CHANNELS.runWorkflow, input), autoAssignTask: (input) => ipcRenderer.invoke(IPC_CHANNELS.autoAssignTask, input), + recoveryRetry: (taskId: string, agentId: string, companyId: string) => + ipcRenderer.invoke("recovery:retry", { taskId, agentId, companyId }), + recoveryDismiss: (taskId: string) => + ipcRenderer.invoke("recovery:dismiss", { taskId }), + recoveryFail: (taskId: string) => + ipcRenderer.invoke("recovery:fail", { taskId }), subscribe: (callback: (event: DesktopEvent) => void) => { const listener = (_event: Electron.IpcRendererEvent, payload: DesktopEvent) => callback(payload); ipcRenderer.on(IPC_CHANNELS.rendererEvent, listener); diff --git a/src/renderer/components/dashboard/Dashboard.tsx b/src/renderer/components/dashboard/Dashboard.tsx index 5c8bdecf..76b9c106 100644 --- a/src/renderer/components/dashboard/Dashboard.tsx +++ b/src/renderer/components/dashboard/Dashboard.tsx @@ -42,6 +42,7 @@ import { TaskPipeline } from "./TaskPipeline"; import { SprintProgress } from "./SprintProgress"; import { GoalProgress } from "./GoalProgress"; import { NeedsAttention } from "./NeedsAttention"; +import { RecoveryBanner } from "./RecoveryBanner"; export interface DashboardProps { agents: AgentRecord[]; @@ -86,6 +87,9 @@ export function Dashboard(props: DashboardProps) { return (
+ {/* Recovery banner for interrupted runs */} + + {/* Health + Metrics */}
diff --git a/src/shared/contracts.ts b/src/shared/contracts.ts index 9d255ace..249bd119 100644 --- a/src/shared/contracts.ts +++ b/src/shared/contracts.ts @@ -577,5 +577,8 @@ export type DesktopApi = { deleteWorkflow: (input: z.infer) => Promise>; runWorkflow: (input: z.infer) => Promise>; autoAssignTask: (input: z.infer) => Promise>; + recoveryRetry: (taskId: string, agentId: string, companyId: string) => Promise>; + recoveryDismiss: (taskId: string) => Promise>; + recoveryFail: (taskId: string) => Promise>; subscribe: (callback: (event: DesktopEvent) => void) => () => void; };