State Machines

September 21, 2026 · View on GitHub

LoopX does not have one giant state machine. It has a small set of cooperating machines that are projected from the same canonical state bodies: registry entries, active state, todo metadata, run history, quota events, operator gates, scheduler acknowledgements, and projection sinks.

This document is not a new store and not a private incident narrative. It is a public-safe map over the current repository contracts, especially:

How The Machines Compose

flowchart LR
  Registry["Registry / active state"] --> Todo["Todo lifecycle"]
  Todo --> Quota["Quota runtime"]
  Gate["Gate scope"] --> Quota
  Owner["Owner route / handoff"] --> Quota
  Evidence["Evidence / rollout"] --> Todo
  Quota --> Scheduler["Scheduler / heartbeat"]
  Quota --> Vision["Agent vision / replan"]
  Vision --> Todo
  Quota --> Projection["Projection sinks"]
  Projection --> WriteAPI["LoopX write APIs"]
  WriteAPI --> Registry
  Onboard["Agent onboarding"] --> Registry

The top-level loop is simple:

  1. Resolve registry and active state.
  2. Project todos, gates, evidence, and current agent identity.
  3. Ask quota should-run.
  4. Either run exactly one bounded segment, ask a concrete gate, observe a waiting handle, repair the control plane, or quiet no-op according to the projected machine state.
  5. State changes return through LoopX write APIs, not through dashboard text or chat memory.

State Machine As Effect Interpretation Table

Every state machine below can be read through the same lens:

input effect -> interpreter -> decision -> observation -> next effect

This is the model described in the Agent Loop Effect Interpreter RFC. The agent loop is the loop. The harness is the effectful program. The state machine is not the product; it is the decision table inside that effect interpreter. This framing follows the public lecture 主线一:Agent Loop 是 effectful program(1).

State familyInput effectInterpreterDecisionObservationNext effect
Todo lifecycleAgent proposes work, claim, completion, or blockerTodo projection and authority rulesopen / claimed / deferred / blocked / done / supersededTodo summary and frontierNext runnable todo or successor
Quota runtimeAgent proposes a bounded turnquota should-runrun / gate / wait / repair / quietQuota packet + interaction_contractExecute, ask owner, observe, repair, or no-op
Scheduler / heartbeatHost asks when to wake againScheduler hint and ACK rulesHost RRULE / initial interval / backoffscheduler_hint packetNext heartbeat or monitor poll
Gate and capabilityAgent requests an effect with external authorityCapability and user gate rulesrepair_bridge / ask_owner / allow / blockGate packet and primary actionRepair, ask, execute, or stop
Vision and replanAgent closes or continues a bounded stageReplan and vision rulesContinue / replan / watch / closegoal_frontier_projection + vision_continuation_auditNext advancement or successor
MonitorHost polls a targetMonitor scheduler and evidence rulesDue / future / quiet / external observeMonitor poll event and scheduler hintNext poll or material transition

Each table row should answer: who owns the source state, who may interpret the effect, what decision is legal, what observation is returned, and what effect should come next.

1. Todo Lifecycle Machine

Todo is the smallest executable or waiting unit. Current source fields include status, task_class, action_kind, claimed_by, blocks_agent, global_gate, decision_scope, required_decision_scopes, required_capabilities, unblocks_todo_id, resume_when, no_followup, superseded_by, monitor metadata, and evidence/reason fields.

stateDiagram-v2
  [*] --> Suggested
  Suggested --> Open: promoted / todo add
  Open --> Claimed: claimed_by set
  Claimed --> Running: quota selects this todo
  Running --> Done: validated evidence or blocker accepted
  Done --> SuccessorOpen: successor or unblock relation exists
  Done --> Archived: no follow-up or archive policy
  Open --> Blocked: status=blocked / blocker reason
  Open --> Deferred: status=deferred or resume_when
  Deferred --> ResumeReady: resume condition satisfied
  ResumeReady --> SuccessorReplan: no stable successor yet
  Open --> Superseded: superseded_by
  Superseded --> ReplacementOpen
StateSource FieldsRuntime MeaningLegal Exit
SuggestedSuggestion output or planning promptCandidate work that has not entered the durable todo list.Promote to Open or drop it.
Openstatus=open or unchecked Markdown itemDurable backlog item.Claim, block, defer, supersede, or complete.
Claimedclaimed_by=<agent_id>Soft ownership/routing signal. It is not a lock.Run if quota selects it, reassign, block, or complete.
RunningDerived from quota should-run plus run historyA bounded turn is currently attempting this item.Write evidence/blocker, then complete or reopen.
Donestatus=done or checked item plus evidenceThe item has a terminal outcome.Archive, create successor, or expose handoff clearance.
Blockedstatus=blocked, reason, capability/gate fieldsKnown blocker, not vague waiting.Repair, ask owner, supersede, or reopen.
Deferredstatus=deferred, resume_whenWaiting for a concrete condition.ResumeReady when the condition is satisfied.
Supersededsuperseded_byReplaced without deleting history.Follow ReplacementOpen.

Running is deliberately derived. Adding a persistent todo status for it would duplicate quota/run-history truth.

2. Quota / Runtime Machine

quota should-run is the compute gate. It decides whether the next automatic tick should spend compute, but it does not grant protected permissions. The current state order in loopx/quota.py is:

blocked_health -> operator_gate -> focus_wait -> eligible -> waiting -> throttled -> paused

Additional fields such as effective_action, safe_bypass_allowed, capability_gate, workspace_guard, agent_scope_frontier, heartbeat_recommendation, execution_obligation, and interaction_contract refine what the agent and host must do next.

stateDiagram-v2
  [*] --> QuotaCheck
  QuotaCheck --> Eligible: healthy + runnable or repairable
  QuotaCheck --> OperatorGate: gate covers selected action
  QuotaCheck --> FocusWait: outcome or fresh-evidence floor
  QuotaCheck --> Waiting: external evidence or monitor handle pending
  QuotaCheck --> BlockedHealth: registry/projection/boundary health broken
  QuotaCheck --> Throttled: quota exhausted
  QuotaCheck --> Paused: explicit pause

  Eligible --> BoundedRun: effective_action=run or repair
  Eligible --> ScopedFallback: scoped user gate + independent todo
  Eligible --> MonitorQuiet: effective_action=monitor_quiet_skip
  Eligible --> AgentScopeWait: no current-agent candidate
  Eligible --> SuccessorReplan: cleared handoff lacks successor/no-follow-up

  BoundedRun --> WritebackSpend: validated output
  ScopedFallback --> WritebackSpend: independent fallback validated
  SuccessorReplan --> WritebackSpend: successor/reopen/no-follow-up recorded
  MonitorQuiet --> NoSpend
  AgentScopeWait --> NoSpend
  OperatorGate --> NoSpend
  FocusWait --> WritebackSpend: recovery evidence validated
  FocusWait --> NoSpend: no safe recovery
  Waiting --> NoSpend: unchanged observation
  BlockedHealth --> WritebackSpend: repair validated
  BlockedHealth --> NoSpend: unsafe to repair
  Throttled --> NoSpend
  Paused --> NoSpend
Runtime State / ActionAgent BehaviorSpend Rule
eligible + runnable actionAttempt one bounded delivery, recovery, or repair.Spend only after validated writeback.
operator_gate / user gateAsk or surface the concrete payload.No spend for asking.
scoped_user_gate_fallbackSurface the gate and run only an independent fallback.Spend after fallback writeback.
focus_waitProduce the named outcome/fresh-evidence recovery or write a blocker.Recovery can spend after validation; passive waiting cannot.
waiting / external_evidence_observeObserve a public-safe handle or write a compact blocker.Follow the observation contract; unchanged waiting is usually no-spend.
monitor_quiet_skipPreserve liveness, optionally append one no-spend monitor poll.No spend.
agent_scope_waitStay active but quiet until reassignment, unblock, or scoped todo appears.No spend.
blocked_healthRepair registry/projection/boundary/workspace/capability if allowed.Spend only after validated repair writeback.
throttled / pausedDo not deliver.No spend.

3. Gate Decision Scope Machine

Gates are scoped authority, not a universal boolean. A gate blocks a selected action only when its scope covers that action or agent. Current source fields include task_class=user_gate, global_gate, blocks_agent, decision_scope, required_decision_scopes, operator_gate, and interaction_contract.user_channel.

flowchart TD
  Open["Gate open"] --> Scope{"scope covers selected action?"}
  Scope -->|"yes"| Ask["ask concrete user/controller question"]
  Scope -->|"no"| Fallback["keep gate visible; run independent fallback"]
  Scope -->|"ambiguous"| Repair["repair projection or ask controller"]
  Ask -->|"approve"| Consume["consume covered required scopes"]
  Consume --> Unblock["unblock gated todo when otherwise ready"]
  Ask -->|"reject"| Supersede["supersede or compensation todo"]
  Ask -->|"defer"| Defer["deferred resume_when"]
  Fallback --> Write["write fallback evidence"]
  Repair --> Recheck["rerun quota"]
TransitionRequired Evidence
Open gate -> AskConcrete payload todo/question, not only "owner gate".
Open gate -> FallbackProof that selected fallback is independent of the gate scope.
Open gate -> RepairExplanation of missing or contradictory scope fields.
ApproveCompleted, exactly linked user_gate; consume only covered target scopes and preserve the rest.
RejectSupersede or compensation record; never consume decision authority.
DeferDecision event with a supported resume_when.
Fallback completeArtifact/blocker/evidence linked to the independent todo.

This machine is why user and agent channels can intentionally disagree:

user_channel.action_required = true
agent_channel.must_attempt = true
selected_action = independent_fallback

4. Owner Route / Multi-Agent Handoff Machine

Multi-agent routing is modeled with todo ownership and handoff gates. Review is not a separate kernel state. It is a todo/gate relation that can block a named agent until an owner route completes, reassigns, or records no follow-up.

loopx/control_plane/todos/handoff_gate.py currently projects blocks_agent todos into: blocking, cleared_without_successor, cleared_with_successor, cleared_no_followup, superseded, and deferred.

stateDiagram-v2
  [*] --> OpenWork
  OpenWork --> ClaimedByAgent: claimed_by
  ClaimedByAgent --> WorkspaceGuard: quota --agent-id
  WorkspaceGuard --> AgentDelivery: correct worktree / capability
  WorkspaceGuard --> Reassigned: wrong owner or workspace
  AgentDelivery --> SelfMerged: small validated eligible change
  AgentDelivery --> OwnerRouteWait: broad / high-risk / owner-held change
  OwnerRouteWait --> Blocking: gate_state=blocking
  Blocking --> ClearedWithSuccessor: owner todo done + successor
  Blocking --> ClearedWithoutSuccessor: owner todo done + no successor
  Blocking --> ClearedNoFollowup: no_followup=true
  Blocking --> Superseded: superseded_by
  Blocking --> Deferred: resume_when
  ClearedWithoutSuccessor --> SuccessorReplan
  ClearedWithSuccessor --> SuccessorRun
  ClearedNoFollowup --> Done
  Superseded --> SuccessorRun
  Deferred --> OwnerRouteWait
  SelfMerged --> Done
Handoff StateMeaningNext Legal Action
blockingAnother owner route blocks this agent.Wait quietly or surface concrete gate.
cleared_with_successorThe blocker is done and a successor exists.Route to successor.
cleared_without_successorThe blocker is done but no successor/no-follow-up is projected.Enter successor replan before ordinary delivery.
cleared_no_followupOwner explicitly says no follow-up.Archive or continue unrelated work.
supersededA replacement todo exists.Follow replacement.
deferredResume condition is not yet satisfied.Wait or observe condition.

5. Evidence / Rollout / Rollback Machine

Evidence determines whether a state transition is trustworthy. Agent-declared "done" is not enough. A transition should be backed by artifact refs, source refs, validation results, blocker evidence, commit/PR/doc revision anchors, or compact external observations.

flowchart LR
  Hypothesis["Hypothesis / intended action"] --> Evidence["Evidence bundle"]
  Evidence -->|"validation passed"| Snapshot["Validated run snapshot"]
  Evidence -->|"validation failed"| Blocker["Blocker evidence"]
  Snapshot -->|"state changed"| Event["Rollout event"]
  Event --> Anchor["Mutation anchor"]
  Anchor -->|"needs compensation"| Rollback["Rollback / compensation event"]
  Blocker --> Successor["Successor todo"]
  Rollback --> Successor
Evidence StateCan Change Control-Plane State?Notes
HypothesisNoExplains direction only.
Evidence bundleMaybeMust include enough refs and validation shape.
Validated run snapshotYesCan drive todo completion, spend, or status projection.
Blocker evidenceYesCan justify blocked/deferred/successor states.
Rollout eventYesAppend-only lifecycle fact.
Mutation anchorYesCommit, PR, doc revision, Base row, automation version, or equivalent.
Rollback / compensationYesFix-forward and rollback remain part of history.

Rollback should never mean deleting the evidence chain. It appends a new compensating fact and usually creates or unblocks a successor todo.

6. Scheduler / Heartbeat Machine

scheduler_hint is waiting policy, not execution permission. It is derived from quota payload fields such as should_run, effective_action, heartbeat_recommendation, execution_obligation, automation_liveness, and interaction_contract.

stateDiagram-v2
  [*] --> Tick
  Tick --> RunNow: action=run_now
  Tick --> WaitUser: action=backoff_waiting_for_user
  Tick --> WaitReassign: action=backoff_until_reassigned
  Tick --> WaitMaterial: action=backoff_until_material_transition
  Tick --> WaitEvidence: action=backoff_until_fresh_evidence
  Tick --> QuietWait: action=backoff_until_state_change
  Tick --> TerminalStop: action=stop_until_explicit_resume
  Tick --> DefaultCadence: action=keep_default_cadence

  RunNow --> ActiveCadence
  WaitUser --> WiderCadence
  WaitReassign --> ConservativeWiden
  WaitMaterial --> MonitorCadence
  WaitEvidence --> EvidenceCadence
  QuietWait --> WiderCadence
  TerminalStop --> [*]
  DefaultCadence --> Tick

  WiderCadence --> FinalCheck: unchanged limit reached
  ConservativeWiden --> FinalCheck: unchanged limit reached
  MonitorCadence --> FinalCheck: unchanged limit reached
  EvidenceCadence --> FinalCheck: unchanged limit reached
  FinalCheck --> RunNow: quota changed
  FinalCheck --> StopOrKeepAlive: unchanged

  Tick --> ResetToInitial: reset_token changed
  ResetToInitial --> ActiveCadence
Scheduler ActionCurrent Cadence ClassTypical Codex App Initial / MaxMeaning
run_nowactive_work3 / 10 minutesWork or repair must be attempted.
backoff_waiting_for_userhuman_gate30 / 120 minutesConcrete user/controller action is next.
backoff_until_reassignedagent_scope_wait or peer_coordination_wait10 / 60 minutes, progression 10/20/30/60Handoff owner, peer readiness, coordinator configuration, reassignment, or new local work may unblock this agent.
backoff_until_material_transitionmonitor_wait15 / 60 minutesMonitor-only liveness without compute spend.
backoff_until_fresh_evidenceunchanged_noop60 / 240 minutesWait for fresh mapped or post-handoff evidence.
backoff_until_state_changequiet_wait30 / 120 minutesNo specific user/monitor path is projected.
stop_until_explicit_resumeterminal_no_followupstoppedLoopX-derived closure from complete todo sources, no-follow-up evidence, and an empty frontier stops recurring automation until resume or new work.
keep_default_cadencedefault3 / 30 minutesNo backoff condition is projected.

The reset token is part of the machine. When identity, selected action, recommended mode, user feedback, gate resolution, reassignment, material evidence, or active work changes the token, hosts should return to the profile initial cadence and acknowledge the scheduler state. Cadence changes do not spend quota.

7. Projection Sink Machine

Status, review packet, frontstage, manager summary, Lark Kanban, and dashboard rows are projection sinks. They make state readable; they do not own state.

flowchart LR
  Source["Canonical stores"] --> Builder["Projection builder"]
  Builder -->|"complete + fresh"| View["Read-only view"]
  Builder -->|"missing / stale / conflicting"| Gap["Projection gap"]
  Gap --> Repair["repair source or builder"]
  Repair --> Builder
  View -->|"user action"| WriteAPI["LoopX write API"]
  WriteAPI --> Source
Projection StateMeaningRequired Behavior
Read-only viewThe sink matches current source fields closely enough to display.It may guide a user/agent, but writes go through LoopX APIs.
Projection gapMissing concrete todo, stale route, conflicting source, or collapsed user/agent channel.Repair the source or projection builder before relying on it.
Write APITodo update, gate decision, refresh-state, monitor poll, spend, scheduler ack, or event append.Append durable facts; do not mutate the sink as truth.

This machine protects the public/private boundary: a projection may render public-safe summaries and evidence refs, but it must not become a dependency on private raw docs, transcripts, credentials, local paths, benchmark logs, or unredacted connector payloads.

8. Agent Onboarding / Automation Enablement Machine

Connecting a project is not the same as enabling long-running automation. The current code separates project registration, global sync, quota visibility, heartbeat opt-in, host-loop installation, and first tick verification.

stateDiagram-v2
  [*] --> Unregistered
  Unregistered --> ProjectRegistered: bootstrap / connect
  ProjectRegistered --> GlobalSyncPending: sync-global requested
  GlobalSyncPending --> GlobalRegistered: global sync wrote
  GlobalSyncPending --> GlobalWriteBlocked: registry write failed
  GlobalWriteBlocked --> RepairNeeded
  ProjectRegistered --> QuotaVisible: project registry mode
  GlobalRegistered --> QuotaVisible: global quota recognizes goal/agent
  QuotaVisible --> HeartbeatConsentRequired: codex_app_heartbeat=ask
  QuotaVisible --> HeartbeatPreauthorized: codex_app_heartbeat=yes
  QuotaVisible --> ManualLoopOnly: codex_app_heartbeat=no or unsupported host
  HeartbeatConsentRequired --> HeartbeatEnabled: user confirms + host installed
  HeartbeatPreauthorized --> HeartbeatEnabled: host installed
  HeartbeatEnabled --> FirstTickVerified: heartbeat fires + quota checked
  ManualLoopOnly --> FirstTickVerified: manual/TUI/Claude tick checked quota
  RepairNeeded --> ProjectRegistered: repair validated
  FirstTickVerified --> [*]
StateSource Fields / CommandsProduct Meaning
ProjectRegisteredRegistry goal, adapter kind/status, active state pathLoopX knows the project. Automation is not implied.
GlobalSyncPending / GlobalRegisteredglobal_sync payloadShared status/quota can discover the goal.
GlobalWriteBlocked / RepairNeededRegistry writability probe or sync errorProduce a concrete repair/gate; do not silently downgrade.
QuotaVisiblequota should-run can resolve goal and agentThe scheduler can reason about the target.
HeartbeatConsentRequiredcodex_app_heartbeat=askAsk before installing a recurring Codex App automation.
HeartbeatPreauthorizedcodex_app_heartbeat=yesInstall/update the host loop before claiming automation is active.
ManualLoopOnlycodex_app_heartbeat=no or host unsupportedManual, TUI, Claude, or on-demand loops remain valid.
FirstTickVerifiedRun history or quota evidence from a real tickThe operating loop has actually been exercised.

For read-only project maps, adapter.status=planned permits only a dry-run preview until the read_only_map_opt_in operator gate approves it. Connected read-only states such as connected, connected-read-only, and read-only-map-ready can append a real read-only map.

9. Agent Vision / Replan Machine

Agent vision is compact executable routing state, not a scratchpad. Each agent may have a bounded vision packet that describes its current role direction, scope, acceptance summary, replan trigger, dreaming policy, and latest patch. The CLI/write API must enforce those budgets before quota or status consumes the projection.

Vision is per agent_id, including closeout checks. A material refresh-state emits vision_checkpoint_v0 for the current agent: patched, unchanged with reason, retired/superseded, missing required, or not required. Missing required checkpoints are preserved in compact run history, filtered by the current agent, and can become goal-frontier acceptance gaps before local quiet/wait decisions.

stateDiagram-v2
  [*] --> Unset
  Unset --> DraftVision: goal configured or preset seeded
  DraftVision --> ActiveVision: budget + acceptance validated
  ActiveVision --> VisionDriftDetected: frontier exhausted or objective shifted
  ActiveVision --> DreamProposal: advisory patch proposed
  VisionDriftDetected --> ReplanRequired: goal-level trigger accepted
  DreamProposal --> ReplanRequired: delivery route needed
  ReplanRequired --> ReplanDrafted: bounded plan + todo delta
  ReplanDrafted --> VisionPatchProposed: bounded vision patch
  VisionPatchProposed --> ActiveVision: write correctness validated
  ActiveVision --> Superseded: successor route replaces it
  ActiveVision --> Retired: acceptance or no-follow-up recorded
StateProduct MeaningLegal Exit
DraftVisionA compact packet is being seeded or rewritten.Validate budget and acceptance.
ActiveVisionThe role may use the packet for lane-local work.Evidence, drift, dreaming proposal, supersession, or retirement.
ReplanRequiredGoal-level progress requires replan before quiet/wait.Write a bounded vision/todo/acceptance delta.
VisionPatchProposedReplan produced a bounded patch.Apply through LoopX write APIs or reject as over budget.

The important ordering is goal-level first: required replan is evaluated before monitor quiet skip, scoped gate wait, or an individual agent's no-candidate state. Those local states may remain visible, but they cannot clear a required replan. An acknowledgement without a vision, todo, acceptance, or no-follow-up delta is replan_noop. A future monitor next_due_at is scheduler metadata, not a frontier delta, and cannot by itself suppress a monitor-only empty-frontier replan.

The same ordering applies when an agent records a bounded replan_trigger_summary in its vision packet. Status/quota exposes that trigger as a goal-frontier acceptance_gaps[] entry. If no advancement frontier remains, the gap becomes a replan trigger before the lane can quietly back off.

Long runnable lanes also pass through this machine. When the current agent owns 15 open advancement todos, or 20 claimed open todos with claimed advancement work still present, quota should trigger a bounded vision replan before continuing linearly. The replan reads the agent-scoped evidence log, uses bounded public research when local evidence is insufficient for a public claim, then groups, prunes, or reprioritizes the chain into the next high-value runnable slice. Shared unclaimed candidates remain selectable but do not count toward this lane threshold. A valid evidence-linked vision path can retain existing runnable work and settle the projected Turn without adding another planning Todo. Shared-pool churn preserves its obligation identity; owned material changes rearm it.

The same ordering also applies to vision_checkpoint_v0: if a role records material progress but omits both a vision patch and an unchanged/no-follow-up decision, quota should project that role's vision_checkpoint_missing gap and route that role back through replan.

See goal_vision_replan_contract_v0 for the field budgets and projection contract.

Catalog Linkage

MachineRepresentative Patterns
Todo lifecycleIP-001 Bounded Delivery, IP-029 Handoff Todo Gate State
Quota / runtimeIP-001, IP-007 Outcome Floor Recovery, IP-008 Monitor Quiet Skip
Gate decision scopeIP-002 Blocked Priority With Safe Fallback, IP-003 Scoped Gate With Safe Fallback, IP-004 Concrete User Todo Projection
Owner route / handoffIP-026 Agent-Scoped No-Candidate Gap, IP-029 Handoff Todo Gate State
Evidence / rolloutIP-001 Bounded Delivery, IP-007 Outcome Floor Recovery
Scheduler / heartbeatIP-008 Monitor Quiet Skip, IP-026 Agent-Scoped No-Candidate Gap
Projection sinkIP-005 State Projection Gap
Agent onboardingProject bootstrap/connect and read-only-map opt-in flows
Agent vision / replanAutonomous replan, dreaming proposal promotion, successor replan

If a new interaction pattern cannot be placed in one of these machines, first check whether it is a UI variant, wording variant, or private incident label. Only add a new machine when a source field, legal transition, owner, and validation path are all observable from public-safe LoopX state.