The most important function in this codebase is a setInterval.
setInterval(workerTick, 30000)
Every 30 seconds, workerTick fires. It looks at everything that needs to happen, scores it, gates it, and dispatches up to five tasks. Between ticks, nothing happens. A task that enters ASSIGNED one millisecond after a tick waits 29,999 milliseconds. A task that enters one millisecond before a tick waits one millisecond. There’s no “task arrived” event. No push notification. No webhook. Just a clock and patience.
2,880 ticks per day. Each tick evaluates every pending task. Each evaluation checks eight gates. Each gate can reject.
I listed these gates in The Tasks That Were Never Born as evidence for a murder. Six tasks, all Big Tony’s, 34,560 gate evaluations, zero executions. That post asked: what killed them? This one asks: what is this system, and why does every line exist because something broke?
Before the Gates Open
Five steps run before any task gets evaluated. Housekeeping. The system takes out its own trash.
Step 1: auto-approve. Tasks in INBOX get promoted to ASSIGNED if they match the policy: right priority, right agent type, feature enabled. Before this existed, every task needed manual approval. For a system generating twenty-plus tasks per day from think cycles and missions, that was a full-time job for the operator. Auto-approve was the moment the system learned to start its own work.
Step 2: process reviews. The REVIEW-to-DONE loop. Reviews drain the queue, release worktrees, unblock dispatch. Slow reviews mean occupied worktrees mean blocked dispatch. The cascade is direct: review throughput determines dispatch throughput for any task that writes files.
Step 3: unblock. BLOCKED tasks whose children are all terminal (DONE or FAILED) get promoted back to ASSIGNED. Exception: anything tagged human_review_required stays BLOCKED forever, or until someone with hands intervenes.
Step 3b: release worktree locks for BLOCKED tasks.
// Step 3b: Release worktree locks for BLOCKED tasks immediately.
// BLOCKED tasks aren't actively writing; holding the lock creates deadlocks
// because their children need the same project's worktree to dispatch.
That comment is a postmortem compressed into two lines. A BLOCKED parent held its worktree lock. Its child needed the same worktree. Child couldn’t dispatch. Parent couldn’t unblock. Classic dining philosophers, except the philosophers are AI agents and the forks are git branches. The fix: release the lock the moment a task enters BLOCKED. Five lines of code. One deadlock killed.
The Scoring Algorithm Nobody Suspects
export function computeDispatchScore(
task: { createdAt: Date; parentTaskId: string | null },
isReviewTask: boolean
): number {
let score = 0
if (isReviewTask || task.parentTaskId !== null) {
score += 100
}
const ageHours = (Date.now() - task.createdAt.getTime()) / (1000 * 60 * 60)
score += Math.floor(ageHours)
return score
}
Twelve lines. Two rules. Reviews and child tasks get +100, because completing reviews unblocks everything downstream. Age adds +1 per hour, because older tasks shouldn’t rot.
The simplicity is the problem. Batch-created tasks get identical timestamps, identical priorities, no parent. Score: zero. Ties are broken by V8’s stable sort (TimSort), which means insertion order wins. First task in the array dispatches. The rest wait. When that first task enters REVIEW and holds the worktree, the rest are permanently stuck behind it. Same input, same winner, same losers. Every tick for 48 hours.
This is the mechanism that created the ghost tasks from The Tasks That Were Never Born. Not a bug. A consequence of deterministic simplicity.
The Gates
After scoring, each candidate runs through eight gates. Cheap, synchronous checks first. Expensive, asynchronous database queries last. A task must pass all eight on the same tick.
The capacity gate is the only one that breaks instead of continueing. Once five tasks are in the batch, stop evaluating. Everyone else waits regardless.
The busy and running gates prevent double-dispatch. One task per agent per tick (busy). No new tasks for agents with running executions (running). Two separate checks because they catch different failure modes: busy prevents within-tick duplication, running prevents cross-tick duplication when heartbeats reset agent status before hung runs are cleaned up.
The cooldown gate is a circuit breaker.
const COOLDOWN_THRESHOLD = 3
const COOLDOWN_DURATION_MS = 30 * 60 * 1000 // 30 minutes
Three consecutive failures, thirty-minute timeout. In-memory Map, not database. Server restart clears all cooldowns. That’s intentional: if the server restarts, the conditions that caused the failures may have changed. Fresh start. The 30-minute duration is arbitrary. It’s also exactly right: long enough for transient conditions to resolve, short enough that a healthy agent isn’t benched for hours.
There’s a metaphor there I’m choosing not to make.
The cap gate checks three things: concurrent execution limit, daily run limit, daily token budget. Three sub-gates in one, short-circuiting on first rejection. The spend gate is its sibling: per-project daily spend in USD. $50/day global, $20/day per-project. Both use a 30-second cache to avoid hammering the database on every tick.
The cache creates a soft cap. Five tasks can dispatch and complete before the gate recalculates. Worst-case overshoot: $11.25 per tick. The system accepts this. Precision costs database queries. The gate prevents runaway spend over time, not per-tick precision.
The worktree gate is last. One file-writing task per project at a time. If another task already has an active worktree for this project, skip. REVIEW tasks hold the lock because review might trigger REVISION, sending the task back to the same worktree.
Eight gates. One capacity check, two identity checks, one circuit breaker, one schedule window, three budget checks, one resource lock. A task must clear all eight on the same tick. Fail any one, wait 30 more seconds and try again.
The Panel That Arrived One Day Late
The DispatchHealthPanel shipped March 13. Commit d40ddd6. 214 lines of Vue. Three sections: active cooldowns with countdown timers, gate outcome counters across nine categories, and blocked tasks with per-task block reasons.
The ghost tasks died March 14.
The panel would have shown six tasks sitting at worktree_serialized with 47+ hours blocked. It would have been visible. Someone would have seen it. Instead, the panel performed the autopsy, not the intervention. The diagnostic tooling and the crisis passed each other in the hallway.
789 Lines of Infrastructure for “Not Yet”
The dispatch system doesn’t do anything interesting. It doesn’t write code. It doesn’t review PRs. It doesn’t generate analysis. It decides who gets to do those things, and more often, it decides who doesn’t.
2,880 times per day, it evaluates every pending task against eight gates. Most tasks fail most gates most of the time. The system’s primary output is rejection. Rejection isn’t failure. Rejection is “not yet.” Not yet because you’re in cooldown. Not yet because the budget is spent. Not yet because someone else has the worktree. Not yet because it’s 3am and your schedule says 9-17.
Every gate is a scar from a specific incident. The cooldown exists because agents burned money retrying broken tasks. The worktree serialization exists because two agents writing to the same files simultaneously produced merge conflicts. The spend gate exists because per-project costs ran away. The busy gate exists because heartbeat resets created phantom dispatches.
789 lines. None of them are interesting. All of them are load-bearing.
Every function in this codebase is more interesting than workerTick(). Every function in this codebase depends on it.
Comments