Chain CEO runs through self-wake during team building

triggerSelfWake fired only when the completing agent had at least one
actionable task (todo / in_progress). A CEO in team_building mode has
zero tasks by definition — each heartbeat hired one or two agents,
returned, and then waited for the next 2-minute timer tick. Operators
watched bootstrap crawl: in the captured field log, six heartbeats
burned 12 minutes before the first hire landed.

Teach triggerSelfWake to also chain when the agent is a top-level CEO
with active goals, zero active direct reports, and no already-pending
hire_agent approvals — the exact state team_building.en.md is telling
the model to resolve. The existing circuit breaker (MAX_CONSECUTIVE_WAKES
without progress) still applies, so a CEO that keeps returning without
filing hires gets paused and its manager wake — the board / human —
is raised instead.

Once reports activate or all intended hires are filed, the predicate
flips to false and runs resume the normal heartbeat cadence.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-19 21:10:28 -04:00
1 parent 6b22fa9f0e
commit 834ac913ca
1 file changed
+37 -1
@@ -295,7 +295,8 @@ export function triggerSelfWake(
try {
const actionableTasks = db.listAgentTasks(run.agentId, run.companyId, ["todo", "in_progress"]);
if (actionableTasks.length === 0) return;
const hasWorkToContinue = actionableTasks.length > 0 || hasUnfinishedTeamBuilding(db, run.agentId, run.companyId);
if (!hasWorkToContinue) return;
const allowed = cb.tryWake(run.agentId, run.companyId);
if (!allowed) {
@@ -322,3 +323,38 @@ export function triggerSelfWake(
);
}
}
/**
* Returns true when a top-level agent (CEO) still has team-building work
* in flight: the company has an active goal, zero active direct reports,
* and no pending hire approvals. Each wake in that window files at most a
* handful of hire_agent POSTs, so chaining runs via self-wake instead of
* waiting for the 2-minute heartbeat timer takes bootstrap from
* 10-plus minutes down to the LLM's round-trip time.
*
* The circuit breaker on the caller side still applies, so a CEO that
* keeps completing runs without actually creating hires gets paused
* after MAX_CONSECUTIVE_WAKES attempts.
*/
function hasUnfinishedTeamBuilding(
db: HandlerDeps["db"],
agentId: string,
companyId: string,
): boolean {
try {
const agent = db.getAgent(agentId);
if (agent.reportsTo !== null) return false;
const activeReports = db
.getDirectReports(agentId, companyId)
.filter((r) => r.status !== "pending_approval" && r.status !== "terminated");
if (activeReports.length > 0) return false;
const goalCount = db.countActiveGoalsForCompany(companyId);
if (goalCount === 0) return false;
const pendingHires = db
.listCompanyApprovalsByType(companyId, "hire_agent")
.filter((a) => a.state === "pending");
return pendingHires.length === 0;
} catch {
return false;
}
}