Merge branch 'worktree-agent-a8526086'

# Conflicts:
#	.planning/STATE.md
This commit is contained in:
ZacharyZhang-NY committed 2026-03-28 03:19:28 -04:00
commit 29e4f9f430
3 files changed
+412 -24

No files matched your search

+52
View File
@@ -2020,6 +2020,58 @@ export class AppDatabase {
);
}
/**
* Find the most recent completed heartbeat task for this agent that can be reused.
* Returns a task with status "done" or "in_review", ordered by most recently updated.
* Returns null if no reusable heartbeat task exists.
*/
findReusableHeartbeatTask(agentId: string, companyId: string): TaskRecord | null {
const row = this.db
.prepare(
`SELECT id, company_id as companyId, project_id as projectId, goal_id as goalId,
parent_id as parentId, title, description,
assignee_agent_id as assigneeAgentId, workspace_id as workspaceId,
status, priority, active_run_id as activeRunId,
session_json as sessionJson, session_display_id as sessionDisplayId,
cost_usd as costUsd, requires_user_review as requiresUserReview,
created_at as createdAt, updated_at as updatedAt
FROM tasks
WHERE company_id = ? AND assignee_agent_id = ? AND title LIKE '[Heartbeat]%'
AND status IN ('done', 'in_review')
ORDER BY updated_at DESC
LIMIT 1`,
)
.get(companyId, agentId) as TaskRecord | undefined;
if (row) {
row.requiresUserReview = Boolean(row.requiresUserReview);
}
return row ?? null;
}
/**
* Cancel all duplicate heartbeat tasks per agent within a company, keeping only the most
* recently updated one per agent. Does NOT touch in_progress tasks (they may have active runs).
* Returns the number of tasks cancelled.
*/
cleanupDuplicateHeartbeatTasks(companyId: string): number {
const ts = nowIso();
const result = this.db
.prepare(
`UPDATE tasks SET status = 'cancelled', updated_at = ?
WHERE company_id = ? AND title LIKE '[Heartbeat]%'
AND status IN ('done', 'in_review', 'todo', 'blocked', 'backlog')
AND id NOT IN (
SELECT id FROM (
SELECT id, ROW_NUMBER() OVER (PARTITION BY assignee_agent_id ORDER BY updated_at DESC) as rn
FROM tasks
WHERE company_id = ? AND title LIKE '[Heartbeat]%' AND status != 'cancelled'
) WHERE rn = 1
)`,
)
.run(ts, companyId, companyId);
return Number(result.changes);
}
/**
* List tasks from an agent's direct reports that are awaiting review (status = in_review).
* Used to populate the "Tasks Awaiting Your Review" section in the agent prompt.
+48 -24
View File
@@ -401,9 +401,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;
@@ -426,28 +424,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}`);
}
}
// Generate runId upfront for CAS checkout (EXEC-01, EXEC-02)
@@ -707,6 +718,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;