fix(orchestration): enforce approval gates on heartbeat and recovery paths

The heartbeat path (canRunAgent → startAgentHeartbeatRun) only checked
isAgentPendingApproval (hire gate), completely bypassing plan_review,
draft_review, strategy_review and other blocking approval types. Agents
in Co-Founder mode continued running despite pending approvals.

Root cause: task-runner.prepareTaskRun had the full getBlockingApproval
check, but heartbeat runs never went through prepareTaskRun — they
created runs directly, skipping the approval gate entirely.

Three fixes:
- heartbeat.canRunAgent: add getBlockingApproval(companyId, agentId, null)
  for agent-level blocking approvals
- heartbeat.startAgentHeartbeatRun: add getBlockingApproval with task id
  after task resolution, before run creation
- task-runner.resumeQueuedRuns: add getBlockingApproval check so app
  restart does not dispatch runs past pending approvals

Also replaced the hardcoded type list in hasPendingReviewApprovalForAgent
with a dynamic join on approval_policies.blocks_execution, eliminating
the sync risk when new blocking approval types are added.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-29 20:17:33 -04:00
1 parent c9deca5454
commit 1e36259c5d
3 files changed
+27 -1

No files matched your search

+2 -1
View File
@@ -415,9 +415,10 @@ export function buildApprovalMethods(
.prepare(
`SELECT 1 as hit FROM approvals a
JOIN tasks t ON a.related_task_id = t.id
JOIN approval_policies p ON p.approval_type = a.type
WHERE a.company_id = ? AND a.state = 'pending'
AND t.assignee_agent_id = ?
AND a.type IN ('plan_review','scope_review','draft_review','strategy_review','deliverable_review','exception_review')
AND p.blocks_execution = 1
LIMIT 1`,
)
.get(companyId, agentId) as { hit: number } | undefined;