pub fn run_loop(
queue: &mut dyn Queue,
dispatcher: &dyn Dispatcher,
budget: &LoopBudget,
journal: &Journal,
cancel: &dyn Fn() -> bool,
per_unit_max_dispatches: Option<u64>,
) -> Result<LoopOutcome, LoopError>Expand description
Run a walk over the queue until the queue is empty, the budget is exhausted, or the cancel sentinel fires.
§Algorithm
loop:
check cancel -> Interrupted
unit = queue.next() -> None: NoWork
resume guard: if journal has a termination event for unit.session_key,
close the unit without dispatching, journal node_closed, and continue.
inner dispatch loop:
check budget -> Budget
check cancel -> Interrupted
iterations_used += 1
per_unit_cap check: if unit_dispatches >= cap, synthesize NoProgress park
emit loop_unit_dispatched
run session, wait
if journal has termination event: close unit, journal node_closed, break
if exit is claude's bg-guard refusal: close unit (NoProgress) + node_closed,
break -- do NOT re-dispatch (x-4504, AC1-ERR)
else: emit node_failed, continue inner loop (re-dispatch)§Journal invariant
Every journal.append call for project events is fatal on failure.
An unobservable walk must not continue spending.
§per_unit_max_dispatches
When Some(N), a unit that accumulates N dispatches without a termination
event is synthesized a NoProgress Evidence and parked via queue.close().
The walk continues to the next unit. None means no per-unit cap (the
walk-level budget is the only ceiling, as in the original degenerate policy).