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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user