diff --git a/src/main/database-base.ts b/src/main/database-base.ts index 1133cb60..fdfed48d 100644 --- a/src/main/database-base.ts +++ b/src/main/database-base.ts @@ -285,6 +285,7 @@ export class AppDatabaseBase { listTasksForAssignees(companyId: string, assigneeIds: string[]) { return this.tasks.listTasksForAssignees(companyId, assigneeIds); } listReviewTasksForManager(managerId: string, companyId: string) { return this.tasks.listReviewTasksForManager(managerId, companyId); } countActiveTasksForCompany(companyId: string) { return this.tasks.countActiveTasksForCompany(companyId); } + countAdvancingTasksForCompany(companyId: string) { return this.tasks.countAdvancingTasksForCompany(companyId); } listChildTasks(parentId: string, companyId: string) { return this.tasks.listChildTasks(parentId, companyId); } advanceTaskStatus(taskId: string, newStatus: string, comment?: string) { return this.tasks.advanceTaskStatus(taskId, newStatus, comment); } setTaskRequiresUserReview(taskId: string, value: boolean) { return this.tasks.setTaskRequiresUserReview(taskId, value); } diff --git a/src/main/database/task-queries.ts b/src/main/database/task-queries.ts index 4d7fca43..02e2eebf 100644 --- a/src/main/database/task-queries.ts +++ b/src/main/database/task-queries.ts @@ -112,6 +112,24 @@ export function buildTaskQueryMethods( return row.cnt; }, + /** + * Count tasks that are actively producing progress this cycle — + * todo (waiting to be checked out) and in_progress (currently being + * worked). Excludes in_review (waiting on a human or manager) and + * blocked (waiting on a resolution). Used by the bootstrap + * scheduler to decide whether the CEO needs to be pinged: a company + * where every task is stuck in review or blocked is functionally + * idle even though countActiveTasksForCompany reports > 0. + */ + countAdvancingTasksForCompany(companyId: string): number { + const row = getDb() + .prepare( + "select count(*) as cnt from tasks where company_id = ? and status in ('todo','in_progress')", + ) + .get(companyId) as { cnt: number }; + return row.cnt; + }, + listChildTasks( parentId: string, companyId: string, diff --git a/src/main/orchestration/scheduler-heartbeat.ts b/src/main/orchestration/scheduler-heartbeat.ts index 5da0096c..02a5a342 100644 --- a/src/main/orchestration/scheduler-heartbeat.ts +++ b/src/main/orchestration/scheduler-heartbeat.ts @@ -95,15 +95,23 @@ export function createHeartbeatScheduler( } const goalCount = db.countActiveGoalsForCompany(company.id); - const taskCount = db.countActiveTasksForCompany(company.id); - if (goalCount > 0 && taskCount === 0) { + // Bootstrap when the company has active goals but no work is + // actually advancing. The previous gate ("taskCount === 0") + // missed the case where every task is in_review or blocked — + // the company looks busy to the counter but produces zero + // progress, and the CEO would not be woken until the next + // regular timer interval. countAdvancingTasksForCompany + // counts only todo + in_progress so bootstrapping still + // fires once the team stalls. + const advancingCount = db.countAdvancingTasksForCompany(company.id); + if (goalCount > 0 && advancingCount === 0) { const ceo = db.getCeoForCompany(company.id); const ceoLastWake = ceo?.lastHeartbeatAt ? new Date(ceo.lastHeartbeatAt).getTime() : 0; const ceoLastAttempt = ceo ? attemptedAtByAgent.get(ceo.id) ?? 0 : 0; const ceoLastTouch = Math.max(ceoLastWake, ceoLastAttempt); const bootstrapCooldown = 120_000; if (ceo && !hasActiveRun({ agentId: ceo.id, companyId: company.id }) && (now - ceoLastTouch > bootstrapCooldown)) { - logger.info(`[heartbeat] Company ${company.name} has ${goalCount} goals but no tasks — bootstrapping CEO ${ceo.name}`); + logger.info(`[heartbeat] Company ${company.name} has ${goalCount} goal(s) and no advancing tasks — bootstrapping CEO ${ceo.name}`); tryWake(ceo.id, company.id, ceo.name, "bootstrap"); } } diff --git a/src/main/prompt-builder/wake-reasons/en.ts b/src/main/prompt-builder/wake-reasons/en.ts index 0b3ecba9..467f0452 100644 --- a/src/main/prompt-builder/wake-reasons/en.ts +++ b/src/main/prompt-builder/wake-reasons/en.ts @@ -27,8 +27,8 @@ export const WAKE_REASONS: Record = { title: "Regular heartbeat check-in. This is your periodic wake.", steps: () => [ "Call GET /api/agents/me/standup to get your full briefing", - "Review all your assigned tasks and their statuses", - "Take action on the most important item: reviews > unblocking > new work", + "Walk `unreadMessages` first — every unanswered peer DM blocks somebody else's next move. Reply and mark read before you pick up task work.", + "Then take action on the most important item remaining: reviews > unblocking reports > next step on assigned work", "If nothing actionable is in your briefing, return control without posting a status comment — activity noise is worse than silence.", ], },