From 0c6777901277a226c14df80dcc8734363ae6aaaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 19 Apr 2026 20:09:33 -0400 Subject: [PATCH] Drop stale active_run_id when a task returns to todo/backlog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit advanceTaskStatus cleared checkout_run_id when transitioning out of in_progress but left active_run_id alone. Heartbeat selects candidate tasks by !activeRunId, so a task recovered from a crash (moved blocked → todo by the bootstrap recovery) could carry a dead active_run_id pointer and remain silently unpickable by every future wake, even though the run it points to is long since interrupted. interruptStaleRuns normally clears active_run_id for tasks whose owning run is now interrupted, but the guarantee relied on a single startup pass. Make advanceTaskStatus defensive: when the terminal state is todo or backlog, clear the pointer too. The invariant holds end-to-end instead of relying on interruptStaleRuns having seen every edge case. Also harden the new-CEO team-building cue: make it an explicit imperative ("call the bash tool to run the fetch POSTs now") and route app_restart_resume through the same branch so a startup-crash recovery wake lands on the same focused action script. --- src/main/database/tasks-status.ts | 7 +++++++ src/main/orchestration/initial-user-turn.ts | 9 +++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/database/tasks-status.ts b/src/main/database/tasks-status.ts index 9c7aa8d8..f0697cec 100644 --- a/src/main/database/tasks-status.ts +++ b/src/main/database/tasks-status.ts @@ -110,6 +110,13 @@ export function buildTaskStatusMethods( if (newStatus !== "in_progress") { timestampSets.push("checkout_run_id = NULL"); } + // Returning a task to todo / backlog drops any active-run claim + // as well — heartbeat picks candidate tasks by !activeRunId, so + // leaving a stale pointer here (e.g. after startup recovery of a + // crashed run) silently hides the task from every future wake. + if (newStatus === "todo" || newStatus === "backlog") { + timestampSets.push("active_run_id = NULL"); + } getDb() .prepare(`UPDATE tasks SET ${timestampSets.join(", ")} WHERE id = ?`) diff --git a/src/main/orchestration/initial-user-turn.ts b/src/main/orchestration/initial-user-turn.ts index 49181084..bffe9064 100644 --- a/src/main/orchestration/initial-user-turn.ts +++ b/src/main/orchestration/initial-user-turn.ts @@ -59,7 +59,7 @@ export function resolveInitialUserTurn( // Shortcut: when a CEO has zero active direct reports, replace the // generic timer cue with a team-building cue that matches the CEO // mode prompt. - if (trigger === "timer" || trigger === "bootstrap") { + if (trigger === "timer" || trigger === "bootstrap" || trigger === "app_restart_resume") { try { const agent = deps.db.getAgent(agentId); if (!agent.reportsTo) { @@ -67,7 +67,12 @@ export function resolveInitialUserTurn( .getDirectReports(agentId, companyId) .filter((r) => r.status !== "pending_approval" && r.status !== "terminated"); if (activeReports.length === 0) { - return "You are the CEO and have zero active direct reports. Follow your Team Building Phase operating instructions: file 3–5 hire_agent POSTs this wake, then file one approve_ceo_strategy, then return. Do not write plan documents or analyse the filesystem beyond a 20-second read."; + return [ + "You are the CEO of this company. Your team is empty (zero active direct reports).", + "This wake has ONE job: call the `bash` tool to execute `node -e '...'` fetch POSTs that file 3–5 hire_agent requests against /api/companies/{companyId}/agents, then one approve_ceo_strategy POST against /api/companies/{companyId}/approvals.", + "The exact body shape and connectorId rules are in your Team Building Phase operating instructions above. Use a connectorId and model from the Available Connectors section (only ready connectors).", + "Start the first POST NOW. Do not write a plan document. Do not describe what you will do — execute the tool call.", + ].join("\n\n"); } } } catch {