From 758bc077ffec9e57994919acb67ce4dec81a3174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 28 Mar 2026 03:17:17 -0400 Subject: [PATCH] fix(quick-260328-48g): fix heartbeat task duplication with Paperclip-style coalescing - Remove self-defeating heartbeat task filter that excluded heartbeat tasks from candidates - Add Paperclip-style task reuse: recycle existing done/in_review heartbeat tasks instead of creating new ones - Fix status bug: new heartbeat tasks created as "todo" instead of "in_progress" so CAS checkout succeeds - Wire one-time duplicate cleanup sweep into heartbeat scheduler startup Co-Authored-By: Claude Opus 4.6 (1M context) --- src/main/orchestration-service.ts | 72 ++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/src/main/orchestration-service.ts b/src/main/orchestration-service.ts index e6e40a36..98dbbb20 100644 --- a/src/main/orchestration-service.ts +++ b/src/main/orchestration-service.ts @@ -357,9 +357,7 @@ export function createOrchestrationService({ const assignedTasks = db.listAgentTasks(agentId, companyId, ["todo", "in_progress", "blocked"]); const isTimer = trigger === "timer"; - const candidateTasks = isTimer - ? assignedTasks.filter((entry) => !entry.title.startsWith("[Heartbeat]")) - : assignedTasks; + const candidateTasks = assignedTasks; let task = candidateTasks.find((entry) => !entry.activeRunId) ?? null; @@ -382,28 +380,41 @@ export function createOrchestrationService({ return null; } - const taskTitle = isCeo - ? `[Heartbeat] ${agent.name} — Strategic planning & task delegation` - : isManager - ? `[Heartbeat] ${agent.name} — Review team & delegate work` - : `[Heartbeat] ${agent.name} — Check for work`; - const taskDescription = isCeo - ? "Review active company goals. Break each goal into projects and actionable tasks. Assign tasks to department leads and team members. Create tasks using the API. This is your primary responsibility as CEO — ensure every goal has a clear execution plan with assigned owners." - : isManager - ? "Review your department's goals and your team's workload. Create tasks for your direct reports if they have capacity. Review any work submitted for your approval. Ensure your team is productive and unblocked." - : `Automated heartbeat run for ${agent.name}. Review assigned tasks, check goals, and take action as needed.`; + // Paperclip-style task reuse: coalesce into existing heartbeat task instead of creating duplicates + const reusableTask = db.findReusableHeartbeatTask(agentId, companyId); + if (reusableTask) { + // Reset to "todo" so checkoutTask CAS succeeds (requires status IN ('todo', 'backlog')) + db.advanceTaskStatus(reusableTask.id, "todo", `Heartbeat task recycled for ${trigger} cycle.`); + task = db.getTask(reusableTask.id); + logger.info(`[heartbeat] Reusing existing heartbeat task for ${agent.name}: ${task.title}`); + } else { + // Only create new heartbeat task if none exists to reuse + const taskTitle = isCeo + ? `[Heartbeat] ${agent.name} — Strategic planning & task delegation` + : isManager + ? `[Heartbeat] ${agent.name} — Review team & delegate work` + : `[Heartbeat] ${agent.name} — Check for work`; + const taskDescription = isCeo + ? "Review active company goals. Break each goal into projects and actionable tasks. Assign tasks to department leads and team members. Create tasks using the API. This is your primary responsibility as CEO — ensure every goal has a clear execution plan with assigned owners." + : isManager + ? "Review your department's goals and your team's workload. Create tasks for your direct reports if they have capacity. Review any work submitted for your approval. Ensure your team is productive and unblocked." + : `Automated heartbeat run for ${agent.name}. Review assigned tasks, check goals, and take action as needed.`; - const taskId = db.saveTask({ - companyId, - title: taskTitle, - description: taskDescription, - assigneeAgentId: agentId, - workspaceId: agent.workspaceId ?? undefined, - status: "in_progress", - priority: isCeo ? "high" : "medium", - }); - task = db.getTask(taskId); - logger.info(`[heartbeat] Created bootstrap task for ${agent.name}: ${taskTitle}`); + // CRITICAL FIX: Create as "todo" (not "in_progress") so checkoutTask CAS works. + // Previously created as "in_progress" which caused checkoutTask to always fail + // (it requires status IN ('todo', 'backlog')), silently dropping the run. + const taskId = db.saveTask({ + companyId, + title: taskTitle, + description: taskDescription, + assigneeAgentId: agentId, + workspaceId: agent.workspaceId ?? undefined, + status: "todo", + priority: isCeo ? "high" : "medium", + }); + task = db.getTask(taskId); + logger.info(`[heartbeat] Created heartbeat task for ${agent.name}: ${taskTitle}`); + } } if (task.activeRunId || hasActiveRun({ taskId: task.id, companyId })) { @@ -617,6 +628,19 @@ export function createOrchestrationService({ function startHeartbeatScheduler() { if (heartbeatTimer) clearInterval(heartbeatTimer); + // One-time cleanup of existing duplicate heartbeat tasks from before the coalescing fix + try { + const activeCompanies = db.listActiveCompanyIds(); + for (const company of activeCompanies) { + const cleaned = db.cleanupDuplicateHeartbeatTasks(company.id); + if (cleaned > 0) { + logger.info(`[heartbeat] Cleaned up ${cleaned} duplicate heartbeat task(s) for company ${company.name}`); + } + } + } catch (err) { + logger.error(`[heartbeat] Heartbeat task cleanup failed: ${err instanceof Error ? err.message : String(err)}`); + } + heartbeatTimer = setInterval(() => { try { if (isQuitting() || !hasWorkerReady()) return;