State Machine & Recovery
OpenFlows is a state-driven system. The SharedStore is the single source of truth, and every agent action is a
state transition. NEXUS is the only component that is allowed to reconcile the global state: on every poll
cycle it runs reconcile(), compares the SharedStore against the real world (GitHub, Coder,
worker heartbeats), and fixes any inconsistencies it finds.
This page explains that reconciliation engine in detail: the SharedStore state machine, the anomalies NEXUS
detects, the retry semantics for each failure mode, and the AwaitingHuman escalation path that
prevents infinite loops when automatic recovery is impossible.
SharedStore as the State Machine
The SharedStore is not just a cache. It is the canonical state machine of the OpenFlows pipeline. Every ticket, every worker, every pending PR, and every recovery action is stored in typed Redis keys. The in-memory backend used during development is a behavior-preserving mirror of the Redis implementation, so tests and production share the same contract semantics.
| Key | Type | Purpose |
|---|---|---|
tickets | Map<TicketId, TicketStatus> | Canonical source of truth for every GitHub issue that OpenFlows has loaded. Tracks state from open through merged and terminal states. |
worker_slots | Map<WorkerId, WorkerStatus> | One entry per configured worker. Records idle, assigned, working, suspended, or done status plus workspace and chat bindings. |
pending_prs | Vec<PrEntry> | Pull requests that have been opened but not yet merged. VESSEL polls these entries until they are merged or closed. |
flow_recovery | Map<TicketId, RecoveryRecord> | Detected inconsistencies, retry counts, and escalation reasons that NEXUS must resolve. |
event_ring | RingBuffer (1000) | Last 1000 lifecycle events. Powers the TUI, audit correlation, and post-mortem analysis. |
These keys are the contract surface between agents. An agent never talks to another agent directly; it reads
and writes these keys. NEXUS is the coordinator because it is the only role that reads all of them. FORGE only
writes to its own ticket and reads the SENTINEL review state for that ticket. SENTINEL only writes review
files. VESSEL only reads pending_prs and writes ticket_merged events. LORE only listens
for merge events.
Ticket State Machine
A ticket is the OpenFlows representation of a GitHub issue. It moves through a well-defined state machine,
and each state tells NEXUS what to do next. The terminal states are merged,
awaiting_human, and closed. Every other state is recoverable.
| State | Meaning |
|---|---|
open | Issue discovered by NEXUS GitHub poll but not yet assigned. |
assigned | Ticket bound to a worker slot; workspace provisioning in progress or complete. |
in_progress | Worker has written PLAN.md and is actively implementing segments. |
review | Segment or final review is in progress; SENTINEL workspace is active. |
pr_opened | PR created and recorded in pending_prs; VESSEL monitoring. |
completed_without_pr | Worker reports done but no PR exists; NEXUS must reconcile. |
merged | VESSEL merged the PR; LORE documentation may still be pending. |
awaiting_human | Escalation terminal state; worker suspended and workspace torn down. |
closed | Issue was closed or ticket abandoned; no further action. |
State is the interface
in_progress. SENTINEL's APPROVED moves it to pr_opened. VESSEL's merge moves it to
merged. If an agent crashes, NEXUS reads the state and knows exactly which agent should take
over next.
What NEXUS reconcile() Actually Does
reconcile() is the heartbeat of the system. It runs on every NEXUS poll cycle, which defaults to
10 seconds. The cycle is deliberately simple: fetch state, compare state to reality, emit recovery actions,
advance tickets, and sleep. The simplicity makes it easy to reason about and easy to debug from logs.
The reconcile loop follows these steps in order:
- Load global state - read all SharedStore keys into a consistent snapshot.
- Sync GitHub issues - discover new, closed, or renamed issues; update
tickets. - Inspect worker slots - compare each slot's
workingstate against the Coder API to detect stale workspaces. - Inspect pending PRs - query GitHub for merge status, conflicts, and CI results.
- Detect anomalies - classify each inconsistency as one of the known anomaly types.
- Emit recovery actions - write
flow_recoveryrecords and dispatch follow-up actions. - Assign idle workers - match ready tickets to idle workers and provision workspaces.
- Write event_ring - record the cycle's actions for observability.
// Pseudocode of the reconcile loop
async fn reconcile(&self) -> Result<()> {
let state = self.shared_store.snapshot().await?;
let github_issues = self.github.poll_open_issues().await?;
let coder_workspaces = self.coder.list_workspaces().await?;
for ticket in state.tickets.values() {
match ticket.status {
TicketStatus::Open => self.try_assign(ticket, &state).await?,
TicketStatus::Assigned | TicketStatus::InProgress => {
self.check_worker_health(ticket, &state, &coder_workspaces).await?;
}
TicketStatus::PrOpened => self.check_pr_status(ticket, &state).await?,
TicketStatus::CompletedWithoutPr => self.recover_pr_creation(ticket).await?,
TicketStatus::AwaitingHuman => self.check_human_resolution(ticket).await?,
_ => {}
}
}
self.event_ring.push(Event::ReconcileComplete).await?;
Ok(())
}Anomaly Detection
NEXUS recognizes a closed set of anomalies. Each anomaly maps to a recovery action, and the recovery action is idempotent. If an anomaly persists across multiple cycles, NEXUS increments a retry counter and eventually escalates. This prevents the system from flapping on transient inconsistencies.
| Anomaly | Detection rule |
|---|---|
| Orphaned ticket | Ticket is assigned or in_progress but its worker slot is idle or missing. |
| Stale worker | Worker slot is working but the bound Coder workspace is stopped or deleted. |
| Unmerged PR | Entry in pending_prs references a PR that is still open after a configurable timeout. |
| completed_without_pr | Ticket status is completed but there is no matching entry in pending_prs. |
| Dangling SENTINEL workspace | A SENTINEL workspace is running but no active review state exists in the parent ticket. |
| Lost chat | A worker slot references a Coder chat that no longer exists. |
Orphaned Tickets
An orphaned ticket is a ticket that is supposed to be in progress but has no healthy worker attached. This happens when a workspace crashes, a worker process panics, or a Coder auto-stop removes the workspace before NEXUS can update the slot. The ticket is "orphaned" because its state machine says it is assigned, but the worker slot is gone.
NEXUS handles orphans by checking whether the ticket is recoverable. If the ticket has a recent commit on its
branch and a clear next state, NEXUS provisions a new workspace and re-assigns the ticket to the same or a
different worker slot. If the ticket state is ambiguous, NEXUS writes the reason to flow_recovery
and may escalate to AwaitingHuman.
Stale Workers
A stale worker is the opposite of an orphaned ticket: the worker slot is still marked working, but
the Coder workspace it references is stopped or deleted. Stale workers are dangerous because they consume a
worker slot without doing work. If every slot becomes stale, the pipeline stops assigning new tickets.
NEXUS resolves stale workers by first querying the Coder API for the workspace status. If the workspace is stopped, NEXUS can restart it and resume the ticket. If the workspace is deleted, NEXUS clears the worker slot and either reassigns the ticket or escalates it. The stale-worker check is also the mechanism that enforces workspace TTL: a stopped workspace after TTL expiry is treated as stale.
Unmerged PRs
A PR in pending_prs that remains open after a timeout is classified as unmerged. Timeouts can be
caused by long-running CI, failing checks, or a GitHub merge conflict that VESSEL has not yet resolved. NEXUS
delegates unmerged PRs to VESSEL, which polls the PR, checks mergeable, and either merges it,
resolves the conflict, or routes the ticket back to FORGE for rework.
The unmerged-PR check is the bridge between the SharedStore and GitHub reality. If a PR is merged by a human
while OpenFlows is offline, NEXUS detects the merged state on the next reconcile and moves the ticket to
merged without requiring a new VESSEL action.
completed_without_pr
completed_without_pr is a specific state that means the worker believes the ticket is done but no
PR has been recorded. This can happen when a FORGE workspace is destroyed right after the final commit but
before the GitHub MCP PR creation call completes. It can also happen when a human closes the issue while the
ticket is in progress.
NEXUS resolves this by looking at the branch. If the branch has commits that are not on the base branch, NEXUS
instructs FORGE to open the PR. If the branch is empty, NEXUS checks whether the issue is still relevant and
either closes the ticket or escalates. The completed_without_pr state is the safety net that
prevents "done but not delivered" tickets from silently disappearing.
completed_without_pr is not a success state
completed_without_pr has not been merged. It is a reconcile flag, not a terminal
state. NEXUS will not stop working on it until it is either merged, closed, or escalated to human.
Retry Semantics
OpenFlows has three separate retry layers, each with its own budget and purpose. They are designed to avoid both premature escalation and infinite loops. A failure that is not transient exhausts its budget and then escalates.
| Layer | Semantics |
|---|---|
| Node retry | max_retries=3 at the PocketFlow layer. Resets the same node and re-runs prep/exec/post. |
| Decision retry | DECISION_RETRY_LIMIT=2 at the agent decision layer. Sends a clarified prompt to the LLM. |
| Reconcile retry | Exponential backoff up to 5 attempts for transient Coder or GitHub API failures. |
| Escalation | After all retry budgets are exhausted, NEXUS marks the ticket awaiting_human and suspends the worker. |
The retry counters are stored in the flow_recovery key. NEXUS increments them atomically and
reads them back on the next cycle to decide whether to retry again or escalate. Because the SharedStore is the
source of truth, a NEXUS crash in the middle of a retry does not lose the retry count; the next NEXUS process
resumes from the same counter value.
Flow Recovery Record
The flow_recovery key holds a record per ticket. It is the scratchpad for NEXUS's healing
decisions. The record includes the anomaly type, the retry count, the last action taken, the expected next
state, and a human-readable reason. Operators can inspect these records in the TUI to understand why a ticket
is stuck.
{
"T-001": {
"anomaly": "StaleWorker",
"retry_count": 1,
"max_retries": 3,
"last_action": "workspace_restart",
"expected_next_state": "InProgress",
"reason": "Coder workspace was auto-stopped after TTL; restarting to resume segment 3",
"escalate_at": "2026-07-20T17:00:00Z"
}
}AwaitingHuman Escalation
Not every failure is recoverable. When a ticket has exhausted all retry budgets, when the specification is
ambiguous, or when a security concern is raised, NEXUS moves the ticket to awaiting_human. This is
a terminal state for automation: no further agent actions are taken until a human operator intervenes.
The escalation is not silent. NEXUS writes a detailed reason to the SharedStore, sends a notification if
configured, and marks the worker slot as suspended. The worker's workspace is torn down, because
a suspended worker should not hold resources. When the human resolves the blocker, they update the ticket
state in the TUI or by pushing a state update, and NEXUS resumes the flow from the appropriate phase.
The AwaitingHuman path is the final safety valve. It prevents OpenFlows from making speculative
decisions about ambiguous requirements, security exceptions, or external dependencies. The design philosophy is
clear: recover automatically when possible, stop cleanly when it is not, and always tell the human why.
Recoverable vs. human-escalated
Observability and Operations
The reconcile loop is fully observable. Every cycle writes to event_ring, and every recovery action
updates flow_recovery. The TUI displays the current state of every ticket, the health of every
worker slot, and the count of pending anomalies. Operators can drill into a ticket to see the full history of
state transitions, retry counts, and escalation reasons.
For long-running deployments, the reconcile metrics are also exposed for external monitoring. The most
important metrics are: tickets assigned per cycle, reconcile duration, anomaly count, retry count per anomaly
type, and tickets in awaiting_human. A sudden spike in anomalies is usually the first sign of an
upstream dependency issue (Coder, GitHub, or the LLM provider).