feat(desktop): auto-recover in-progress desktop tasks on client lifecycle events

- reset stale in_progress tasks owned by a client on startup, offline, and lease expiry
- monitor tasks re-queue for another pick; publish tasks move to unknown for manual reconcile
- send startup=true flag on first heartbeat so the server can trigger recovery
- finalize the linked desktop task when a phase2 monitor callback arrives via desktop_tasks path
- short-circuit the desktop monitor attempt write after the callback already finalized it

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 09:11:23 +08:00
parent f91d2645d3
commit ce028c56bf
6 changed files with 491 additions and 38 deletions
@@ -749,6 +749,7 @@ async function sendHeartbeat(source: "startup" | "timer"): Promise<void> {
cpu_arch: process.arch,
client_version: state.client?.client_version ?? "0.1.0-dev",
channel: state.client?.channel ?? "dev",
startup: source === "startup",
account_ids: state.accounts
.filter((account) =>
getProjectedAccountHealth({
@@ -1634,18 +1635,17 @@ async function executeLeasedDesktopMonitorTask(
buildMonitoringResultPayload(leased.lease_token as string, execution),
);
const completed = await completeDesktopTask(taskRecord.id, {
lease_token: leased.lease_token as string,
status: execution.status,
payload: execution.payload,
error: execution.error,
});
upsertTaskFromInfo(completed, {
routing,
summary: execution.summary,
attemptId: null,
leaseToken: null,
});
const existing = state.tasks.get(taskRecord.id);
if (existing) {
existing.status = execution.status;
existing.summary = execution.summary;
existing.result = execution.payload ?? null;
existing.error = execution.error ?? null;
existing.updatedAt = Date.now();
existing.attemptId = null;
existing.leaseToken = null;
state.tasks.set(taskRecord.id, existing);
}
noteLeaseReleased(execution.status === "failed" ? "failed" : "completed", taskRecord.id);
noteMonitorTaskCompleted(taskRecord.id);
recordActivity(
@@ -1738,29 +1738,15 @@ async function executeLeasedDesktopMonitorTask(
const failureSummary = monitoringResultWriteError
? `${failureMessage || "监控任务执行失败。"}monitoring 失败回传失败:${errorMessage(monitoringResultWriteError)}`
: (failureMessage || "监控任务执行失败。");
const completed = await completeDesktopTask(taskRecord.id, {
lease_token: leased.lease_token as string,
status: "failed",
error: structuredError,
}).catch(async (completeError) => {
const existing = state.tasks.get(taskRecord.id);
if (existing) {
existing.status = "failed";
existing.error = structuredError;
existing.summary = `${failureSummary}desktop task 回写失败:${errorMessage(completeError)}`;
existing.updatedAt = Date.now();
state.tasks.set(taskRecord.id, existing);
}
return null;
});
if (completed) {
upsertTaskFromInfo(completed, {
routing,
summary: failureSummary,
attemptId: null,
leaseToken: null,
});
const existing = state.tasks.get(taskRecord.id);
if (existing) {
existing.status = "failed";
existing.error = structuredError;
existing.summary = failureSummary;
existing.updatedAt = Date.now();
existing.attemptId = null;
existing.leaseToken = null;
state.tasks.set(taskRecord.id, existing);
}
noteLeaseReleased("failed", taskRecord.id);
noteMonitorTaskCompleted(taskRecord.id);