aion-server 0.31.0

Aion workflow server library: HTTP, gRPC, WebSocket, and worker endpoints. Run it with the `aion` binary from the aion-cli crate.
Documentation
//! What auto-provision did about one task queue, and why.
//!
//! Every arm is reported, including the ones where nothing was minted. A queue
//! that quietly got no worker is indistinguishable from a queue nobody asked
//! about, and the difference is the whole operator question: "I deployed a
//! document with a `harness` section and nothing is serving it."

use serde::{Deserialize, Serialize};

/// What was decided about one task queue.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AutoWorkerDecision {
    /// No record served this queue, so one was minted and converged.
    Minted,
    /// The queue's own auto record existed and named a DIFFERENT document, so
    /// it was replaced and its worker restarted onto the new one.
    Reminted,
    /// The queue's own auto record already names this exact document. Nothing
    /// was written; the running worker was left alone and merely converged.
    Unchanged,
    /// The record was written durably, and this server could NOT start it. The
    /// record stands and the next boot's reconcile honours it; `detail` carries
    /// the supervision failure and its remedy.
    ///
    /// Distinct from every arm above because those all claim a running worker,
    /// and claiming one that does not exist is the exact defect this whole lane
    /// exists to end — one layer up.
    RecordedNotRunning,
    /// The queue's own auto record was deliberately STOPPED by an operator, and
    /// their intent was preserved rather than reversed.
    OperatorStopped,
    /// The document no longer declares this queue, so the record this server
    /// minted for it was withdrawn and its worker stopped.
    Retired,
    /// A record the OPERATOR authored already serves this queue. Their record
    /// wins and nothing here touched or duplicated it.
    OperatorRecord,
    /// This server binds no liminal worker listener, so a minted record would
    /// dial nothing. Refused, with the `[outbox]` remedy in `detail`.
    DarkOutbox,
    /// The document's harness names an agent THIS host cannot launch — an
    /// absent or non-executable agent binary, or a literal working directory
    /// that does not exist here. Refused with the missing path named; the
    /// deploy itself landed. Measured 2026-08-26 on the estate: a fleet
    /// document deployed from a Linux box minted workers on the Mac server
    /// whose harness named `/home/aion/...` paths, and every dispatch routed
    /// to them burned an attempt on a spawn that could only fail.
    UnrunnableHarness,
    /// Auto-provision could not be carried out. `detail` says what refused.
    Failed,
}

impl AutoWorkerDecision {
    /// Stable token for logs and metrics.
    #[must_use]
    pub const fn token(self) -> &'static str {
        match self {
            Self::Minted => "minted",
            Self::Reminted => "reminted",
            Self::Unchanged => "unchanged",
            Self::RecordedNotRunning => "recorded-not-running",
            Self::OperatorStopped => "operator-stopped",
            Self::Retired => "retired",
            Self::OperatorRecord => "operator-record",
            Self::DarkOutbox => "dark-outbox",
            Self::UnrunnableHarness => "unrunnable-harness",
            Self::Failed => "failed",
        }
    }

    /// Whether this outcome left the queue WITHOUT the built-in agent worker
    /// the document asked for by declaring a `harness` section.
    ///
    /// Three arms are deliberately NOT refusals, because in each of them a
    /// person already decided: [`Self::OperatorRecord`] (their record serves
    /// the queue), [`Self::OperatorStopped`] (they stopped it on purpose), and
    /// [`Self::Retired`] (the document stopped declaring the queue).
    #[must_use]
    pub const fn is_refusal(self) -> bool {
        matches!(
            self,
            Self::DarkOutbox | Self::Failed | Self::RecordedNotRunning | Self::UnrunnableHarness
        )
    }

    /// Whether this outcome CLAIMS a running worker.
    ///
    /// The claim is only ever made where a convergence returned success. It is
    /// separated from the decision because a record can be written correctly
    /// and still not run, and reporting those two as one thing is how a status
    /// surface comes to say a worker started when no process exists.
    #[must_use]
    pub const fn claims_running(self) -> bool {
        matches!(self, Self::Minted | Self::Reminted | Self::Unchanged)
    }
}

/// One task queue's auto-provision outcome, as reported to the deploy caller,
/// the boot log, and the managed-worker status surface.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct AutoWorkerOutcome {
    /// The task queue this is about.
    pub task_queue: String,
    /// The workflow type whose document declared the queue.
    ///
    /// Carried so a document-level failure — one where no queue name could be
    /// read out of the source at all — is still attributable to a document.
    /// Without it two unparseable documents are one indistinguishable entry.
    pub workflow_type: String,
    /// What was decided.
    pub decision: AutoWorkerDecision,
    /// The deployment record's name, when there is one to name — the auto
    /// record on the minted arms, the operator's own on [`AutoWorkerDecision::OperatorRecord`].
    pub deployment: Option<String>,
    /// The sentence an operator reads. ALWAYS present, on every arm: a
    /// decision token with no account of itself is a decision nobody can act on.
    pub detail: String,
}

impl AutoWorkerOutcome {
    /// One outcome for a queue, with its account.
    pub fn new(
        task_queue: impl Into<String>,
        workflow_type: impl Into<String>,
        decision: AutoWorkerDecision,
        deployment: Option<String>,
        detail: impl Into<String>,
    ) -> Self {
        Self {
            task_queue: task_queue.into(),
            workflow_type: workflow_type.into(),
            decision,
            deployment,
            detail: detail.into(),
        }
    }

    /// A refusal that could not be attributed to any one queue — the document
    /// itself did not parse, so no queue name was ever read out of it.
    pub fn document_level(workflow_type: impl Into<String>, detail: impl Into<String>) -> Self {
        Self::new(
            String::new(),
            workflow_type,
            AutoWorkerDecision::Failed,
            None,
            detail,
        )
    }

    /// The key this outcome is kept under in the supervisor's per-queue log.
    ///
    /// The task queue, because the operator's question is about a queue and one
    /// queue has one auto record however many documents declare it. A
    /// document-level failure has no queue, so it is keyed by the document
    /// instead — otherwise two unparseable documents overwrite each other under
    /// one blank name.
    #[must_use]
    pub fn log_key(&self) -> String {
        if self.task_queue.is_empty() {
            format!("document:{}", self.workflow_type)
        } else {
            self.task_queue.clone()
        }
    }
}