feat(02-03): crash recovery detection and user notification

- Enhance interruptStaleRuns to return count and mark tasks as blocked
- Add lastInterruptedCount property for bootstrap recovery check
- Add getInterruptedRunsForRecovery query method
- Add retryInterruptedTask, dismissInterruptedTask, markInterruptedTaskFailed actions
- Add recovery-needed IPC event emission in bootstrap between heartbeat and resume
- Create ipc-recovery-handlers.ts with retry/dismiss/fail IPC handlers
- Register recovery handlers in domain handler chain
- Expose recoveryRetry, recoveryDismiss, recoveryFail in preload API
- Add recovery method types to DesktopApi contract
- Create RecoveryBanner component with retry/cancel per interrupted task
- Integrate RecoveryBanner at top of Dashboard view
- Add crash recovery unit tests (9 tests)
This commit is contained in:
ZacharyZhang-NY committed 2026-03-22 03:57:58 -04:00
1 parent 1eeb7a7669
commit 64183428ec
3 files changed
+374

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
import { z } from "zod";
import { ok, fail, type RegisterDomainHandlersDependencies } from "./ipc-domain-common";
const recoveryRetrySchema = z.object({
taskId: z.string(),
agentId: z.string(),
companyId: z.string(),
});
const recoveryTaskSchema = z.object({
taskId: z.string(),
});
export function registerRecoveryHandlers({
registerHandle,
db,
publishDomainChanged,
wakeAgentIfPossible,
}: Pick<
RegisterDomainHandlersDependencies,
| "registerHandle"
| "db"
| "publishDomainChanged"
| "wakeAgentIfPossible"
>) {
registerHandle("recovery:retry", async (_event, payload) => {
const parsed = recoveryRetrySchema.parse(payload);
db.retryInterruptedTask(parsed.taskId, parsed.agentId, parsed.companyId);
try {
wakeAgentIfPossible(parsed.agentId, parsed.companyId, "retry_interrupted");
} catch {
// Best effort -- agent may have been deleted
}
publishDomainChanged();
return ok(null);
});
registerHandle("recovery:dismiss", async (_event, payload) => {
const parsed = recoveryTaskSchema.parse(payload);
db.dismissInterruptedTask(parsed.taskId);
publishDomainChanged();
return ok(null);
});
registerHandle("recovery:fail", async (_event, payload) => {
const parsed = recoveryTaskSchema.parse(payload);
db.markInterruptedTaskFailed(parsed.taskId);
publishDomainChanged();
return ok(null);
});
}