Scheduler

This page defines the current contract for Holon's scheduler: what inputs it consumes, how it derives posture and runnability, and what decisions it emits. It also documents the additive protocol transition layer that wraps scheduler decisions in atomic transactions with replay protection, explicit activation ownership, terminal settlement, and a public diagnostic event stream.

Last verified: 2026-08-18 against src/runtime/scheduler.rs, src/runtime/scheduler_executor.rs, src/runtime/waiting.rs, src/runtime/closure.rs, src/runtime/turn/execution.rs, src/runtime_db/transitions.rs, src/runtime_event.rs, and src/types.rs.

Source RFCs

Core model

The scheduler is the runtime component that answers: given the current agent state, what should happen next?

It consumes a SchedulerProjection — a snapshot assembled from:

InputSource
Agent statusAgentState.status
Queue depthAgentState.pending
Active tasksTaskRecords with non-terminal status
Current WorkItemcurrent_work_item_idWorkItemRecord
Runnable WorkItemsOpen WorkItems with is_runnable()=true
Wait conditionsActive WaitConditionRecords
Waiting intentsActive WaitingIntentRecords
Wake hintsPendingWakeHint
Turn stateturn_in_progress, last_turn_terminal
Runtime errorsruntime_error_active()

The projection is a read-only snapshot; the scheduler never mutates durable state directly. Decisions are emitted and handed to the executor.

Scheduler inputs (SchedulerInput)

Input variantTrigger
MessageA new message arrived in the agent's queue
IdleSignal::WakeHintA pending wake hint was received
IdleSignal::ContinueActiveA WorkItem was runnable at the last closure
IdleSignal::QueuedAvailableA queued message is ready for processing
IdlePeriodic idle boundary check

Scheduler decisions (SchedulerDecisionKind)

DecisionMeaning
StartModelTurnStart a new model turn with context assembly
ReduceMessageOnlyReduce a message without starting a full model turn
EmitSystemTickEmit a runtime-owned follow-up message (system tick)
WaitForTaskBlock until a non-terminal task completes
WaitForExternalChangeBlock until an external event arrives
WaitForTimerBlock until a timer fires
WaitForOperatorBlock until operator input arrives
SleepRuntime moves the agent to asleep; no immediate action
StayIdleAgent is already asleep; no action
StopAgent is stopped; no scheduling possible
NoopNo action (duplicate suppressed, turn in progress)

Each decision carries metadata: reason, model_reentry, liveness_only, work_item_id, task_id, and evidence.

Decision flow

                    SchedulerInput
                         │
                         ▼
              ┌─────────────────────┐
              │ Status == Stopped?  │──Yes──► Stop
              └─────────┬───────────┘
                        │ No
                        ▼
              ┌─────────────────────┐
              │ Turn in progress?   │──Yes──► Noop
              └─────────┬───────────┘
                        │ No
                        ▼
         ┌──────────────────────────┐
         │ Queue has pending input? │──Yes──► StartModelTurn
         └──────────────┬───────────┘        (or ReduceMessageOnly)
                        │ No
                        ▼
         ┌──────────────────────────┐
         │ Runnable WorkItem?       │──Yes──► EmitSystemTick
         └──────────────┬───────────┘        (ContinueActive)
                        │ No
                        ▼
         ┌──────────────────────────┐
         │ Active wait condition?   │──Yes──► WaitFor{Task,
         └──────────────┬───────────┘         External,Timer,Operator}
                        │ No
                        ▼
                      Sleep

WorkItem scheduling states

WorkItems flow through scheduling states that the scheduler consumes:

StateMeaningScheduler action
RunnableReady for processingMay be auto-picked as current
WaitingOperatorplan_status=NeedsInput or operator waitAgent waits for operator
Blockedblocked_by set without a more specific waitNot runnable; check legacy recheck_at when present
WaitingTaskWait condition on task resultWake on task terminal
WaitingExternalWait condition on external eventWake on external trigger
WaitingTimerRuntime timer waitWake when timer fires
WaitingSystemRuntime system-tick waitEmit system tick
Completedstate=CompletedExcluded from runnable set

Wake/sleep boundary

Protocol transition layer

The scheduler wraps each boundary in an atomic QueueTransitionCommand transaction that can simultaneously:

  1. commit the queue operation (admit, claim, or enqueue);
  2. update the agent state projection;
  3. persist message evidence, transcript entries, and audit events;
  4. bind a canonical activation owner and execution disposition; and
  5. persist settlement, recovery, and delivery evidence.

All effects commit in the same SQLite transaction. If the transaction fails or the CAS does not match, no partial queue, activation, settlement, or delivery state is left behind.

The canonical scheduler is the only runtime engine. Queue, WorkItem, wait, task, Turn, transcript, brief, delivery, activation, settlement, and execution facts share one authority and transaction path.

The accepted transition contract retires runtime manifest/preflight gates, per-scenario authority, automatic hard-blocker rollback, production shadow comparison, the legacy engine, and the runtime engine selector. Historical selector configuration is not a runtime input.

Integration points

QueueTransitionCommand is committed at every scheduler boundary. Each boundary records the canonical facts required by the next boundary:

BoundaryOperationRequired canonical evidence
Message admission (scheduler_executor::prepare_message)Claiminput identity, activation owner, disposition, authority fence
Wait resumeClaimexact wait id and generation, consuming activation
Settlement (runtime::commit_queue_settlement)Settlematching activation, terminal Turn, WorkItem disposition
Delivery dispositionSettlesettlement-bound brief or delivery evidence
Operator interjectionAdmitrunning activation and safe-point identity
Work-queue idle tick (memory_refresh::emit_system_tick_from_work_queue)Admitrunnable WorkItem identity, generation, and source revision

The semantic decision plane is not part of production admission. Its production module and fixtures have been removed. Deterministic structural binding and the canonical protocol retain all state-transition control.

Public diagnostic event stream

The scheduler emits a typed SchedulerDiagnosticAuditEvent for every decision that passes through append_scheduler_decision. This event carries:

FieldContent
decisionSchedulerDecisionKind variant
reasonHuman-readable decision reason
boundaryWhere the decision was made (e.g. run_loop, after_provider_round)
message_idOptional message that triggered the decision
evidenceEvidence strings used by the decision
scenario_classOptional scenario classification (e.g. operator_interjection)
shadow_matchedHistorical compatibility field; production does not require shadow comparison
divergence_codeHistorical compatibility field for previously recorded comparisons

The event is emitted via RuntimeEventKind::SchedulerDiagnostic alongside the legacy scheduler_decision audit event. Both are persisted in the same transaction as the scheduler decision. The typed event is the public observability surface; the legacy audit event remains for backward compatibility.

Scheduling advisories

SchedulingAdvisory is an internal, non-authoritative warning system that detects potential scheduler state mismatches: idle posture with runnable work, weak external wait recoverability, unrecoverable blocked WorkItems, and similar conditions. Advisories are appended as scheduling_advisory audit events with deduplication against recent events.

Advisories are not diagnostics in the diagnostic event stream sense. They are internal hints for debugging and operational awareness; the deterministic scheduler projection and posture derivation remain the sole authority for scheduling decisions.

Known gaps