Skip to main content

aion_server/
run.rs

1//! Run loop for the Aion workflow server: tracing initialization,
2//! configuration load, transport startup, and signal-driven graceful
3//! shutdown.
4//!
5//! This is the library entry point behind the `aion server` command. It
6//! preserves the operational contract of the former standalone
7//! `aion-server` binary: exit code 2 for configuration errors, the drain
8//! outcome's exit code on shutdown, and 130 when a second termination
9//! signal forces immediate exit.
10
11use std::{net::SocketAddr, process::ExitCode};
12
13use tokio::net::TcpListener;
14use tonic::transport::Server as TonicServer;
15use tracing::{error, info, warn};
16
17use std::sync::Arc;
18
19use crate::{
20    ServerConfig, ServerError, ServerState, api,
21    config::{CliOverrides, NamespaceMode, OutboxConfig, OutboxTransport, StoreBackend},
22    observability,
23    shutdown::{self, ShutdownOutcome},
24    worker::{
25        ActivityDispatcher, DeliveryGate, OutboxDeliveryCallback, OutboxDispatcher,
26        OutboxDispatcherConfig, OutboxReconciler, OutboxReconcilerConfig, OutboxRowDispatch,
27        ServerOutboxDeliveryCallback, WorkerOutboxDispatch,
28    },
29};
30
31/// Short TTL for the dispatcher's per-namespace placement cache (Control-Plane
32/// Phase 2, P2-P3). Kept small so an operator's `PUT /namespaces/{name}/placement`
33/// takes effect on the hot claim loop within a couple of seconds, while still
34/// collapsing a per-sweep quorum `get_namespace` into a cheap in-process lookup.
35/// A stale entry under `Prefer` only mis-prefers a worker for at most one window
36/// and self-corrects — it never affects correctness or replay.
37const PLACEMENT_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(2);
38
39/// Short TTL for the dispatcher's per-namespace quota cache (Control-Plane Phase 2,
40/// P2-Q2). Kept small so an operator raising/lowering a tenant's
41/// `max_in_flight_activities` takes effect on the hot claim loop within a couple of
42/// seconds, while still collapsing a per-sweep quorum `get_namespace` into a cheap
43/// in-process lookup. A stale entry only over- or under-admits slightly for one
44/// window and self-corrects — backpressure never drops a row, so it cannot affect
45/// correctness or replay.
46const QUOTA_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(2);
47
48/// Cadence of the ops-console quota-state broadcaster (Control-Plane Phase 2,
49/// P2-Q3). Each tick samples every registry namespace's durable Claimed-row count
50/// and cluster-wide ceiling, then pushes one `NamespaceQuotaState` per namespace
51/// onto the cluster channel, so the console badge tracks live load. Kept at 1s:
52/// brisk enough that the badge visibly ticks as work flows, throttled enough that
53/// it is never a per-row firehose (in-flight changes on every claim/settle). It is
54/// a server-side push on a timer, NOT a client poll — the dashboard rule bans the
55/// latter, not a throttled server snapshot of REAL durable state.
56const QUOTA_BROADCAST_CADENCE: std::time::Duration = std::time::Duration::from_secs(1);
57
58/// Resolved keyed-backpressure inputs for the outbox dispatcher (Control-Plane
59/// Phase 2, P2-Q2): the generous platform-default ceiling and this node's
60/// owned-shard fraction of the cluster shard space.
61#[derive(Clone, Copy, Debug)]
62struct BackpressureSettings {
63    /// The `[namespaces] max_in_flight_activities` platform default, applied to any
64    /// namespace carrying no explicit per-tenant override.
65    platform_default: u32,
66    /// This node's owned-shard fraction of the cluster's virtual shard space,
67    /// derived from `[store] owned_shards` and `[store] shard_count`.
68    fraction: crate::worker::OwnedShardFraction,
69}
70
71impl BackpressureSettings {
72    /// Derive the backpressure inputs from the merged server config.
73    ///
74    /// An empty `[store] owned_shards` means own-all (the single-node default), so
75    /// the fraction is 1 and per-node ceilings equal the cluster-wide quota. A
76    /// declared owned set enforces the proportional per-node slice
77    /// `|owned| / shard_count` (CP-Phase-2 §3.6).
78    fn from_config(config: &ServerConfig) -> Self {
79        let total = u32::try_from(config.store.shard_count).unwrap_or(u32::MAX);
80        let fraction = if config.store.owned_shards.is_empty() {
81            crate::worker::OwnedShardFraction::own_all()
82        } else {
83            let owned = u32::try_from(config.store.owned_shards.len()).unwrap_or(u32::MAX);
84            crate::worker::OwnedShardFraction::new(owned, total)
85        };
86        Self {
87            platform_default: config.namespaces.max_in_flight_activities,
88            fraction,
89        }
90    }
91}
92
93/// Owns the liminal worker listener for the server's lifetime when the outbox is
94/// commissioned over the liminal transport.
95///
96/// The aion-server HOSTS the liminal listener that remote workers connect IN to;
97/// its inner [`ServerListener`](liminal_server::server::listener::ServerListener)
98/// owns the accept worker. Held as a local in [`run_server`] across the whole
99/// serve `select!`, so it is dropped exactly at server shutdown — and the
100/// listener's own `Drop` stops the accept worker cleanly (no leaked thread, no
101/// orphaned listener). Every non-liminal boot (the default) carries the `None`
102/// guard, which holds nothing and drops to a no-op, so behaviour is unchanged.
103#[derive(Debug, Default)]
104struct OutboxWorkerListener {
105    /// Held purely for its `Drop` side-effect (stopping the accept worker on
106    /// server shutdown); never read after construction, hence the leading
107    /// underscore.
108    #[cfg(feature = "liminal-transport")]
109    _inner: Option<liminal_server::server::listener::ServerListener>,
110}
111
112/// Run the Aion workflow server until it shuts down, returning the process
113/// exit code.
114///
115/// Initializes the JSON tracing subscriber, loads and validates the merged
116/// configuration (file, environment, then `overrides`), serves the gRPC and
117/// HTTP transports, and drains gracefully after the first termination
118/// signal. Every failure is logged through tracing and mapped to the exit
119/// code contract above; the caller only has to exit with the returned code.
120pub async fn run(overrides: CliOverrides) -> ExitCode {
121    match run_server(overrides).await {
122        Ok(code) => code,
123        Err(error) => {
124            error!(%error, "aion-server failed");
125            if error.is_config() {
126                ExitCode::from(2)
127            } else {
128                ExitCode::FAILURE
129            }
130        }
131    }
132}
133
134/// The where-to-edit half of the missing `outbox.liminal_listen_address`
135/// refusal: a liminal outbox refusal must name the FILE to edit, not just the
136/// key — the operator reading it is exactly the operator who did not write
137/// the config (a scaffolded or setup-script home).
138fn liminal_address_hint(source: &crate::config::ConfigSource) -> String {
139    match source {
140        crate::config::ConfigSource::BuiltInDefaults => {
141            "set AION_OUTBOX_LIMINAL_LISTEN_ADDRESS, or add `liminal_listen_address = \
142             \"127.0.0.1:50061\"` to `[outbox]` in a config file"
143                .to_owned()
144        }
145        source => format!(
146            "add `liminal_listen_address = \"127.0.0.1:50061\"` to `[outbox]` in the {source}"
147        ),
148    }
149}
150
151async fn run_server(cli: CliOverrides) -> Result<ExitCode, ServerError> {
152    observability::tracing::init()?;
153
154    // #180: a boot that discovers no config anywhere first scaffolds
155    // `<AION_HOME>/config.toml` from the embedded template (claim-only-when-
156    // empty), then loads it — config LOAD itself stays pure and read-only.
157    let loaded = crate::config::load_or_scaffold(&cli)?;
158    loaded.resolution.ensure_private_home()?;
159    // Arm the death note as early as the home exists, so every later failure
160    // path — including config validation and state build — runs inside the
161    // ARMED/DISARMED bracket. Two anonymous server deaths on 2026-08-16 are
162    // why this exists; see the module docs for the exact coverage.
163    let death_note = crate::death_note::DeathNote::arm(&loaded.resolution.home)?;
164    loaded.resolution.log_startup();
165    let liminal_address_hint = liminal_address_hint(&loaded.resolution.source);
166    let config = loaded.config;
167    reject_auth_without_feature(&config)?;
168    let store_backend = config.store.backend;
169    // Static shard assignment (SS-1): read the operator's pinned shard set from
170    // `[store] owned_shards`. Empty means own ALL shards (single-node default).
171    // The set is carried into `RuntimeConfig` by `into_parts` and applied to the
172    // `EngineBuilder` during state construction; surface it here so the boot
173    // banner records which shards this node serves. No election is performed.
174    let owned_shards = config.store.owned_shards.clone();
175    // Capture the outbox settings before `build` consumes `config`, so the
176    // (default-off) outbox dispatcher can be wired after state is up. The
177    // dispatcher shares the engine's already-opened haematite store via
178    // `state.outbox_store()`, so no store settings are needed.
179    let outbox_config = config.outbox.clone();
180    // Control-Plane Phase 2 (P2-Q2): capture the keyed-backpressure inputs — the
181    // generous platform-default ceiling and this node's owned-shard fraction —
182    // before `build` consumes `config`. On a single-node / own-all boot the fraction
183    // is 1, so per-node ceilings equal the cluster-wide quota and, with the generous
184    // default and no tenant override, the ceiling never engages (byte-identical claim).
185    let backpressure_settings = BackpressureSettings::from_config(&config);
186    // Capture the SS-5b failover supervisor knobs before `build` consumes config.
187    // Only a distributed haematite boot carries a `[store.cluster]` section; this
188    // is `None` for every single-node boot, so no supervisor is ever spawned.
189    let cluster_config = config.store.cluster.clone();
190    // Capture the managed-worker supervision policy before `build` consumes
191    // `config`. Resolution already happened during config validation, so this
192    // cannot surprise an operator at boot; it is re-read here because the
193    // policy is COMMISSIONED onto the supervisor built into state below, and a
194    // server without the section supervises nothing.
195    let supervision_policy = config.worker_supervision.resolve()?;
196    let state = ServerState::build(config).await?;
197    reject_tls_until_supported(&state)?;
198
199    let runtime = state.runtime_config();
200    let grpc_address = runtime.listen.grpc;
201    let http_address = runtime.listen.http;
202    let workflow_packages: Vec<String> = runtime
203        .workflow_packages
204        .iter()
205        .map(|path| path.display().to_string())
206        .collect();
207    // The revision, not just the version. A crate version cannot distinguish
208    // two builds from different commits of the same version, and that is the
209    // distinction an operator needs when deciding whether a restart restores
210    // what was running or substitutes something else (#123). The endpoint
211    // answers this too, but a crashed server leaves only its log.
212    let build = crate::build_identity::BuildIdentity::current();
213    // #139: the server-resolved workspace root (the aion home's `clones/`
214    // directory) that declared bodies expand `{workspace_root}` with. Reported
215    // here so composition points (setup.sh today, the workspace verb later)
216    // READ the value from the server that will use it instead of re-deriving
217    // it. An unresolvable root is reported as exactly that — never fabricated;
218    // a placeholder-bearing dispatch will refuse terminally with this reason.
219    // The rendering itself is `WorkspaceRoot::banner_value`, pinned by its own
220    // two-case test, so the banner and the tests cannot drift apart.
221    let workspace_root = state.workspace_root().banner_value();
222    info!(
223        version = env!("CARGO_PKG_VERSION"),
224        build = %build.line(),
225        commit = build.commit,
226        grpc_address = %grpc_address,
227        http_address = %http_address,
228        default_namespace = %runtime.default_namespace,
229        namespace_mode = namespace_mode_label(&runtime.namespace.mode),
230        store_backend = store_backend_label(store_backend),
231        auth_enabled = runtime.auth.enabled,
232        deploy_enabled = runtime.deploy.enabled,
233        metrics_enabled = runtime.metrics.enabled,
234        workspace_root = %workspace_root,
235        death_note = %death_note.path().display(),
236        workflow_package_count = workflow_packages.len(),
237        workflow_packages = ?workflow_packages,
238        owned_shards = ?owned_shards,
239        owns_all_shards = owned_shards.is_empty(),
240        "aion-server startup banner"
241    );
242    // #139 leg C: the assistant ships IN aion. The embedded document is
243    // installed here — after the engine has reloaded every persisted package,
244    // so the install can see what is already resident, and before the
245    // transports accept traffic, so the first caller finds it. It claims only a
246    // catalog holding no version of the assistant type; anything else is the
247    // operator's cut to make, and the outcome says so in the log either way.
248    crate::assistant::install_embedded_assistant_for_server(&state, &liminal_address_hint).await;
249    // #189 slice one: the built-in update check ships the same way, under the
250    // same only-the-empty-case install rule. Installing makes it STARTABLE
251    // and nothing else — no check runs without an explicit operator act.
252    crate::update_check::install_embedded_update_check_for_server(&state).await;
253    let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
254    // LSUB-4-1: a distributed haematite boot carries a `[store.cluster]` section.
255    // The single outbox dispatcher task is spawned in BOTH modes; the difference
256    // is only how ownership is enforced. Single-node (`None`) owns all shards by
257    // construction (`owned_shard_scope() == None`), so its claim sweeps see every
258    // row. Clustered (`Some`) relies on `claim_outbox_rows`' `owned_shard_scope()`
259    // filter — already seeded by `set_owned_shards` during `ServerState::build`,
260    // which runs before this point — so each node only ever claims rows on the
261    // shards it owns. Compute the flag here where the cluster section is in
262    // scope; pass it to the gate so the boot banner records the mode.
263    let outbox_clustered = cluster_config.is_some();
264    // Dormant by default: only when `outbox.enabled` is set does the
265    // non-replayed outbox dispatcher task start. With the flag off (the
266    // default) nothing here runs and server behaviour is unchanged.
267    // Hold the liminal worker listener (if any) for the server's lifetime: it is
268    // dropped at the end of `run_server`, after the serve `select!` completes, so
269    // its accept worker stops cleanly on shutdown via the listener's own `Drop`.
270    // #204/#253: rebuild the pause dispatch-hold and settle terminal
271    // workflows' stranded outbox rows BEFORE the dispatcher's first claim.
272    rebuild_outbox_boot_state(&state, &outbox_config).await;
273    let _outbox_worker_listener = maybe_spawn_outbox_dispatcher(
274        &state,
275        &outbox_config,
276        outbox_clustered,
277        backpressure_settings,
278        &shutdown_rx,
279        &liminal_address_hint,
280    )?;
281    // SS-5b: a distributed boot whose peers declare owned shards runs the cluster
282    // supervisor — automatic failover detection. A single-node boot spawns
283    // nothing here (the method returns `false`), so default behaviour is
284    // unchanged.
285    maybe_spawn_cluster_supervisor(&state, cluster_config.as_ref(), &shutdown_rx)?;
286    // #176: the worker heartbeat expiry sweeper is ALWAYS commissioned —
287    // dead-worker detection is a liveness correctness property, not an opt-in
288    // feature. It is the production caller of `fail_expired_workers`: a worker
289    // whose stream stays open while its process wedges (stops heartbeating
290    // without disconnecting) is expired, deregistered with the provable Timeout
291    // reason, and its in-flight tasks surface as TRANSPORT losses, re-dispatched
292    // attempt-neutrally rather than charged to the action's retry budget.
293    // Cadence derives from `worker.heartbeat_window` (quarter-window, clamped to
294    // [1s, window]; the default 30s window sweeps every 7.5s) — deliberately no
295    // separate config knob. It drains on the same shutdown watch as the
296    // transports; dropping the JoinHandle only detaches the task.
297    drop(state.spawn_heartbeat_sweeper(shutdown_rx.clone()));
298    commission_worker_supervision(&state, supervision_policy).await;
299    // Instant doors: the startup catch-up legs (owed timer fires, schedule
300    // catch-up) run as a background task CONCURRENT with the transports —
301    // the backlog has no upper bound, and a boot that blocks on it keeps the
302    // doors shut for the whole sweep (the 2026-08-24 estate outage shape:
303    // 37+ minutes of healthy catch-up with every listener refusing).
304    // Workflow-residency recovery already ran inside `ServerState::build`,
305    // so every surface the transports serve answers correctly while the
306    // catch-up drains behind them.
307    drop(state.spawn_startup_catchup(shutdown_rx.clone())?);
308    let mut grpc = tokio::spawn(serve_grpc(state.clone(), grpc_address, shutdown_rx.clone()));
309    let mut http = tokio::spawn(serve_http(state.clone(), http_address, shutdown_rx));
310
311    let outcome = tokio::select! {
312        result = &mut grpc => {
313            transport_result("gRPC", result)?;
314            state.shutdown()?;
315            ShutdownOutcome::Clean
316        },
317        result = &mut http => {
318            transport_result("HTTP", result)?;
319            state.shutdown()?;
320            ShutdownOutcome::Clean
321        },
322        result = shutdown_signal() => {
323            result?;
324            let _receiver_count = shutdown_tx.send(true);
325            let outcome = shutdown::drain_after_first_signal(state.clone(), async {
326                let _ = shutdown_signal().await;
327            }).await?;
328            if !matches!(outcome, ShutdownOutcome::Forced) {
329                transport_result("gRPC", grpc.await)?;
330                transport_result("HTTP", http.await)?;
331            }
332            outcome
333        },
334    };
335
336    let exit_code = outcome.exit_code();
337    death_note.disarm(&format!(
338        "clean run-loop exit: shutdown outcome {outcome:?}"
339    ));
340    Ok(exit_code)
341}
342
343/// Install the operator's supervision policy and converge the fleet.
344///
345/// Uncommissioned is a first-class but never SILENT state: a server with no
346/// `[worker_supervision]` section supervises nothing, and every deployment that
347/// wanted to be running is named in the warning, so the gap between "the
348/// operator deployed a worker" and "nothing is running it" is never quiet.
349async fn commission_worker_supervision(
350    state: &ServerState,
351    policy: Option<crate::worker::SupervisionPolicy>,
352) {
353    let supervisor = state.worker_supervisor();
354    let Some(policy) = policy else {
355        match supervisor.report().await {
356            Ok(report) => {
357                let wanted: Vec<&str> = report
358                    .workers
359                    .iter()
360                    .filter(|worker| worker.desired == aion_store::DesiredState::Running)
361                    .map(|worker| worker.name.as_str())
362                    .collect();
363                if wanted.is_empty() {
364                    info!("managed-worker supervision is not configured; no deployment wants it");
365                } else {
366                    warn!(
367                        deployments = wanted.join(", "),
368                        remedy = crate::worker::supervisor::UNCOMMISSIONED_REMEDY,
369                        "worker deployments want to be running but supervision is not configured"
370                    );
371                }
372            }
373            Err(error) => error!(
374                %error,
375                "managed-worker supervision is not configured and the deployment records \
376                 could not be read to say what that costs"
377            ),
378        }
379        return;
380    };
381    if !supervisor.commission(policy, crate::worker::ManagedExecutable::CurrentServer) {
382        error!("managed-worker supervision was already commissioned before boot completed");
383        return;
384    }
385    match supervisor.reconcile().await {
386        Ok(0) => info!("managed-worker supervision commissioned; no deployment wants to run"),
387        Ok(supervised) => info!(supervised, "managed-worker supervision commissioned"),
388        Err(error) => error!(%error, "managed-worker fleet could not be converged at boot"),
389    }
390}
391
392fn transport_result(
393    transport: &'static str,
394    result: Result<Result<(), ServerError>, tokio::task::JoinError>,
395) -> Result<(), ServerError> {
396    match result {
397        Ok(transport_outcome) => transport_outcome,
398        Err(join_error) => Err(ServerError::Transport {
399            transport,
400            message: join_error.to_string(),
401        }),
402    }
403}
404
405async fn serve_grpc(
406    state: ServerState,
407    address: SocketAddr,
408    shutdown: tokio::sync::watch::Receiver<bool>,
409) -> Result<(), ServerError> {
410    let workflow = api::grpc::workflow_service(state.clone());
411    let worker = api::worker_grpc::worker_service(state.clone());
412    let mut router = TonicServer::builder()
413        .add_service(workflow)
414        .add_service(worker);
415    // Dark by default: the deploy service joins the listener only when the
416    // operator commissioned it; otherwise the surface answers Unimplemented.
417    if state.runtime_config().deploy.enabled {
418        router = router.add_service(api::deploy_grpc::deploy_service(state)?);
419    }
420    router
421        .serve_with_shutdown(address, shutdown_requested(shutdown))
422        .await
423        .map_err(|source| transport_bind("grpc", address, source))?;
424    Ok(())
425}
426
427async fn serve_http(
428    state: ServerState,
429    address: SocketAddr,
430    shutdown: tokio::sync::watch::Receiver<bool>,
431) -> Result<(), ServerError> {
432    let listener = TcpListener::bind(address)
433        .await
434        .map_err(|source| transport_bind("http", address, source))?;
435    axum::serve(listener, api::http::http_router(state)?)
436        .with_graceful_shutdown(shutdown_requested(shutdown))
437        .await
438        .map_err(|source| transport_bind("http", address, source))?;
439    Ok(())
440}
441
442async fn shutdown_requested(mut shutdown: tokio::sync::watch::Receiver<bool>) {
443    while !*shutdown.borrow_and_update() {
444        if shutdown.changed().await.is_err() {
445            break;
446        }
447    }
448}
449
450async fn shutdown_signal() -> Result<(), ServerError> {
451    #[cfg(unix)]
452    {
453        use tokio::signal::unix::{SignalKind, signal};
454
455        let mut terminate = signal(SignalKind::terminate())
456            .map_err(|source| signal_listener("SIGTERM", &source))?;
457        let mut interrupt =
458            signal(SignalKind::interrupt()).map_err(|source| signal_listener("SIGINT", &source))?;
459        tokio::select! {
460            _ = terminate.recv() => Ok(()),
461            _ = interrupt.recv() => Ok(()),
462        }
463    }
464
465    #[cfg(not(unix))]
466    {
467        tokio::signal::ctrl_c()
468            .await
469            .map_err(|source| signal_listener("shutdown signal", &source))
470    }
471}
472
473fn signal_listener(listener: &'static str, source: &std::io::Error) -> ServerError {
474    ServerError::SignalListener {
475        listener,
476        message: source.to_string(),
477    }
478}
479
480fn reject_auth_without_feature(config: &ServerConfig) -> Result<(), ServerError> {
481    if cfg!(not(feature = "auth")) && config.auth.enabled {
482        return Err(ServerError::Config {
483            message: "auth.enabled=true but binary compiled without auth feature".to_owned(),
484        });
485    }
486    Ok(())
487}
488
489/// Rebuild the outbox-related boot state BEFORE the dispatcher's first claim,
490/// when (and only when) the outbox is commissioned:
491///
492/// - #204: repopulate the durable pause dispatch-hold from `list_paused`, so a
493///   run paused before a restart keeps its outbox rows held (never claimed)
494///   after recovery. A run projecting `Paused` is excluded from `list_active`
495///   respawn for free; this repopulates the hold that would otherwise be empty
496///   in memory after a crash.
497/// - #253: settle terminal workflows' stranded outbox rows. A workflow that
498///   reached a durable terminal without its rows being settled (a settle-hook
499///   failure, or a crash between the terminal append and the settle) must not
500///   have those rows re-armed and redelivered after restart — that is the
501///   zombie-round incident. A sweep error is loud but non-fatal: the
502///   settle-at-terminal hook and the reconciler's liveness gate remain as
503///   repair paths, and the residual window is one bounded dispatch whose
504///   completion drops unmatched, never a re-arm loop.
505async fn rebuild_outbox_boot_state(state: &ServerState, outbox_config: &OutboxConfig) {
506    if !outbox_config.enabled {
507        return;
508    }
509    let Ok(engine) = state.engine() else {
510        return;
511    };
512    if let Err(error) = engine.rebuild_paused_runs().await {
513        warn!(%error, "failed to rebuild paused-runs dispatch hold at startup");
514    }
515    let Some(outbox_store) = state.outbox_store() else {
516        return;
517    };
518    match crate::worker::settle_terminal_outbox_rows(engine.store().as_ref(), outbox_store.as_ref())
519        .await
520    {
521        Ok(settled) if settled.is_empty() => {}
522        Ok(settled) => {
523            info!(
524                settled = settled.len(),
525                "boot sweep settled stranded outbox rows for terminal workflows"
526            );
527        }
528        Err(error) => {
529            error!(
530                %error,
531                "boot sweep failed to settle terminal workflows' outbox rows; \
532                 the reconciler liveness gate remains the backstop"
533            );
534        }
535    }
536}
537
538/// Spawn the durable-outbox fan-out dispatcher when, and only when, the
539/// operator commissioned it (`outbox.enabled = true`).
540///
541/// This is the single gate that keeps Phase 2 dormant: with the flag off (the
542/// default) the function returns immediately without spawning a task, so
543/// default server behaviour — and the live workflow dispatch path — is entirely
544/// unchanged. When commissioned, the dispatcher claims rows through the engine's
545/// own shared haematite leaf, so its writes serialize through the same durable
546/// store. The dispatcher shares the server's shutdown watch, so it drains on the
547/// same signal as the transports.
548///
549/// NOTE (Phase boundary): the spawned dispatcher dispatches claimed rows and
550/// records each row's terminal outbox state (done / retry / failed). Routing the
551/// worker completion back into workflow history through the Recorder is Phase 3
552/// and is not wired here.
553fn maybe_spawn_outbox_dispatcher(
554    state: &ServerState,
555    outbox_config: &OutboxConfig,
556    clustered: bool,
557    backpressure_settings: BackpressureSettings,
558    shutdown_rx: &tokio::sync::watch::Receiver<bool>,
559    liminal_address_hint: &str,
560) -> Result<OutboxWorkerListener, ServerError> {
561    if !outbox_config.enabled {
562        return Ok(OutboxWorkerListener::default());
563    }
564    let dispatcher_config = resolve_outbox_config(outbox_config)?;
565    // Share the engine's already-opened haematite store. The
566    // dispatcher's `claim_outbox_rows` writes serialize against the engine's
567    // `append_with_outbox`; the
568    // in-memory backend has no outbox table, so `outbox_store()` is `None` and
569    // commissioning the dispatcher against it is a configuration error (LSUB-4-2).
570    let outbox_store = state.outbox_store().ok_or_else(|| ServerError::Config {
571        message: "outbox.enabled=true requires store.backend=haematite: \
572                  the durable outbox dispatcher claims rows from the store's outbox table, which \
573                  the in-memory store does not provide"
574            .to_owned(),
575    })?;
576    let dispatcher_builder = OutboxDispatcher::new(Arc::clone(&outbox_store), dispatcher_config);
577    let delivery_gate = dispatcher_builder.delivery_gate();
578    let engine = state.engine()?;
579    let delivery_callback: Arc<dyn OutboxDeliveryCallback> =
580        Arc::new(ServerOutboxDeliveryCallback::new(engine));
581    let (row_dispatch, worker_listener) = select_outbox_row_dispatch(
582        state,
583        outbox_config,
584        shutdown_rx,
585        delivery_gate.clone(),
586        Arc::clone(&delivery_callback),
587        liminal_address_hint,
588    )?;
589    // LSUB-2: share the engine's advisory wake so the stage seam pulses this
590    // dispatcher the instant a fan-out row commits, dispatching in ~RTT instead of
591    // up to one poll interval. The wake is always-on and free; the interval poll is
592    // untouched, so it remains the correctness backstop for any lost wake.
593    // Control-Plane Phase 2 (P2-Q2): attach per-tenant keyed backpressure so each
594    // sweep claims per-namespace, round-robin, capped at each tenant's CLAIMED-only
595    // headroom (`per_node_ceiling − claimed`). The quota cache front-runs a per-sweep
596    // quorum `get_namespace`. With the generous platform default and no tenant
597    // override the ceiling never engages, so a default deployment's claim behaviour is
598    // byte-identical to the pre-Phase-2 single unscoped claim.
599    let quota_cache = crate::worker::QuotaCache::new(
600        Arc::clone(state.namespace_store()),
601        backpressure_settings.platform_default,
602        QUOTA_CACHE_TTL,
603    );
604    let backpressure =
605        crate::worker::Backpressure::new(quota_cache.clone(), backpressure_settings.fraction);
606    let mut dispatcher = dispatcher_builder
607        .with_dispatch(row_dispatch)
608        .with_delivery_callback(delivery_callback)
609        .with_wake(state.outbox_wake())
610        .with_backpressure(backpressure);
611    // #204: attach the engine's durable pause dispatch-hold so a held (paused)
612    // run's rows are never claimed. The hold set is rebuilt from `list_paused`
613    // BEFORE this spawn (see `run_server`), so the dispatcher's first claim
614    // already excludes pre-pause rows after a restart.
615    if let Ok(engine) = state.engine() {
616        dispatcher = dispatcher.with_paused_runs(engine.paused_runs());
617    }
618    tokio::spawn(dispatcher.run(shutdown_rx.clone()));
619    // Control-Plane Phase 2 (P2-Q3): commission the ops-console quota-state
620    // broadcaster on the SAME durable stores + quota cache the dispatcher enforces
621    // against, so the console badge is a faithful window onto the live per-tenant
622    // in-flight/ceiling the backpressure caps. It shares the shutdown watch, so it
623    // drains with the dispatcher. Only spawned alongside the (default-off)
624    // dispatcher: quota state is meaningless without the outbox fan-out path, and
625    // `in_flight` is the durable Claimed outbox count that path produces.
626    let quota_broadcaster = crate::worker::QuotaBroadcaster::new(
627        Arc::clone(state.namespace_store()),
628        Arc::clone(&outbox_store),
629        quota_cache,
630        state.cluster_publisher().clone(),
631        QUOTA_BROADCAST_CADENCE,
632    );
633    tokio::spawn(quota_broadcaster.run(shutdown_rx.clone()));
634    // LSUB-4-1: the single dispatcher task is spawned in both modes. In a
635    // single-node boot it owns all shards by construction; in an active-active
636    // clustered boot it claims ONLY the shards this node owns, enforced by
637    // `claim_outbox_rows`' owned-shard scope (already seeded before this point).
638    info!(
639        clustered,
640        "outbox dispatcher commissioned (active-active per-shard ownership enforced by claim scope \
641         when clustered; single-node owns all shards)"
642    );
643    // LSUB-4-4: the stale-claim reconciler is the in-flight recovery backstop. It
644    // is only configured when BOTH reconcile knobs are set, so on a clustered boot
645    // that left them unset, owner-kill in-flight recovery latency is bounded only
646    // by re-residency replay (a survivor adopting the shard re-residents from
647    // history and re-arms via `rearm_outbox_pending`), NOT by `stale_after`. Warn
648    // so the operator knows the backstop is absent.
649    if let Some(reconciler_config) = resolve_outbox_reconciler_config(outbox_config)? {
650        // #253: the reconciler's liveness gate projects each stale candidate's
651        // workflow status from the engine's event store before any re-arm, so
652        // a terminal workflow's stranded row settles instead of redelivering.
653        let event_store = state.engine()?.store();
654        let reconciler = OutboxReconciler::new(outbox_store, event_store, reconciler_config)
655            .with_delivery_gate(delivery_gate);
656        tokio::spawn(reconciler.run(shutdown_rx.clone()));
657        info!("outbox reconciler commissioned (terminal-workflow liveness gate active)");
658    } else if clustered {
659        warn!(
660            "outbox reconciler is UNCONFIGURED on a clustered boot (outbox.reconcile_interval_ms \
661             and outbox.reconcile_stale_after_ms are both unset): in-flight recovery after an \
662             owner is killed is then bounded only by re-residency replay on the adopting node, \
663             not by a stale-claim backstop; set both knobs to bound stale-claim recovery latency"
664        );
665    }
666    Ok(worker_listener)
667}
668
669/// Spawn the SS-5b cluster supervisor when, and only when, this is a distributed
670/// haematite boot whose `[store.cluster]` declared peers with owned shards.
671///
672/// Reads the failover cadence + debounce from the cluster config (or the
673/// documented defaults), then asks the state to spawn the supervisor over its
674/// retained concrete store and live engine. With no `[store.cluster]` section —
675/// or with no peer declaring `owned_shards` — nothing is spawned and behaviour
676/// is unchanged.
677fn maybe_spawn_cluster_supervisor(
678    state: &ServerState,
679    cluster_config: Option<&crate::config::ClusterConfig>,
680    shutdown_rx: &tokio::sync::watch::Receiver<bool>,
681) -> Result<(), ServerError> {
682    let Some(cluster) = cluster_config else {
683        return Ok(());
684    };
685    let poll_interval = std::time::Duration::from_millis(
686        cluster
687            .failover_poll_interval_ms
688            .unwrap_or(crate::config::DEFAULT_FAILOVER_POLL_INTERVAL_MS),
689    );
690    let confirmations = cluster
691        .failover_confirmations
692        .unwrap_or(crate::config::DEFAULT_FAILOVER_CONFIRMATIONS);
693    let supervisor_config = crate::cluster::SupervisorConfig {
694        poll_interval,
695        confirmations,
696    };
697    let spawned = state.spawn_cluster_supervisor(supervisor_config, shutdown_rx.clone())?;
698    if spawned {
699        info!(
700            poll_interval_ms = %poll_interval.as_millis(),
701            confirmations,
702            "SS-5b cluster supervisor commissioned (automatic peer-down failover)"
703        );
704    }
705    Ok(())
706}
707
708/// Select the outbox row-dispatch sink by the configured `outbox.transport`,
709/// returning the sink plus the worker listener whose lifetime the caller must
710/// hold.
711///
712/// `grpc` (the default) builds the unchanged [`WorkerOutboxDispatch`] over the
713/// connected-worker registry and carries the empty [`OutboxWorkerListener`], so a
714/// default server is byte-identical. `liminal` builds the cross-node
715/// [`RegistryLiminalDispatch`](crate::worker::RegistryLiminalDispatch) AND stands
716/// up the liminal worker listener the aion-server hosts (returned in the guard);
717/// it is only reachable when the `liminal-transport` feature is compiled in, and
718/// selecting it without that feature is a configuration error rather than a
719/// silent fall-through to gRPC.
720fn select_outbox_row_dispatch(
721    state: &ServerState,
722    outbox_config: &OutboxConfig,
723    shutdown_rx: &tokio::sync::watch::Receiver<bool>,
724    delivery_gate: DeliveryGate,
725    delivery_callback: Arc<dyn OutboxDeliveryCallback>,
726    liminal_address_hint: &str,
727) -> Result<(Arc<dyn OutboxRowDispatch>, OutboxWorkerListener), ServerError> {
728    match outbox_config.transport {
729        OutboxTransport::Grpc => {
730            let push_dispatcher = ActivityDispatcher::new(state.worker_registry().clone())
731                .with_drain_state(state.drain_state().clone())
732                .with_completion_fences(state.pending_activities().completion_fences())
733                // Share the SAME queue-service seams the direct dispatch path
734                // uses, so a row parked on this leg reaches `GET
735                // /queues/unserved` and `describe`'s `unserved` list rather
736                // than being invisible to both.
737                .with_queue_service(
738                    state.queue_declarations().clone(),
739                    state.queue_service_state().clone(),
740                    state.runtime_config().worker.queue_service.clone(),
741                )
742                // ...including the cluster publisher, so an unbounded park on
743                // this leg is announced on the operator's real-time channel
744                // exactly like the direct path (#266 T4).
745                .with_cluster_publisher(state.cluster_publisher().clone());
746            // Control-Plane Phase 2 (P2-P3): attach the short-TTL placement cache
747            // so an unpinned row in a `Prefer{L}` namespace prefers an L-labelled
748            // worker (spilling to any live worker). The cache front-runs a per-row
749            // quorum `get_namespace` on the hot claim loop; a default-`Unplaced`
750            // deployment is byte-identical (every row falls through to any-worker).
751            let placement_cache = crate::worker::PlacementCache::new(
752                Arc::clone(state.namespace_store()),
753                PLACEMENT_CACHE_TTL,
754            );
755            let dispatch: Arc<dyn OutboxRowDispatch> = Arc::new(
756                WorkerOutboxDispatch::new(push_dispatcher).with_placement_cache(placement_cache),
757            );
758            Ok((dispatch, OutboxWorkerListener::default()))
759        }
760        OutboxTransport::Liminal => build_liminal_row_dispatch(
761            state,
762            outbox_config,
763            shutdown_rx,
764            delivery_gate,
765            delivery_callback,
766            liminal_address_hint,
767        ),
768    }
769}
770
771/// Build the production liminal row-dispatch sink and host the worker listener, or
772/// fail with the missing-feature error.
773///
774/// This lifts the tested cross-node wiring (the `lsub1`/`lsub5` e2e blueprint)
775/// into the production boot. The aion-server HOSTS the liminal listener that
776/// remote workers connect IN to, so its
777/// [`ConnectionSupervisor`](liminal_server::server::connection::ConnectionSupervisor)
778/// owns each worker's connection and can push a dispatch out on it. The
779/// constructor cycle resolves the notifier <-> supervisor dependency:
780///
781/// 1. Reuse the registry already in [`ServerState`] — gRPC and liminal workers
782///    share ONE registry and the same `select_worker`, so routing is identical.
783/// 2. Build the [`LiminalConnectionNotifier`] over that registry (no supervisor
784///    yet).
785/// 3. Build the [`LiminalConnectionServices`] from the liminal listen config.
786/// 4. Build the [`ConnectionSupervisor`] WITH the services + notifier.
787/// 5. Bind the supervisor back into the notifier (must succeed).
788/// 6. Bind the [`ServerListener`] on the configured listen address — workers
789///    connect IN here.
790/// 7. Reuse the SAME completion callback the gRPC completion path installs
791///    ([`ServerOutboxDeliveryCallback`] over the live engine), so a liminal
792///    completion re-enters aion through the identical terminal-recording seam.
793/// 8. Build the [`RegistryLiminalDispatch`] over the registry + callback (it
794///    constructs the [`LiminalCompletionSource`] internally).
795///
796/// The returned listener is held by the caller for the server's lifetime; its
797/// `Drop` stops the accept worker on shutdown.
798///
799/// [`LiminalConnectionServices`]: liminal_server::server::connection::LiminalConnectionServices
800/// [`ServerListener`]: liminal_server::server::listener::ServerListener
801/// [`ServerOutboxDeliveryCallback`]: crate::worker::ServerOutboxDeliveryCallback
802/// [`LiminalCompletionSource`]: crate::worker::LiminalCompletionSource
803/// [`LiminalConnectionNotifier`]: crate::worker::LiminalConnectionNotifier
804#[cfg(feature = "liminal-transport")]
805fn build_liminal_row_dispatch(
806    state: &ServerState,
807    outbox_config: &OutboxConfig,
808    shutdown_rx: &tokio::sync::watch::Receiver<bool>,
809    delivery_gate: DeliveryGate,
810    callback: Arc<dyn OutboxDeliveryCallback>,
811    liminal_address_hint: &str,
812) -> Result<(Arc<dyn OutboxRowDispatch>, OutboxWorkerListener), ServerError> {
813    use liminal_server::config::ServerConfig as LiminalServerConfig;
814    use liminal_server::config::{LimitsConfig, ServicesConfig};
815    use liminal_server::server::connection::{ConnectionSupervisor, LiminalConnectionServices};
816    use liminal_server::server::listener::ServerListener;
817
818    use crate::worker::{LiminalConnectionNotifier, RegistryLiminalDispatch};
819
820    let listen_address = outbox_config
821        .liminal_listen_address
822        .as_ref()
823        .ok_or_else(|| ServerError::Config {
824            message: format!(
825                "outbox.transport=liminal requires outbox.liminal_listen_address (host:port \
826                 the aion-server listens on for inbound liminal worker connections); \
827                 {liminal_address_hint}"
828            ),
829        })?;
830    let listen_address: SocketAddr =
831        listen_address
832            .parse()
833            .map_err(|error| ServerError::Config {
834                message: format!(
835                    "outbox.liminal_listen_address must be a host:port socket address: {error}"
836                ),
837            })?;
838
839    // The liminal listener is the worker-connection front door only: it binds the
840    // wire listen address and serves the connection supervisor. `from_config` and
841    // `ServerListener::bind` read neither `health_listen_address` nor `channels`
842    // (the health probe is bound only by the standalone liminal server's full
843    // boot, not this embedded path), so no separate health port is bound here;
844    // it is set structurally to the listen address and never used.
845    let liminal_config = LiminalServerConfig {
846        listen_address,
847        health_listen_address: listen_address,
848        drain_timeout_ms: 30_000,
849        channels: Vec::new(),
850        routing_rules: Vec::new(),
851        persistence_path: None,
852        cluster: None,
853        // liminal 0.2.3 (H4) added an optional shared-token Connect gate. `None`
854        // keeps this embedded worker front door open at the liminal layer —
855        // identical to the pre-0.2.3 wire behavior; worker identity/authorization
856        // stays aion's job (x-aion-* registration metadata). Threading an
857        // operator-configured token through aion's outbox config is a separate
858        // feature decision, not part of the dependency alignment.
859        auth: None,
860        // liminal 0.2.4 (D2/§5): service profile + operational bounds. Defaults =
861        // full profile + the certifying-pair-signed caps — byte-equivalent to the
862        // 0.2.3 behaviour this embedded front door always had. A worker-front-door
863        // profile election here is a future feature decision, not this migration.
864        services: ServicesConfig::default(),
865        limits: LimitsConfig::default(),
866        // liminal 0.3.0 (LP-WS-TRANSPORT R1 / LP Part B): optional WebSocket
867        // acceptor and participant lifecycle activation. `None` for both starts
868        // no WebSocket listener and leaves the participant capability disabled —
869        // documented as byte-identical to the pre-0.3.0 build. Electing either
870        // for this embedded worker front door is a feature decision, not part of
871        // the dependency alignment.
872        websocket: None,
873        participant: None,
874    };
875
876    // (1) Reuse the registry already in ServerState: gRPC + liminal workers share
877    // ONE registry and the same `select_worker`.
878    let registry = state.worker_registry().clone();
879    // (2) Notifier over that registry (supervisor bound after it is built), with the
880    // NOI-5b transcript tap: a worker's observability publishes on the reserved
881    // channel drain into the SAME transcript sequencer the transcript socket serves,
882    // so a live agent's transcript is persisted + fanned out. (Captures the current
883    // runtime handle to bridge the sync connection callback onto the async append.)
884    let notifier = Arc::new(
885        LiminalConnectionNotifier::new(registry.clone())
886            .with_contract_catalog(state.engine()?)
887            .with_transcript_publisher(state.transcript_publisher().clone())
888            // The SAME per-task liveness tracker the engine-seam bridge tracks
889            // into: a liminal worker's automatic liveness beats refresh it, so
890            // the #176 expiry sweeper never falsely expires a healthy liminal
891            // worker running an activity longer than the heartbeat window.
892            .with_heartbeat_tracker(state.heartbeat_tracker().clone()),
893    );
894    // (3) Connection services from the liminal listen config.
895    let services = Arc::new(
896        LiminalConnectionServices::from_config(&liminal_config).map_err(|error| {
897            ServerError::Config {
898                message: format!("liminal connection services build failed: {error}"),
899            }
900        })?,
901    );
902    // (4) Supervisor WITH the services + notifier (the cycle's forward edge).
903    let supervisor = ConnectionSupervisor::with_services_and_notifier(services, notifier.clone())
904        .map_err(|error| ServerError::Config {
905        message: format!("liminal connection supervisor build failed: {error}"),
906    })?;
907    // (5) Bind the supervisor back into the notifier (the cycle's back edge); a
908    // failure here is a wiring bug, surfaced rather than silently ignored.
909    if !notifier.bind_supervisor(supervisor.clone()) {
910        return Err(ServerError::Config {
911            message: "liminal notifier supervisor handle was already bound during boot".to_owned(),
912        });
913    }
914    // (5b) Commission the connection dead-man switch over the SAME notifier. It
915    // pings every connected worker on a derived quarter-window cadence: the
916    // answers keep a healthy IDLE connection's lease alive (so the idle expiry
917    // cannot fire on a live worker), and the pings themselves are what a worker
918    // measures silence against (so a wedged half-open socket becomes a declared,
919    // logged death on the worker side instead of an unbounded blind wait). Not
920    // opt-in: liveness detection is a correctness property of this transport.
921    // The handle is detached — dropping a tokio `JoinHandle` never cancels the
922    // task — exactly as the heartbeat sweeper is spawned.
923    drop(state.spawn_liminal_liveness_probe(notifier.clone(), shutdown_rx.clone()));
924    // (6) Bind the listener on the configured address — workers connect IN here.
925    let listener =
926        ServerListener::bind(&liminal_config, supervisor).map_err(|error| ServerError::Config {
927            message: format!("liminal worker listener failed to bind {listen_address}: {error}"),
928        })?;
929    // (7) Reuse the SAME completion callback the gRPC completion path uses, over
930    // the live engine, so a liminal completion re-enters aion through the
931    // identical terminal-recording seam (`record_fan_out_completion`).
932    // (8) The registry-backed dispatch builds its LiminalCompletionSource from the
933    // shared callback internally. Attach the SAME short-TTL placement cache the
934    // gRPC arm installs (Control-Plane Phase 2, P2-P3), so an unpinned row in a
935    // `Prefer{L}` namespace prefers an L-labelled worker (spilling to any live
936    // worker) on the cross-node liminal transport too — the cluster-failover
937    // demo behaviour. A default-`Unplaced` deployment is byte-identical.
938    let placement_cache = crate::worker::PlacementCache::new(
939        Arc::clone(state.namespace_store()),
940        PLACEMENT_CACHE_TTL,
941    );
942    // NOI-6: install the SAME attempt-owner back-index the server's intervention
943    // router resolves through, so each dispatched agent attempt binds its owning
944    // worker and a pushed command reaches the worker this dispatcher sent it to.
945    let dispatch: Arc<dyn OutboxRowDispatch> = Arc::new(
946        RegistryLiminalDispatch::new(registry, callback, delivery_gate)
947            .with_placement_cache(placement_cache)
948            .with_attempt_owners(state.attempt_owners().clone()),
949    );
950
951    info!(
952        listen_address = %listen_address,
953        "liminal outbox worker listener commissioned (remote workers connect in and self-register)"
954    );
955    Ok((
956        dispatch,
957        OutboxWorkerListener {
958            _inner: Some(listener),
959        },
960    ))
961}
962
963/// Feature-off stub: selecting the liminal transport without the
964/// `liminal-transport` feature is a configuration error, never a silent
965/// fall-through to gRPC.
966#[cfg(not(feature = "liminal-transport"))]
967fn build_liminal_row_dispatch(
968    _state: &ServerState,
969    _outbox_config: &OutboxConfig,
970    _shutdown_rx: &tokio::sync::watch::Receiver<bool>,
971    _delivery_gate: DeliveryGate,
972    _delivery_callback: Arc<dyn OutboxDeliveryCallback>,
973    _liminal_address_hint: &str,
974) -> Result<(Arc<dyn OutboxRowDispatch>, OutboxWorkerListener), ServerError> {
975    Err(ServerError::Config {
976        message: "outbox.transport=liminal requires the aion-server `liminal-transport` \
977                  Cargo feature, which is not enabled in this build"
978            .to_owned(),
979    })
980}
981
982/// Resolve the validated, all-present outbox knobs into the dispatcher's
983/// non-optional config. Validation already guaranteed each value is set and in
984/// range when `outbox.enabled` is true, so an absent value here is a defensive
985/// configuration error, not a default to invent.
986fn resolve_outbox_config(outbox: &OutboxConfig) -> Result<OutboxDispatcherConfig, ServerError> {
987    let poll_interval_ms = outbox.poll_interval_ms.ok_or_else(|| ServerError::Config {
988        message: crate::config::OUTBOX_POLL_INTERVAL_REQUIRED.to_owned(),
989    })?;
990    let batch_size = outbox.batch_size.ok_or_else(|| ServerError::Config {
991        message: crate::config::OUTBOX_BATCH_SIZE_REQUIRED.to_owned(),
992    })?;
993    let max_attempts = outbox.max_attempts.ok_or_else(|| ServerError::Config {
994        message: crate::config::OUTBOX_MAX_ATTEMPTS_REQUIRED.to_owned(),
995    })?;
996    let backoff_base_ms = outbox.backoff_base_ms.ok_or_else(|| ServerError::Config {
997        message: crate::config::OUTBOX_BACKOFF_BASE_REQUIRED.to_owned(),
998    })?;
999    let backoff_multiplier = outbox
1000        .backoff_multiplier
1001        .ok_or_else(|| ServerError::Config {
1002            message: crate::config::OUTBOX_BACKOFF_MULTIPLIER_REQUIRED.to_owned(),
1003        })?;
1004    let backoff_max_ms = outbox.backoff_max_ms.ok_or_else(|| ServerError::Config {
1005        message: crate::config::OUTBOX_BACKOFF_MAX_REQUIRED.to_owned(),
1006    })?;
1007    Ok(OutboxDispatcherConfig {
1008        poll_interval: std::time::Duration::from_millis(poll_interval_ms),
1009        batch_size,
1010        max_attempts,
1011        backoff_base: std::time::Duration::from_millis(backoff_base_ms),
1012        backoff_multiplier,
1013        backoff_max: std::time::Duration::from_millis(backoff_max_ms),
1014    })
1015}
1016
1017fn resolve_outbox_reconciler_config(
1018    outbox: &OutboxConfig,
1019) -> Result<Option<OutboxReconcilerConfig>, ServerError> {
1020    let (Some(interval_ms), Some(stale_after_ms)) = (
1021        outbox.reconcile_interval_ms,
1022        outbox.reconcile_stale_after_ms,
1023    ) else {
1024        return Ok(None);
1025    };
1026    let batch_size = outbox.batch_size.ok_or_else(|| ServerError::Config {
1027        message: crate::config::OUTBOX_BATCH_SIZE_REQUIRED.to_owned(),
1028    })?;
1029    Ok(Some(OutboxReconcilerConfig {
1030        interval: std::time::Duration::from_millis(interval_ms),
1031        stale_after: std::time::Duration::from_millis(stale_after_ms),
1032        batch_size,
1033    }))
1034}
1035
1036fn reject_tls_until_supported(state: &ServerState) -> Result<(), ServerError> {
1037    if state.runtime_config().tls.is_some() {
1038        return Err(ServerError::Config {
1039            message: "configured TLS material cannot be served until transport TLS is wired"
1040                .to_owned(),
1041        });
1042    }
1043    Ok(())
1044}
1045
1046fn store_backend_label(backend: StoreBackend) -> &'static str {
1047    match backend {
1048        StoreBackend::Memory => "memory",
1049        StoreBackend::Haematite => "haematite",
1050    }
1051}
1052
1053fn namespace_mode_label(mode: &NamespaceMode) -> &'static str {
1054    match mode {
1055        NamespaceMode::SharedEngine => "SharedEngine",
1056        NamespaceMode::SingleTenant { .. } => "SingleTenant",
1057    }
1058}
1059
1060fn transport_bind<E>(transport: &'static str, address: SocketAddr, source: E) -> ServerError
1061where
1062    E: std::error::Error,
1063{
1064    ServerError::TransportBind {
1065        transport,
1066        address,
1067        message: source.to_string(),
1068    }
1069}
1070
1071#[cfg(test)]
1072mod tests {
1073    #![allow(clippy::expect_used)]
1074
1075    use super::{
1076        BackpressureSettings, OutboxConfig, OutboxTransport, maybe_spawn_outbox_dispatcher,
1077        resolve_outbox_reconciler_config,
1078    };
1079    use crate::ServerState;
1080    use crate::config::RuntimeConfig;
1081    use aion_store::InMemoryStore;
1082    use std::net::SocketAddr;
1083    use std::time::Duration;
1084
1085    /// Own-all, generous-default backpressure settings for the gate tests (the
1086    /// single-node default: fraction 1, so the ceiling never engages).
1087    fn test_backpressure_settings() -> BackpressureSettings {
1088        BackpressureSettings {
1089            platform_default: crate::config::DEFAULT_MAX_IN_FLIGHT_ACTIVITIES,
1090            fraction: crate::worker::OwnedShardFraction::own_all(),
1091        }
1092    }
1093
1094    /// A minimal `RuntimeConfig` for building an in-memory `ServerState` in unit
1095    /// tests (mirrors `state.rs`'s test `runtime_config`).
1096    fn runtime_config() -> RuntimeConfig {
1097        use crate::config::{
1098            AuthConfig, AuthoringConfig, DeployConfig, DevConfig, ListenConfig, MetricsConfig,
1099            NamespaceConfig, NamespaceMode, OpsConsoleAssetSource, OpsConsoleConfig,
1100            WebSocketConfig, WorkerConfig,
1101        };
1102        RuntimeConfig {
1103            listen: ListenConfig {
1104                grpc: SocketAddr::from(([127, 0, 0, 1], 50051)),
1105                http: SocketAddr::from(([127, 0, 0, 1], 8080)),
1106            },
1107            tls: None,
1108            auth: AuthConfig {
1109                enabled: false,
1110                jwks_url: None,
1111                jwks_refresh_seconds: 300,
1112            },
1113            ops_console: OpsConsoleConfig {
1114                source: OpsConsoleAssetSource::Embedded,
1115            },
1116            namespace: NamespaceConfig {
1117                mode: NamespaceMode::SharedEngine,
1118            },
1119            worker: WorkerConfig {
1120                heartbeat_window: Duration::from_secs(30),
1121                ..WorkerConfig::default()
1122            },
1123            websocket: WebSocketConfig {
1124                outbound_buffer_bound: 32,
1125                event_broadcast_capacity: Some(64),
1126                cluster_broadcast_capacity: Some(64),
1127            },
1128            workflow_packages: Vec::new(),
1129            deploy: DeployConfig::default(),
1130            authoring: AuthoringConfig::default(),
1131            dev: DevConfig::default(),
1132            outbox: OutboxConfig::default(),
1133            observability: crate::config::ObservabilityConfig::with_flush_policy(64, 0),
1134            mcp: crate::config::ResolvedMcpConfig::default(),
1135            scheduler_threads: 1,
1136            jit_threshold: None,
1137            query_timeout: Some(Duration::from_secs(10)),
1138            default_namespace: "default".to_owned(),
1139            auto_create: crate::config::AutoCreate::Open,
1140            max_in_flight_activities: crate::config::DEFAULT_MAX_IN_FLIGHT_ACTIVITIES,
1141            drain_timeout: Duration::from_secs(30),
1142            metrics: MetricsConfig { enabled: true },
1143            owned_shards: Vec::new(),
1144            cors_allowed_origins: Vec::new(),
1145        }
1146    }
1147
1148    /// An `OutboxConfig` with `enabled = true` and every required knob present, so
1149    /// the only remaining gate is the store-backend / outbox-table availability.
1150    fn enabled_outbox_config() -> OutboxConfig {
1151        OutboxConfig {
1152            enabled: true,
1153            poll_interval_ms: Some(250),
1154            batch_size: Some(64),
1155            max_attempts: Some(5),
1156            backoff_base_ms: Some(100),
1157            backoff_multiplier: Some(2),
1158            backoff_max_ms: Some(30_000),
1159            reconcile_interval_ms: None,
1160            reconcile_stale_after_ms: None,
1161            transport: OutboxTransport::Grpc,
1162            liminal_listen_address: None,
1163        }
1164    }
1165
1166    /// LSUB-4-2 / LSUB-4-6 (Memory-backend guard): commissioning the outbox
1167    /// dispatcher against the in-memory backend (which has no outbox table, so
1168    /// `outbox_store()` is `None`) is a configuration error, and the message names
1169    /// haematite as the required durable backend.
1170    #[tokio::test]
1171    async fn outbox_enabled_on_memory_backend_is_a_config_error() {
1172        let state = ServerState::build_with_store(InMemoryStore::default(), runtime_config())
1173            .await
1174            .expect("build in-memory state");
1175        let (_tx, rx) = tokio::sync::watch::channel(false);
1176        let error = maybe_spawn_outbox_dispatcher(
1177            &state,
1178            &enabled_outbox_config(),
1179            false,
1180            test_backpressure_settings(),
1181            &rx,
1182            "set outbox.liminal_listen_address in the test config",
1183        )
1184        .expect_err("outbox.enabled on the memory backend must be a config error");
1185        assert!(
1186            error.is_config(),
1187            "memory-backend outbox error must be Config"
1188        );
1189        let message = error.to_string();
1190        assert!(
1191            message.contains("store.backend=haematite"),
1192            "message must name the durable backend, got: {message}"
1193        );
1194    }
1195
1196    /// LSUB-4-1 (Fork-B fast path): with the outbox disabled (the default), the
1197    /// gate is a no-op even on a memory backend — nothing is spawned and no error
1198    /// is produced, so a default single-node boot is unchanged.
1199    #[tokio::test]
1200    async fn disabled_outbox_is_a_noop_on_any_backend() {
1201        let state = ServerState::build_with_store(InMemoryStore::default(), runtime_config())
1202            .await
1203            .expect("build in-memory state");
1204        let (_tx, rx) = tokio::sync::watch::channel(false);
1205        maybe_spawn_outbox_dispatcher(
1206            &state,
1207            &OutboxConfig::default(),
1208            false,
1209            test_backpressure_settings(),
1210            &rx,
1211            "set outbox.liminal_listen_address in the test config",
1212        )
1213        .expect("disabled outbox gate must be an infallible no-op");
1214    }
1215
1216    /// LSUB-4-4: the reconciler config resolves to `None` unless BOTH knobs are
1217    /// set — the condition under which the clustered-boot WARN fires.
1218    #[test]
1219    fn reconciler_config_absent_unless_both_knobs_set() {
1220        let mut config = enabled_outbox_config();
1221        // Neither knob: absent.
1222        assert!(
1223            resolve_outbox_reconciler_config(&config)
1224                .expect("resolve")
1225                .is_none()
1226        );
1227        // Only interval: still absent (the silent-backstop-absent default).
1228        config.reconcile_interval_ms = Some(1_000);
1229        assert!(
1230            resolve_outbox_reconciler_config(&config)
1231                .expect("resolve")
1232                .is_none()
1233        );
1234        // Both set: present.
1235        config.reconcile_stale_after_ms = Some(60_000);
1236        assert!(
1237            resolve_outbox_reconciler_config(&config)
1238                .expect("resolve")
1239                .is_some()
1240        );
1241    }
1242
1243    /// LSUB-PROD (13-6): the liminal transport requires `liminal_listen_address`.
1244    /// Commissioning the dispatcher with `transport = liminal` but no listen
1245    /// address is a configuration error naming the missing knob, rather than a
1246    /// panic or a silent fall-through to gRPC. Built over haematite (so
1247    /// the outbox-store gate passes and the missing-address check is actually
1248    /// reached). (Feature-gated: the liminal arm of `build_liminal_row_dispatch`
1249    /// only exists with `liminal-transport` on; in a feature-off build the same
1250    /// selection is the missing-feature error instead, covered by the type system
1251    /// rather than this test.)
1252    #[cfg(feature = "liminal-transport")]
1253    #[tokio::test]
1254    async fn liminal_transport_requires_listen_address() {
1255        use crate::config::{
1256            RuntimeSection, ServerConfig, StoreBackend, StoreConfig, WebSocketConfig,
1257        };
1258
1259        let data_dir = std::env::temp_dir().join(format!(
1260            "aion-lsub-prod-listen-guard-{}-{}",
1261            std::process::id(),
1262            std::time::SystemTime::now()
1263                .duration_since(std::time::UNIX_EPOCH)
1264                .map(|elapsed| elapsed.as_nanos())
1265                .unwrap_or_default()
1266        ));
1267        let mut outbox = enabled_outbox_config();
1268        outbox.transport = OutboxTransport::Liminal;
1269        outbox.liminal_listen_address = None;
1270        let config = ServerConfig {
1271            store: StoreConfig {
1272                backend: StoreBackend::Haematite,
1273                data_dir: Some(data_dir.to_string_lossy().into_owned()),
1274                // Required, no default: the haematite boot path refuses a config
1275                // that does not rule on the node cache's byte ceiling.
1276                node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
1277                ..StoreConfig::default()
1278            },
1279            runtime: RuntimeSection {
1280                scheduler_threads: 1,
1281                jit_threshold: None,
1282                query_timeout_ms: Some(10_000),
1283            },
1284            websocket: WebSocketConfig {
1285                outbound_buffer_bound: 32,
1286                event_broadcast_capacity: Some(64),
1287                cluster_broadcast_capacity: Some(64),
1288            },
1289            outbox: outbox.clone(),
1290            // Required, no default: the transcript drain's flush policy.
1291            observability: crate::config::ObservabilityConfig::with_flush_policy(64, 0),
1292            ..ServerConfig::default()
1293        };
1294        let state = ServerState::build(config)
1295            .await
1296            .expect("build haematite state");
1297        let (_tx, rx) = tokio::sync::watch::channel(false);
1298
1299        let error = maybe_spawn_outbox_dispatcher(
1300            &state,
1301            &outbox,
1302            false,
1303            test_backpressure_settings(),
1304            &rx,
1305            "add `liminal_listen_address = \"127.0.0.1:50061\"` to `[outbox]` in the test config",
1306        )
1307        .expect_err("liminal transport without a listen address must be a config error");
1308        assert!(
1309            error.is_config(),
1310            "missing-listen-address error must be Config"
1311        );
1312        assert!(
1313            error.to_string().contains("liminal_listen_address"),
1314            "error must name the missing knob, got: {error}"
1315        );
1316        // #180 review MAJ-4: the refusal must carry the caller's threaded
1317        // where-to-edit hint, so the production message names the resolved
1318        // config FILE, not just the key.
1319        assert!(
1320            error.to_string().contains("in the test config"),
1321            "error must carry the threaded config-location hint, got: {error}"
1322        );
1323    }
1324}
1325
1326/// LSUB-PROD (13-6): production-boot cross-node round-trip over the REAL wiring.
1327///
1328/// This is the proof that the production boot now does the full round-trip the
1329/// retired stub could not. It drives the EXACT production commissioning function
1330/// `run_server` calls — [`maybe_spawn_outbox_dispatcher`] — over a real
1331/// [`ServerState`] built with `outbox.enabled`, `transport = liminal`, and a
1332/// `liminal_listen_address`. That function lifts the full push wiring
1333/// (`build_liminal_row_dispatch`): it hosts the liminal worker listener, builds
1334/// [`RegistryLiminalDispatch`](crate::worker::RegistryLiminalDispatch) over the
1335/// SAME registry the gRPC path uses and the SAME
1336/// [`ServerOutboxDeliveryCallback`](crate::worker::ServerOutboxDeliveryCallback)
1337/// (over the live engine), and spawns the real [`OutboxDispatcher`].
1338///
1339/// A REAL remote [`LiminalActivityWorker`](aion_worker::LiminalActivityWorker)
1340/// connects IN to the listener and self-registers in-band. A `collect_four`
1341/// fan-out is started over the REAL HTTP transport, which stages four pending
1342/// outbox rows; the production-wired dispatcher claims and pushes each to the
1343/// worker, the worker executes it, and its completion re-enters aion through the
1344/// production engine callback — `record_fan_out_completion` — driving the
1345/// workflow to a recorded terminal. The proof asserts BOTH: the worker observably
1346/// executed the activities, AND the terminals were recorded in history (four
1347/// `ActivityCompleted` + one `WorkflowCompleted`), which the stub's
1348/// publish-and-mark-done path never achieved.
1349#[cfg(all(test, feature = "liminal-transport"))]
1350mod lsub_prod_xnode_e2e {
1351    #![allow(clippy::expect_used)]
1352
1353    use std::net::SocketAddr;
1354    use std::path::PathBuf;
1355    use std::sync::Arc;
1356    use std::sync::atomic::{AtomicUsize, Ordering};
1357    use std::time::{Duration, Instant};
1358
1359    use aion_core::Event;
1360    use aion_package::{
1361        ActionContract, BeamModule, BeamSet, CURRENT_FORMAT_VERSION, DeclaredActivity, Manifest,
1362        ManifestVersion, PackageBuilder, PackageContract, WorkerContract,
1363    };
1364    use aion_worker::{ActivityRegistry, LiminalActivityWorker, WorkerConfig};
1365    use axum::body;
1366    use axum::http::{Request, StatusCode};
1367    use serde_json::json;
1368    use tower::ServiceExt;
1369
1370    use super::{BackpressureSettings, maybe_spawn_outbox_dispatcher};
1371    use crate::ServerState;
1372    use crate::api::http::http_router;
1373    use crate::config::{
1374        OutboxConfig, OutboxTransport, RuntimeSection, ServerConfig, StoreBackend, StoreConfig,
1375        WebSocketConfig,
1376    };
1377
1378    type TestError = Box<dyn std::error::Error + Send + Sync>;
1379
1380    /// The `collect_four` fixture passes each member the JSON string `"in"` as
1381    /// activity input, so the worker handler decodes a [`String`], not a struct.
1382    type FanInput = String;
1383
1384    const NAMESPACE: &str = "default";
1385    const TASK_QUEUE: &str = "default";
1386    const OUTBOX_MODULE: &str = "aion_outbox_fixture";
1387    const OUTBOX_BEAM: &[u8] = include_bytes!("../tests/fixtures/aion_outbox_fixture.beam");
1388    const OUTBOX_SOURCE: &[u8] = include_bytes!("../tests/fixtures/aion_outbox_fixture.erl");
1389    const FAN_OUT: usize = 4;
1390    const FAN_ACTIVITY_TYPES: [&str; FAN_OUT] = ["fan:0", "fan:1", "fan:2", "fan:3"];
1391    const POLL_DEADLINE: Duration = Duration::from_secs(20);
1392    /// The one fan-out member the reconnect pin holds. Any of the four would do —
1393    /// they are dispatched independently and served by identical handlers.
1394    const HELD_ACTIVITY_TYPE: &str = FAN_ACTIVITY_TYPES[0];
1395
1396    fn test_error(message: impl std::fmt::Display) -> TestError {
1397        message.to_string().into()
1398    }
1399
1400    /// Reserve a loopback port and return it: the liminal listener binds this exact
1401    /// address (the production path binds the configured `liminal_listen_address`,
1402    /// so the test must commit to a concrete port the worker can also dial).
1403    fn reserve_loopback_port() -> Result<SocketAddr, TestError> {
1404        let listener = std::net::TcpListener::bind("127.0.0.1:0").map_err(test_error)?;
1405        let address = listener.local_addr().map_err(test_error)?;
1406        drop(listener);
1407        Ok(address)
1408    }
1409
1410    /// The fixture's queue-scoped `.v4` contract: the four `fan:N` activities
1411    /// `collect_four` schedules, declared on the queue its worker actually polls.
1412    ///
1413    /// Why the archive cannot just carry the manifest-derived record: by design
1414    /// `PackageContract::from_manifest` "never invents a queue", so a manifest's
1415    /// bare activity names land in `unscoped_activities` — and this server boots
1416    /// queue-routed, where an unscoped catalog is a terminal
1417    /// `NO_QUEUE_DECLARATION` at start admission
1418    /// (`aion::lifecycle::start_admission`). That refusal is EARNED: an unserved
1419    /// queue would otherwise wait silently forever. So the derived record is
1420    /// amended rather than bypassed — the same four names move out of
1421    /// `unscoped_activities` and onto the queue that serves them — and the
1422    /// package still loads through the production boot path with the `.v4`
1423    /// identity `PackageBuilder` stamps over this exact contract.
1424    ///
1425    /// The action schemas come from the SAME generator the worker's typed
1426    /// registry uses, for the SAME Rust types: `collect_four` passes each member
1427    /// the JSON string `"in"` and the handler returns a [`String`]. Deriving both
1428    /// sides from `activity_descriptor::<FanInput, String>` means the package's
1429    /// declaration and the worker's advertisement cannot drift apart, so
1430    /// registration admission (`WORKER_CONTRACT_MISMATCH`) compares two schemas
1431    /// with one source.
1432    fn fixture_contract(manifest: &Manifest) -> Result<PackageContract, TestError> {
1433        let mut actions = Vec::with_capacity(FAN_ACTIVITY_TYPES.len());
1434        for activity_type in FAN_ACTIVITY_TYPES {
1435            let descriptor = aion_worker::activity_descriptor::<FanInput, String>(activity_type)
1436                .map_err(test_error)?;
1437            actions.push(ActionContract {
1438                name: descriptor.name,
1439                input_schema: descriptor.input_schema,
1440                output_schema: descriptor.output_schema,
1441                node: None,
1442                timeout: None,
1443                retry: None,
1444                advisory: false,
1445                // A typed `String -> String` handler serves these, not an agent
1446                // harness — the fan fixture's shape merely coincides with an
1447                // agent seam's, and marking it would route it somewhere no
1448                // handler is.
1449                agent: false,
1450                // A connected worker serves this fixture's queue, so the
1451                // declaration carries no body of its own.
1452                body: None,
1453            });
1454        }
1455        let mut contract = PackageContract::from_manifest(manifest);
1456        contract.workers = vec![WorkerContract {
1457            task_queue: TASK_QUEUE.to_owned(),
1458            actions,
1459        }];
1460        contract.unscoped_activities.clear();
1461        Ok(contract)
1462    }
1463
1464    /// Build the `collect_four` package on disk so the production state-build path
1465    /// loads it exactly as it loads operator-supplied `workflow_packages`.
1466    fn write_package_archive(dir: &std::path::Path) -> Result<PathBuf, TestError> {
1467        let beams =
1468            BeamSet::new(vec![BeamModule::new(OUTBOX_MODULE, OUTBOX_BEAM)]).map_err(test_error)?;
1469        let manifest = Manifest {
1470            entry_module: OUTBOX_MODULE.to_owned(),
1471            entry_function: "collect_four".to_owned(),
1472            input_schema: json!({ "type": "object" }),
1473            output_schema: json!({}),
1474            timeout: Some(Duration::from_secs(30)),
1475            // The four ordinals `collect_four` actually fans out. This manifest
1476            // used to name one invented activity, `fixture_activity`, that the
1477            // fixture never schedules and no worker ever served.
1478            activities: FAN_ACTIVITY_TYPES
1479                .iter()
1480                .map(|activity_type| DeclaredActivity {
1481                    activity_type: (*activity_type).to_owned(),
1482                })
1483                .collect(),
1484            version: ManifestVersion::new("stamped-by-builder"),
1485            format_version: CURRENT_FORMAT_VERSION,
1486            additional_workflows: Vec::new(),
1487        };
1488        let contract = fixture_contract(&manifest)?;
1489        let archive =
1490            PackageBuilder::with_source(manifest, beams, [(OUTBOX_MODULE, OUTBOX_SOURCE.to_vec())])
1491                .with_contract(contract)
1492                .write_to_bytes()
1493                .map_err(test_error)?;
1494        let path = dir.join("collect_four.aion");
1495        std::fs::write(&path, archive).map_err(test_error)?;
1496        Ok(path)
1497    }
1498
1499    /// A production-shaped `ServerConfig`: the haematite backend (so the boot store
1500    /// path shares the leaf as the dispatcher's outbox store, exactly as
1501    /// `ServerState::build` does in production), `outbox.enabled`,
1502    /// `transport = liminal`, the reserved `liminal_listen_address`, and the
1503    /// `collect_four` package. Built through `ServerState::build` (not
1504    /// `build_with_store`), so this is the real boot store seam, not a test stand-in.
1505    fn server_config(
1506        data_dir: &std::path::Path,
1507        package_path: PathBuf,
1508        listen_address: SocketAddr,
1509    ) -> ServerConfig {
1510        ServerConfig {
1511            store: StoreConfig {
1512                backend: StoreBackend::Haematite,
1513                data_dir: Some(data_dir.to_string_lossy().into_owned()),
1514                // Required, no default: the haematite boot path refuses a config
1515                // that does not rule on the node cache's byte ceiling.
1516                node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
1517                ..StoreConfig::default()
1518            },
1519            runtime: RuntimeSection {
1520                scheduler_threads: 1,
1521                jit_threshold: None,
1522                query_timeout_ms: Some(10_000),
1523            },
1524            websocket: WebSocketConfig {
1525                outbound_buffer_bound: 32,
1526                event_broadcast_capacity: Some(64),
1527                cluster_broadcast_capacity: Some(64),
1528            },
1529            workflow_packages: vec![package_path],
1530            outbox: OutboxConfig {
1531                enabled: true,
1532                poll_interval_ms: Some(20),
1533                batch_size: Some(16),
1534                max_attempts: Some(5),
1535                backoff_base_ms: Some(50),
1536                backoff_multiplier: Some(2),
1537                backoff_max_ms: Some(1_000),
1538                reconcile_interval_ms: None,
1539                reconcile_stale_after_ms: None,
1540                transport: OutboxTransport::Liminal,
1541                liminal_listen_address: Some(listen_address.to_string()),
1542            },
1543            // Required, no default: the transcript drain's flush policy.
1544            observability: crate::config::ObservabilityConfig::with_flush_policy(64, 0),
1545            ..ServerConfig::default()
1546        }
1547    }
1548
1549    /// The remote worker self-describes for the fixture's pool `(default, default)`
1550    /// and registers a handler for every `fan:N` activity type, counting executions
1551    /// so the test proves it genuinely ran the pushed dispatches.
1552    fn worker_config() -> Result<WorkerConfig, TestError> {
1553        WorkerConfig::builder()
1554            .endpoint("unused-direct-address")
1555            .namespace(NAMESPACE)
1556            .task_queue(TASK_QUEUE)
1557            .identity("lsub-prod-worker")
1558            .max_concurrency(4)
1559            .reconnect_initial_backoff(Duration::from_millis(5))
1560            .reconnect_max_backoff(Duration::from_millis(20))
1561            .reconnect_max_attempts(3)
1562            .build()
1563            .map_err(test_error)
1564    }
1565
1566    fn worker_registry(executions: &Arc<AtomicUsize>) -> Result<Arc<ActivityRegistry>, TestError> {
1567        let mut registry = ActivityRegistry::new();
1568        for activity_type in FAN_ACTIVITY_TYPES {
1569            let executions = Arc::clone(executions);
1570            // `register_activity_with_contract`, not `register_activity`: the
1571            // bare form registers a handler with NO descriptor, so the worker
1572            // advertises four names and zero typed contracts, and admission —
1573            // which compares CONTRACTS — refuses the registration outright
1574            // (`WORKER_CONTRACT_MISMATCH`). Deriving the advertisement from
1575            // `<FanInput, String>` is what makes it the same source the
1576            // package's `fixture_contract` declares from, so the two sides
1577            // cannot drift.
1578            registry = registry
1579                .register_activity_with_contract(
1580                    activity_type,
1581                    move |_input: FanInput, _context| {
1582                        let executions = Arc::clone(&executions);
1583                        Box::pin(async move {
1584                            executions.fetch_add(1, Ordering::SeqCst);
1585                            Ok(activity_type.to_owned())
1586                        })
1587                    },
1588                )
1589                .map_err(test_error)?;
1590        }
1591        Ok(Arc::new(registry))
1592    }
1593
1594    /// Spawns the remote worker on its own OS thread with a current-thread runtime
1595    /// (the push receive is blocking), connecting IN to the production listener.
1596    struct WorkerThread {
1597        stop: Arc<std::sync::atomic::AtomicBool>,
1598        handle: Option<std::thread::JoinHandle<()>>,
1599    }
1600
1601    impl WorkerThread {
1602        fn spawn(address: String, config: WorkerConfig, registry: Arc<ActivityRegistry>) -> Self {
1603            let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
1604            let thread_stop = Arc::clone(&stop);
1605            let handle = std::thread::spawn(move || {
1606                let runtime = match tokio::runtime::Builder::new_current_thread()
1607                    .enable_all()
1608                    .build()
1609                {
1610                    Ok(runtime) => runtime,
1611                    Err(error) => {
1612                        eprintln!("worker runtime build failed: {error}");
1613                        return;
1614                    }
1615                };
1616                runtime.block_on(async move {
1617                    let worker = match LiminalActivityWorker::connect(&address, &config, registry) {
1618                        Ok(worker) => worker,
1619                        Err(error) => {
1620                            eprintln!("worker connect failed: {error}");
1621                            return;
1622                        }
1623                    };
1624                    if let Err(error) = worker
1625                        .serve_until(|| thread_stop.load(Ordering::SeqCst))
1626                        .await
1627                    {
1628                        eprintln!("worker serve loop ended with error: {error}");
1629                    }
1630                });
1631            });
1632            Self {
1633                stop,
1634                handle: Some(handle),
1635            }
1636        }
1637
1638        /// Spawn the worker through [`aion_worker::serve_with_redial`] — the entry
1639        /// point every REAL worker uses — so a broken link is survivable.
1640        ///
1641        /// [`Self::spawn`] uses `LiminalActivityWorker::serve_until`, which returns
1642        /// the first transport error by design: a single-connection serve has no
1643        /// survivor to migrate to. That is the right shape for a test whose link
1644        /// never breaks, and the wrong instrument entirely for one whose link is
1645        /// broken on purpose — a worker that dies at the break can only ever show
1646        /// that outstanding work fails, whoever is at fault.
1647        ///
1648        /// The redial driver is SYNCHRONOUS and builds its own current-thread
1649        /// runtime, so it runs on the bare thread rather than inside one.
1650        fn spawn_redialing(
1651            address: String,
1652            config: WorkerConfig,
1653            registry: Arc<ActivityRegistry>,
1654            timing: aion_worker::RedialTiming,
1655        ) -> Self {
1656            let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
1657            let thread_stop = Arc::clone(&stop);
1658            let handle = std::thread::spawn(move || {
1659                if let Err(error) = aion_worker::serve_with_redial(
1660                    vec![address],
1661                    &config,
1662                    &registry,
1663                    timing,
1664                    &thread_stop,
1665                    None,
1666                    || {},
1667                ) {
1668                    eprintln!("redialing worker ended with error: {error}");
1669                }
1670            });
1671            Self {
1672                stop,
1673                handle: Some(handle),
1674            }
1675        }
1676
1677        fn stop(mut self) {
1678            self.stop.store(true, Ordering::SeqCst);
1679            if let Some(handle) = self.handle.take() {
1680                handle.join().ok();
1681            }
1682        }
1683    }
1684
1685    fn count_completed(history: &[Event]) -> usize {
1686        history
1687            .iter()
1688            .filter(|event| matches!(event, Event::ActivityCompleted { .. }))
1689            .count()
1690    }
1691
1692    fn count_workflow_completed(history: &[Event]) -> usize {
1693        history
1694            .iter()
1695            .filter(|event| matches!(event, Event::WorkflowCompleted { .. }))
1696            .count()
1697    }
1698
1699    async fn wait_for_history<F>(
1700        store: &dyn aion_store::ReadableEventStore,
1701        workflow_id: &aion_core::WorkflowId,
1702        description: &str,
1703        predicate: F,
1704    ) -> Result<Vec<Event>, TestError>
1705    where
1706        F: Fn(&[Event]) -> bool,
1707    {
1708        let deadline = Instant::now() + POLL_DEADLINE;
1709        loop {
1710            let history = store.read_history(workflow_id).await.map_err(test_error)?;
1711            if predicate(&history) {
1712                return Ok(history);
1713            }
1714            if Instant::now() > deadline {
1715                return Err(test_error(format!(
1716                    "timed out waiting for {description}: {history:#?}"
1717                )));
1718            }
1719            tokio::time::sleep(Duration::from_millis(25)).await;
1720        }
1721    }
1722
1723    /// Start the loaded `collect_four` workflow over the REAL HTTP transport.
1724    async fn start_over_http(router: &axum::Router) -> Result<aion_core::WorkflowId, TestError> {
1725        let build_request = || -> Result<Request<body::Body>, TestError> {
1726            Request::builder()
1727                .uri("/workflows/start")
1728                .method("POST")
1729                .header("content-type", "application/json")
1730                .header("x-aion-subject", "ci")
1731                .header("x-aion-namespaces", NAMESPACE)
1732                .body(body::Body::from(
1733                    serde_json::to_vec(&json!({
1734                        "namespace": NAMESPACE,
1735                        "workflow_type": OUTBOX_MODULE,
1736                        "input": { "fixture": "input" },
1737                    }))
1738                    .map_err(test_error)?,
1739                ))
1740                .map_err(test_error)
1741        };
1742        let response = router
1743            .clone()
1744            .oneshot(build_request()?)
1745            .await
1746            .map_err(test_error)?;
1747        let status = response.status();
1748        let bytes = body::to_bytes(response.into_body(), usize::MAX)
1749            .await
1750            .map_err(test_error)?
1751            .to_vec();
1752        if status != StatusCode::OK {
1753            return Err(test_error(format!(
1754                "workflow start over HTTP must succeed, got {status}: {}",
1755                String::from_utf8_lossy(&bytes)
1756            )));
1757        }
1758        let body: serde_json::Value = serde_json::from_slice(&bytes).map_err(test_error)?;
1759        // The HTTP wire contract (`clean_dtos::StartWorkflowResponse`) serializes
1760        // `workflow_id` as a plain UUID string, not a nested `{ uuid }` object.
1761        let workflow_id = body["workflow_id"]
1762            .as_str()
1763            .ok_or_else(|| test_error("start response missing workflow id"))?
1764            .parse::<uuid::Uuid>()
1765            .map_err(test_error)?;
1766        Ok(aion_core::WorkflowId::new(workflow_id))
1767    }
1768
1769    /// How long a freshly connected worker needs before the dispatch path may
1770    /// select it, DERIVED from the same two facts the server derives it from.
1771    ///
1772    /// A worker is dispatch-ineligible until it serves an OPENING PROBATION:
1773    /// [`Reachability::is_proved`] requires `DISPATCH_PROBATION_PINGS` consecutive
1774    /// answered liveness pings, at the probe's cadence of
1775    /// [`sweep_interval`](crate::worker::sweep_interval)`(heartbeat_window)`. The
1776    /// constant's own documentation states the cost — *"at the probe's cadence a
1777    /// fresh worker is undispatchable for K cadences while its first dispatches
1778    /// park"* — so this is designed behaviour a test must wait out, not a delay to
1779    /// be shortened.
1780    ///
1781    /// One extra cadence is allowed because the first round lands at an arbitrary
1782    /// offset inside the first interval: the worker connects between rounds, so it
1783    /// can miss up to one whole cadence before its first answer is even counted.
1784    ///
1785    /// # Why this is not a raised timeout
1786    ///
1787    /// It was 5 seconds, fixed, and that is how this test became one of four
1788    /// documented carriers of a load-sensitive flake
1789    /// (`gate-logs/lock-race-attribution/VERDICT.md`). The mechanism, measured:
1790    /// `dispatch_ineligible` starts EMPTY and `select_worker` filters only against
1791    /// what the probe has published, so a run in which **no probe round lands
1792    /// inside the window** selects the worker immediately and passes, while a run
1793    /// in which one does correctly withholds it for ~2 cadences and fails. On the
1794    /// default 30s window that is 7.5s per cadence against a 5s wait.
1795    ///
1796    /// 🔴 The passing runs were the WRONG ones. They dispatched to a worker that
1797    /// had not served its probation — a path production does not permit, because
1798    /// production parks those dispatches. Waiting for genuine eligibility makes
1799    /// this test MORE production-shaped, not more lenient, and that is the reason
1800    /// to do it. Raising a bound until a flake stops is how a liveness bug gets
1801    /// buried; deriving the bound from the mechanism that sets it is not the same
1802    /// act, and the register warns about the first for good reason.
1803    fn eligibility_patience(config: &ServerConfig) -> Duration {
1804        let cadence = crate::worker::sweep_interval(config.worker.heartbeat_window);
1805        cadence * (crate::worker::heartbeat::DISPATCH_PROBATION_PINGS + 1)
1806    }
1807
1808    /// Wait until the worker's in-band registration lands in the SAME registry the
1809    /// dispatch path selects from, with every fan-out activity type eligible.
1810    ///
1811    /// On the deadline this reports the state that DISCRIMINATES the worlds a
1812    /// missed registration can be in, because the bare sentence it replaced —
1813    /// "worker never registered in-band for the pool" — is equally true in at
1814    /// least three of them, and they want different fixes:
1815    ///
1816    /// 1. the liminal listener never bound, so nothing could dial in;
1817    /// 2. the worker never connected, or died dialling;
1818    /// 3. it connected and registration was merely slow;
1819    /// 4. it connected, registered correctly, and the SELECTOR refused it anyway —
1820    ///    because the liveness probe published it as unreachable, or because it is
1821    ///    not indexed for the activity type it advertises.
1822    ///
1823    /// The fourth was not in the first version of this report, and it is the world
1824    /// a real occurrence turned out to be in: the listener was bound, a worker was
1825    /// registered under the right namespace and queue advertising all four activity
1826    /// types, and every `select_worker` still returned nothing. A report that
1827    /// cannot separate "not registered" from "registered and refused" names the
1828    /// wrong half of the system.
1829    ///
1830    /// That is not a hypothetical distinction here. This module's e2e is one of
1831    /// four documented carriers of a load-sensitive flake
1832    /// (`gate-logs/lock-race-attribution/VERDICT.md`), it fails through THIS wait,
1833    /// and the reason the carrier has never been explained is that the failure
1834    /// named the fact and withheld the cause.
1835    async fn wait_for_registration(
1836        registry: &crate::worker::ConnectedWorkerRegistry,
1837        heartbeat: &crate::worker::HeartbeatTracker,
1838        listen_address: SocketAddr,
1839        patience: Duration,
1840    ) -> Result<(), TestError> {
1841        let deadline = Instant::now() + patience;
1842        loop {
1843            let now = Instant::now();
1844            let mut ready = true;
1845            for activity_type in FAN_ACTIVITY_TYPES {
1846                let Some(worker) = registry
1847                    .select_worker(NAMESPACE, TASK_QUEUE, activity_type, None)
1848                    .map_err(test_error)?
1849                else {
1850                    ready = false;
1851                    break;
1852                };
1853                if !heartbeat
1854                    .is_dispatch_reachable(worker.id(), now)
1855                    .map_err(test_error)?
1856                {
1857                    ready = false;
1858                    break;
1859                }
1860            }
1861            if ready {
1862                return Ok(());
1863            }
1864            if Instant::now() > deadline {
1865                return Err(test_error(format!(
1866                    "worker never registered in-band for the pool within {patience:?}{}",
1867                    registration_diagnosis(registry, listen_address)
1868                )));
1869            }
1870            tokio::time::sleep(Duration::from_millis(10)).await;
1871        }
1872    }
1873
1874    /// The discriminator behind [`wait_for_registration`]'s failure: enough of the
1875    /// world to tell those three apart, gathered at the moment of the failure.
1876    fn registration_diagnosis(
1877        registry: &crate::worker::ConnectedWorkerRegistry,
1878        listen_address: SocketAddr,
1879    ) -> String {
1880        let mut lines = vec![String::from("--- registration diagnosis ---")];
1881        // World 1, PROBED rather than assumed. The port was reserved by binding a
1882        // listener and dropping it, so losing the race for it is a real
1883        // possibility rather than a theoretical one, and it is indistinguishable
1884        // from every other failure unless something asks.
1885        lines.push(
1886            match std::net::TcpStream::connect_timeout(&listen_address, Duration::from_millis(500))
1887            {
1888                Ok(stream) => {
1889                    drop(stream);
1890                    format!("listener {listen_address}: ACCEPTS — the port is bound and dialable")
1891                }
1892                Err(error) => format!(
1893                    "listener {listen_address}: NOT connectable ({error}) — nothing could have \
1894                     registered, so this is not a timing problem"
1895                ),
1896            },
1897        );
1898        // Worlds 2 and 3: did any worker arrive at all, and if one did, what does
1899        // the registry hold for it against what the dispatch path asks of it? A
1900        // worker present under a different pool or advertising different activity
1901        // types is a contract mismatch wearing a timeout's clothes.
1902        match registry.all_workers() {
1903            Err(error) => lines.push(format!("registry: UNREADABLE ({error})")),
1904            Ok(workers) if workers.is_empty() => lines.push(String::from(
1905                "registry: EMPTY — no worker of any pool registered, so no connection ever \
1906                 completed an in-band registration",
1907            )),
1908            Ok(workers) => {
1909                lines.push(format!("registry: {} worker(s) registered", workers.len()));
1910                for worker in &workers {
1911                    lines.push(format!(
1912                        "  id={:?} namespaces={:?} task_queue={:?} node={:?} types={:?}",
1913                        worker.id(),
1914                        worker.namespaces(),
1915                        worker.task_queue(),
1916                        worker.node(),
1917                        worker.activity_types()
1918                    ));
1919                }
1920            }
1921        }
1922        lines.push(format!(
1923            "asked of it: namespace={NAMESPACE:?} task_queue={TASK_QUEUE:?}"
1924        ));
1925        // The liveness probe's reachability verdict. `select_worker` skips every
1926        // worker in this set, so a registered, correctly-advertised worker that is
1927        // listed here is refused for a reason nothing else in this report shows.
1928        lines.push(match registry.dispatch_ineligible() {
1929            Ok(ineligible) if ineligible.is_empty() => {
1930                String::from("dispatch-ineligible: none — reachability is not refusing anyone")
1931            }
1932            Ok(ineligible) => format!(
1933                "dispatch-ineligible: {ineligible:?} — the liveness probe has published these \
1934                 as unreachable and select_worker skips them"
1935            ),
1936            Err(error) => format!("dispatch-ineligible: UNREADABLE ({error})"),
1937        });
1938        // Which of the four the selector could not satisfy, and — the part that
1939        // discriminates — the pool census beside each refusal.
1940        //
1941        // `select_worker` filters on THREE things: the activity index for
1942        // `(namespace, task_queue) + activity_type`, the node pin, and the
1943        // dispatch-ineligible set. The census counts the first two and does NOT
1944        // apply the third, so the pair of answers separates the remaining worlds
1945        // that a registry dump alone leaves fused:
1946        //
1947        // - census serves it, selector refuses  ⇒ REACHABILITY, not registration;
1948        // - census serves 0 for the activity    ⇒ the worker is in the pool but not
1949        //   indexed for this activity type;
1950        // - census serves 0 for the pool        ⇒ it is not in this pool at all,
1951        //   whatever `all_workers` shows.
1952        //
1953        // Written after the bare registry dump above failed to close a real case:
1954        // it proved the listener was bound and a worker with all four activity
1955        // types was registered, and still could not say why every selection
1956        // returned nothing.
1957        for activity_type in FAN_ACTIVITY_TYPES {
1958            let outcome = match registry.select_worker(NAMESPACE, TASK_QUEUE, activity_type, None) {
1959                Ok(Some(handle)) => format!("worker {:?}", handle.id()),
1960                Ok(None) => String::from("NO worker"),
1961                Err(error) => format!("error: {error}"),
1962            };
1963            let census = match registry.pool_census(NAMESPACE, TASK_QUEUE, activity_type, None) {
1964                Ok(census) => format!(
1965                    "in_pool={} serving_activity={} compatible={} last_compatible_age={:?}",
1966                    census.workers_in_pool,
1967                    census.workers_serving_activity,
1968                    census.compatible_workers,
1969                    census.last_compatible_poller_age
1970                ),
1971                Err(error) => format!("census UNREADABLE ({error})"),
1972            };
1973            lines.push(format!(
1974                "select_worker({activity_type}) -> {outcome}  [census: {census}]"
1975            ));
1976        }
1977        format!("\n  {}", lines.join("\n  "))
1978    }
1979
1980    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1981    async fn production_boot_dispatches_executes_and_records_over_liminal() -> Result<(), TestError>
1982    {
1983        let dir = crate::test_support::private_tempdir().map_err(test_error)?;
1984        let db_path = dir.path().join("aion.db");
1985        let package_path = write_package_archive(dir.path())?;
1986        // The production path binds the CONFIGURED listen address, so commit to a
1987        // concrete reserved loopback port the worker can also dial.
1988        let listen_address = reserve_loopback_port()?;
1989
1990        // (A) Build a real ServerState through the production boot path
1991        // (ServerState::build over a haematite ServerConfig): outbox enabled,
1992        // transport = liminal, the listen address set, collect_four loaded. This
1993        // shares the haematite leaf as the dispatcher's outbox store (the real boot
1994        // store seam) and installs the production ServerOutboxDeliveryCallback over
1995        // the live engine (gated on outbox.enabled).
1996        let config = server_config(&db_path, package_path, listen_address);
1997        let outbox_config = config.outbox.clone();
1998        // Captured before `build` consumes the config: the wait below is derived
1999        // from the very window this server is about to run its liveness probe on.
2000        let patience = eligibility_patience(&config);
2001        let state = ServerState::build(config).await.map_err(test_error)?;
2002
2003        // (B) Drive the EXACT production commissioning function run_server calls:
2004        // it hosts the liminal listener, builds RegistryLiminalDispatch over the
2005        // shared registry + engine callback, and spawns the real OutboxDispatcher.
2006        // Hold the returned listener guard for the test's lifetime, exactly as
2007        // run_server holds it.
2008        let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
2009        // Own-all, generous-default backpressure (single-node e2e): fraction 1 and
2010        // the platform default, so the ceiling never engages — the claim behaves
2011        // exactly as before, proving the production path is byte-identical on default.
2012        let backpressure_settings = BackpressureSettings {
2013            platform_default: crate::config::DEFAULT_MAX_IN_FLIGHT_ACTIVITIES,
2014            fraction: crate::worker::OwnedShardFraction::own_all(),
2015        };
2016        let listener_guard = maybe_spawn_outbox_dispatcher(
2017            &state,
2018            &outbox_config,
2019            false,
2020            backpressure_settings,
2021            &shutdown_rx,
2022            "set outbox.liminal_listen_address in the test config",
2023        )
2024        .map_err(test_error)?;
2025
2026        // (C) A REAL remote worker connects IN to the production listener and
2027        // self-registers in-band for the fixture's pool.
2028        let executions = Arc::new(AtomicUsize::new(0));
2029        let worker = WorkerThread::spawn(
2030            listen_address.to_string(),
2031            worker_config()?,
2032            worker_registry(&executions)?,
2033        );
2034
2035        // Wait until the in-band registration landed in the SAME registry the
2036        // dispatch path selects from (every fan-out activity type is eligible).
2037        let registry = state.worker_registry().clone();
2038        if let Err(error) = wait_for_registration(
2039            &registry,
2040            state.heartbeat_tracker(),
2041            listen_address,
2042            patience,
2043        )
2044        .await
2045        {
2046            worker.stop();
2047            return Err(error);
2048        }
2049
2050        // (D) Start collect_four over the REAL HTTP transport: the engine stages
2051        // four pending outbox rows; the production-wired dispatcher claims and
2052        // pushes each to the worker.
2053        let router = http_router(state.clone()).map_err(test_error)?;
2054        let workflow_id = start_over_http(&router).await?;
2055
2056        // (E) THE PROOF: the worker executed all four activities AND every terminal
2057        // was recorded through the production engine callback (record_fan_out_completion)
2058        // — four ActivityCompleted + one WorkflowCompleted in durable history. This
2059        // is the full round-trip the retired stub never achieved.
2060        let reader = state.engine().map_err(test_error)?.store();
2061        let settled =
2062            wait_for_history(reader.as_ref(), &workflow_id, "fan-out settled", |events| {
2063                count_completed(events) == FAN_OUT && count_workflow_completed(events) == 1
2064            })
2065            .await?;
2066        assert_eq!(
2067            count_completed(&settled),
2068            FAN_OUT,
2069            "every fan-out member must record a terminal through the production callback"
2070        );
2071        assert_eq!(
2072            count_workflow_completed(&settled),
2073            1,
2074            "the workflow must complete exactly once"
2075        );
2076        assert_eq!(
2077            executions.load(Ordering::SeqCst),
2078            FAN_OUT,
2079            "the remote worker must have executed every pushed dispatch exactly once"
2080        );
2081
2082        // Teardown: stop the dispatcher + worker, drop the listener guard (its Drop
2083        // stops the accept worker), shut the engine down so durable appends finish.
2084        shutdown_tx.send(true).ok();
2085        worker.stop();
2086        drop(listener_guard);
2087        state.shutdown().map_err(test_error)?;
2088        Ok(())
2089    }
2090
2091    /// One dispatch as the WORKER saw it: the identity the server sent it under,
2092    /// and when it arrived.
2093    #[derive(Clone, Debug)]
2094    struct SeenDispatch {
2095        activity_type: String,
2096        activity_id: String,
2097        attempt: u32,
2098        at: Instant,
2099    }
2100
2101    /// A loopback TCP relay the test can BREAK, sitting between the worker and the
2102    /// production liminal listener.
2103    ///
2104    /// The worker dials this instead of the listener, so the test owns a socket it
2105    /// can shut from the outside. That is the only way to make a REAL
2106    /// [`LiminalActivityWorker`] lose its connection mid-flight without reaching
2107    /// inside either the worker or the server — and a link broken from the inside
2108    /// would be a different experiment, because the code under test would be the
2109    /// code doing the breaking.
2110    ///
2111    /// # Why this is not the relay in `tests/dead_man_switch_e2e.rs`
2112    ///
2113    /// That file has `WedgeableRelay`, which can both wedge and sever, and this is
2114    /// deliberately not it. The two cannot be one, for a structural reason rather
2115    /// than a matter of taste: an integration test links this crate as an ordinary
2116    /// dependency, so it can see neither `#[cfg(test)] pub(crate) mod test_support`
2117    /// nor the private `maybe_spawn_outbox_dispatcher` this harness is built on,
2118    /// and `src/` cannot see `tests/`. Sharing one instrument would mean exporting
2119    /// a public, feature-gated test surface from a production crate.
2120    ///
2121    /// So the split is stated rather than hidden, and this half is a strict subset:
2122    /// it only severs. Wedging — which leaves both sockets open and merely discards
2123    /// bytes, so writes keep succeeding into the kernel buffer — is a DIFFERENT
2124    /// instrument answering a different question. #69 is about a broken link, not
2125    /// a silent one.
2126    struct SeverableRelay {
2127        address: SocketAddr,
2128        /// Every relayed socket, held so [`Self::sever`] can break them.
2129        sockets: Arc<std::sync::Mutex<Vec<std::net::TcpStream>>>,
2130        stop: Arc<std::sync::atomic::AtomicBool>,
2131        handle: Option<std::thread::JoinHandle<()>>,
2132    }
2133
2134    impl SeverableRelay {
2135        /// Bind a loopback port and relay every accepted connection to `upstream`.
2136        fn spawn(upstream: SocketAddr) -> Result<Self, TestError> {
2137            let listener = std::net::TcpListener::bind("127.0.0.1:0").map_err(test_error)?;
2138            let address = listener.local_addr().map_err(test_error)?;
2139            // Non-blocking accept so the relay can be shut down deterministically
2140            // rather than by parking a thread in `accept` until something happens
2141            // to connect. Accepted sockets are put back into blocking mode
2142            // explicitly: on this platform they would otherwise inherit the flag
2143            // and every pump would spin on `WouldBlock`.
2144            listener.set_nonblocking(true).map_err(test_error)?;
2145            let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
2146            let sockets: Arc<std::sync::Mutex<Vec<std::net::TcpStream>>> =
2147                Arc::new(std::sync::Mutex::new(Vec::new()));
2148            let accept_stop = Arc::clone(&stop);
2149            let accept_sockets = Arc::clone(&sockets);
2150            let handle = std::thread::spawn(move || {
2151                while !accept_stop.load(Ordering::SeqCst) {
2152                    match listener.accept() {
2153                        Ok((downstream, _)) => {
2154                            if let Err(error) =
2155                                Self::relay_one(&downstream, upstream, &accept_sockets)
2156                            {
2157                                // The worker redials, so a connection this relay
2158                                // fails to carry surfaces as a slower recovery
2159                                // rather than as a wrong answer — but silence here
2160                                // would make that indistinguishable from the
2161                                // server never pushing, which is exactly the
2162                                // confusion this pin exists to resolve.
2163                                eprintln!("relay could not carry a connection: {error}");
2164                            }
2165                        }
2166                        Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2167                            std::thread::sleep(Duration::from_millis(2));
2168                        }
2169                        Err(error) => {
2170                            eprintln!("relay accept failed: {error}");
2171                            return;
2172                        }
2173                    }
2174                }
2175            });
2176            Ok(Self {
2177                address,
2178                sockets,
2179                stop,
2180                handle: Some(handle),
2181            })
2182        }
2183
2184        /// Dial upstream for one accepted connection and pump both directions.
2185        fn relay_one(
2186            downstream: &std::net::TcpStream,
2187            upstream: SocketAddr,
2188            sockets: &Arc<std::sync::Mutex<Vec<std::net::TcpStream>>>,
2189        ) -> Result<(), TestError> {
2190            downstream.set_nonblocking(false).map_err(test_error)?;
2191            let up = std::net::TcpStream::connect(upstream).map_err(test_error)?;
2192            let down_read = downstream.try_clone().map_err(test_error)?;
2193            let down_write = downstream.try_clone().map_err(test_error)?;
2194            let up_read = up.try_clone().map_err(test_error)?;
2195            let up_write = up.try_clone().map_err(test_error)?;
2196            let held = downstream.try_clone().map_err(test_error)?;
2197            let mut parked = sockets
2198                .lock()
2199                .map_err(|_| test_error("relay socket register poisoned"))?;
2200            parked.push(held);
2201            parked.push(up);
2202            drop(parked);
2203            for (from, to) in [(down_read, up_write), (up_read, down_write)] {
2204                std::thread::spawn(move || Self::pump(from, to));
2205            }
2206            Ok(())
2207        }
2208
2209        /// Copy one direction until the connection ends.
2210        ///
2211        /// A read or write error here IS the severed link in the expected case, and
2212        /// in every case it means the peer this pump exists to serve is gone: there
2213        /// is no party left to propagate to, so ending the pump is the handling,
2214        /// not an omission of it.
2215        fn pump(mut from: std::net::TcpStream, mut to: std::net::TcpStream) {
2216            use std::io::{Read, Write};
2217            let mut buffer = [0_u8; 8192];
2218            loop {
2219                match from.read(&mut buffer) {
2220                    Ok(0) | Err(_) => return,
2221                    Ok(read) => {
2222                        if to.write_all(&buffer[..read]).is_err() {
2223                            return;
2224                        }
2225                    }
2226                }
2227            }
2228        }
2229
2230        const fn address(&self) -> SocketAddr {
2231            self.address
2232        }
2233
2234        /// BREAK every relayed socket, and report how many were broken.
2235        ///
2236        /// The count is returned, and asserted non-zero by the caller, so that a
2237        /// sever which severed nothing can never masquerade as a measurement — the
2238        /// pin would otherwise pass by never having run its own experiment.
2239        fn sever(&self) -> Result<usize, TestError> {
2240            let mut parked = self
2241                .sockets
2242                .lock()
2243                .map_err(|_| test_error("relay socket register poisoned"))?;
2244            let mut severed = 0;
2245            for socket in parked.iter() {
2246                if socket.shutdown(std::net::Shutdown::Both).is_ok() {
2247                    severed += 1;
2248                }
2249            }
2250            parked.clear();
2251            Ok(severed)
2252        }
2253
2254        fn shutdown(mut self) {
2255            self.stop.store(true, Ordering::SeqCst);
2256            if let Some(handle) = self.handle.take() {
2257                handle.join().ok();
2258            }
2259        }
2260    }
2261
2262    /// Registry for the reconnect pin: every dispatch is RECORDED with the identity
2263    /// the server sent it under, and [`HELD_ACTIVITY_TYPE`]'s FIRST dispatch holds
2264    /// — the work is finished, its reply is not yet on the wire — until released.
2265    ///
2266    /// Only the first is held. A blanket hold would stall the re-delivery this pin
2267    /// exists to observe, and the pin would then measure its own instrument.
2268    fn recording_registry(
2269        seen: &Arc<std::sync::Mutex<Vec<SeenDispatch>>>,
2270        release: &Arc<std::sync::atomic::AtomicBool>,
2271    ) -> Result<Arc<ActivityRegistry>, TestError> {
2272        let mut registry = ActivityRegistry::new();
2273        for activity_type in FAN_ACTIVITY_TYPES {
2274            let seen = Arc::clone(seen);
2275            let release = Arc::clone(release);
2276            let arrivals = Arc::new(AtomicUsize::new(0));
2277            registry = registry
2278                .register_activity_with_contract(
2279                    activity_type,
2280                    move |_input: FanInput, context: &aion_worker::ActivityContext| {
2281                        let seen = Arc::clone(&seen);
2282                        let release = Arc::clone(&release);
2283                        let arrivals = Arc::clone(&arrivals);
2284                        let record = SeenDispatch {
2285                            activity_type: activity_type.to_owned(),
2286                            activity_id: context.activity_id().to_string(),
2287                            attempt: context.attempt(),
2288                            at: Instant::now(),
2289                        };
2290                        Box::pin(async move {
2291                            // Recorded BEFORE the hold: a dispatch that arrives and
2292                            // is never answered must still be visible, or the pin
2293                            // cannot tell "never re-delivered" from "re-delivered
2294                            // and lost again".
2295                            match seen.lock() {
2296                                Ok(mut log) => log.push(record),
2297                                Err(_) => {
2298                                    return Err(aion_worker::ActivityFailure::terminal(
2299                                        "the pin's dispatch log is poisoned, so this run can \
2300                                         observe nothing — failing loudly rather than \
2301                                         returning a result no assertion could trust",
2302                                    ));
2303                                }
2304                            }
2305                            let first = arrivals.fetch_add(1, Ordering::SeqCst) == 0;
2306                            if activity_type == HELD_ACTIVITY_TYPE && first {
2307                                while !release.load(Ordering::SeqCst) {
2308                                    tokio::time::sleep(Duration::from_millis(5)).await;
2309                                }
2310                            }
2311                            Ok(activity_type.to_owned())
2312                        })
2313                    },
2314                )
2315                .map_err(test_error)?;
2316        }
2317        Ok(Arc::new(registry))
2318    }
2319
2320    fn dispatches_of(
2321        seen: &Arc<std::sync::Mutex<Vec<SeenDispatch>>>,
2322        activity_type: &str,
2323    ) -> Result<Vec<SeenDispatch>, TestError> {
2324        let log = seen
2325            .lock()
2326            .map_err(|_| test_error("the pin's dispatch log is poisoned"))?;
2327        Ok(log
2328            .iter()
2329            .filter(|record| record.activity_type == activity_type)
2330            .cloned()
2331            .collect())
2332    }
2333
2334    fn dispatch_log(seen: &Arc<std::sync::Mutex<Vec<SeenDispatch>>>) -> String {
2335        match seen.lock() {
2336            Ok(log) => format!("{:#?}", *log),
2337            Err(_) => String::from("<poisoned>"),
2338        }
2339    }
2340
2341    /// aion #69 at the ENGINE level: what the system DOES after an activity's
2342    /// completion is lost to a broken link.
2343    ///
2344    /// # What this measures, and why the transport-level pin cannot
2345    ///
2346    /// #69's existing red-first pin lives on its fix branch rather than here (it
2347    /// is red on purpose and lands with the fix), and it establishes that the
2348    /// completion is DISCARDED: the server abandons the correlated reply-wait the
2349    /// moment the delivering connection closes. It drives `WorkerDelivery`
2350    /// directly, with no engine, no store and no workflow behind it, so it can say
2351    /// nothing at all about what happens NEXT. That gap is the whole severity of
2352    /// #69: "the work is repeated once" and "the work is lost" are priced very
2353    /// differently, and nothing in-tree could tell them apart.
2354    ///
2355    /// So this pin observes four things, and asserts only what must hold in EVERY
2356    /// world — including the one a #69 fix creates:
2357    ///
2358    /// - **O4, ASSERTED** — the workflow still reaches a recorded terminal. This is
2359    ///   the invariant: a broken link must not cost the workflow. It is not a weak
2360    ///   assertion, because `collect_four` consumes all four members, so the
2361    ///   workflow cannot complete while any member's work is missing;
2362    /// - **O1, REPORTED** — whether the held activity is dispatched a SECOND time.
2363    ///   This is the MECHANISM, and the mechanism is what a fix changes: a fix that
2364    ///   carries the completion across the reconnect would produce NO re-delivery,
2365    ///   and a pin asserting one would read that fix as a regression.
2366    ///   regression;
2367    /// - **O2, asserted CONDITIONALLY** — if a re-delivery happened it must carry
2368    ///   the activity's OWN identity. That is what makes the finished work
2369    ///   discarded rather than recovered; a re-delivery under a different identity
2370    ///   is a different defect and must not pass quietly;
2371    /// - **O3, REPORTED** — the elapsed time from the break to the re-delivery, as
2372    ///   a NUMBER asserted against nothing. No threshold is invented here: the
2373    ///   right bound is a conversation to have with the measurement in hand.
2374    ///
2375    /// ⚠️ **O3 is recovery LATENCY, and latency is not COST.** The number is
2376    /// measured on a fixture activity that is a pure `String -> String`, so its
2377    /// repeat costs microseconds. The real cost of a repeat is the repeated
2378    /// activity's own runtime plus its repeated SIDE EFFECTS, which this pin does
2379    /// not measure and structurally cannot: #69's own exhibit was an *agent*
2380    /// activity, whose repeat is minutes of compute and files written twice.
2381    /// Quote the finding — *repeated work, not lost work, one repeat per in-flight
2382    /// activity* — rather than the milliseconds, which carry their premise (a
2383    /// trivial activity) only for as long as someone remembers to attach it.
2384    ///
2385    /// The settle-wait below is bounded by [`POLL_DEADLINE`], so this pin cannot
2386    /// hang; but that bound is ~100x the observed recovery, so it is a liveness
2387    /// guard and NOT a latency guard. A large latency regression would still pass
2388    /// here, reported in O3 and asserted by nothing — deliberately, because the
2389    /// correct bound is not derivable from the samples taken so far.
2390    ///
2391    /// Executions are REPORTED, never asserted equal to the fan-out. A transport
2392    /// that can lose a reply gives at-least-once delivery, so the sibling test's
2393    /// `executions == FAN_OUT` is the wrong shape here and must not be copied
2394    /// across.
2395    ///
2396    /// # The world this models
2397    ///
2398    /// One server process with its transport-loss ledger live in memory, a worker
2399    /// that redials the SAME address, and a SINGLE loss — well inside
2400    /// `TRANSPORT_LOSS_BUDGET_WINDOWS`. It is NOT a server restart and NOT budget
2401    /// exhaustion, both of which are different worlds with different recoveries.
2402    /// The re-delivery this venue can produce is the outbox dispatcher's re-claim
2403    /// under the `max_attempts`/backoff this test's config sets, not the #266
2404    /// recovery replay — which is what gives O3's number a slot to mean anything in.
2405    ///
2406    /// The relay's own accept poll (2ms) sits inside the measured elapsed.
2407    ///
2408    /// ⚠️ This pin shares a venue with
2409    /// `production_boot_dispatches_executes_and_records_over_liminal`, one of four
2410    /// documented carriers of a load-sensitive flake — 2/24 on a base that
2411    /// predates it (`gate-logs/lock-race-attribution/VERDICT.md`). It inherits that
2412    /// sensitivity, and a red here should be read against that register first.
2413    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
2414    async fn a_completion_lost_to_a_severed_link_is_re_dispatched_and_the_workflow_settles()
2415    -> Result<(), TestError> {
2416        let dir = crate::test_support::private_tempdir().map_err(test_error)?;
2417        let db_path = dir.path().join("aion.db");
2418        let package_path = write_package_archive(dir.path())?;
2419        let listen_address = reserve_loopback_port()?;
2420
2421        let config = server_config(&db_path, package_path, listen_address);
2422        let outbox_config = config.outbox.clone();
2423        // Captured before `build` consumes the config: the wait below is derived
2424        // from the very window this server is about to run its liveness probe on.
2425        let patience = eligibility_patience(&config);
2426        let state = ServerState::build(config).await.map_err(test_error)?;
2427        let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
2428        let backpressure_settings = BackpressureSettings {
2429            platform_default: crate::config::DEFAULT_MAX_IN_FLIGHT_ACTIVITIES,
2430            fraction: crate::worker::OwnedShardFraction::own_all(),
2431        };
2432        let listener_guard = maybe_spawn_outbox_dispatcher(
2433            &state,
2434            &outbox_config,
2435            false,
2436            backpressure_settings,
2437            &shutdown_rx,
2438            "set outbox.liminal_listen_address in the test config",
2439        )
2440        .map_err(test_error)?;
2441
2442        // The worker dials the RELAY, which carries it to the production listener.
2443        let relay = SeverableRelay::spawn(listen_address)?;
2444        let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
2445        let release = Arc::new(std::sync::atomic::AtomicBool::new(false));
2446        // The redial timings are the ones this module's `worker_config` already
2447        // declares, read off it rather than re-chosen here: a reconnect pin that
2448        // picked its own recovery timings would be measuring a world of its own.
2449        let config = worker_config()?;
2450        let timing = aion_worker::RedialTiming::new(
2451            config.reconnect.initial_backoff,
2452            config.reconnect.max_backoff,
2453        );
2454        let worker = WorkerThread::spawn_redialing(
2455            relay.address().to_string(),
2456            config,
2457            recording_registry(&seen, &release)?,
2458            timing,
2459        );
2460
2461        let outcome =
2462            observe_reconnect(&state, &relay, &seen, &release, listen_address, patience).await;
2463
2464        // Teardown runs on EVERY path, including a failing one: a leaked worker
2465        // thread or listener poisons whatever runs next, and this venue is already
2466        // load-sensitive enough without the pin adding to it.
2467        shutdown_tx.send(true).ok();
2468        release.store(true, Ordering::SeqCst);
2469        worker.stop();
2470        relay.shutdown();
2471        drop(listener_guard);
2472        state.shutdown().map_err(test_error)?;
2473        outcome
2474    }
2475
2476    /// The measurement behind
2477    /// [`a_completion_lost_to_a_severed_link_is_re_dispatched_and_the_workflow_settles`],
2478    /// split out so its many early returns cannot skip the harness teardown.
2479    /// Wait until the held member is dispatched and holding — the moment the link
2480    /// can be broken — and report how many of its siblings had already settled.
2481    ///
2482    /// The split at the break is REPORTED, never required. An earlier draft
2483    /// demanded that the other three settle first, for a single-variable
2484    /// experiment. Measured across runs it simply varies: the four pushes land
2485    /// within microseconds of each other and which records a terminal first is a
2486    /// race, so requiring a particular split would fail the pin for a reason that
2487    /// has nothing to do with what it measures.
2488    async fn await_held_dispatch(
2489        reader: &dyn aion_store::ReadableEventStore,
2490        workflow_id: &aion_core::WorkflowId,
2491        seen: &Arc<std::sync::Mutex<Vec<SeenDispatch>>>,
2492    ) -> Result<(SeenDispatch, usize), TestError> {
2493        let deadline = Instant::now() + POLL_DEADLINE;
2494        loop {
2495            if let Some(first) = dispatches_of(seen, HELD_ACTIVITY_TYPE)?.first() {
2496                let at_the_break = reader.read_history(workflow_id).await.map_err(test_error)?;
2497                return Ok((first.clone(), count_completed(&at_the_break)));
2498            }
2499            if Instant::now() > deadline {
2500                let history = reader.read_history(workflow_id).await.map_err(test_error)?;
2501                return Err(test_error(format!(
2502                    "{HELD_ACTIVITY_TYPE} was never dispatched at all within {POLL_DEADLINE:?}, \
2503                     so there was no held completion to lose and this run measured nothing.\n\
2504                     dispatch log: {}\nhistory: {history:#?}",
2505                    dispatch_log(seen),
2506                )));
2507            }
2508            tokio::time::sleep(Duration::from_millis(25)).await;
2509        }
2510    }
2511
2512    async fn observe_reconnect(
2513        state: &ServerState,
2514        relay: &SeverableRelay,
2515        seen: &Arc<std::sync::Mutex<Vec<SeenDispatch>>>,
2516        release: &Arc<std::sync::atomic::AtomicBool>,
2517        listen_address: SocketAddr,
2518        patience: Duration,
2519    ) -> Result<(), TestError> {
2520        wait_for_registration(
2521            state.worker_registry(),
2522            state.heartbeat_tracker(),
2523            listen_address,
2524            patience,
2525        )
2526        .await?;
2527
2528        let router = http_router(state.clone()).map_err(test_error)?;
2529        let workflow_id = start_over_http(&router).await?;
2530        let reader = state.engine().map_err(test_error)?.store();
2531
2532        let (first, settled_before) =
2533            await_held_dispatch(reader.as_ref(), &workflow_id, seen).await?;
2534
2535        // BREAK the link while the finished work is still holding its reply.
2536        let severed = relay.sever()?;
2537        let severed_at = Instant::now();
2538        if severed == 0 {
2539            return Err(test_error(
2540                "the relay severed NOTHING, so no link was ever broken and this run measured \
2541                 nothing — a pass here would have been an artefact of the instrument",
2542            ));
2543        }
2544        // Release the hold: the worker now writes its reply into a dead socket.
2545        release.store(true, Ordering::SeqCst);
2546
2547        // O4 FIRST, because it is the INVARIANT: a broken link must not cost the
2548        // workflow. Every other observable here describes the MECHANISM by which
2549        // that holds, and the mechanism is exactly what a #69 fix is expected to
2550        // change — so asserting today's mechanism would make the fix read as a
2551        // regression, and would be asserting the enumeration rather than the
2552        // invariant.
2553        //
2554        // O4 is load-bearing rather than weak because `collect_four` CONSUMES all
2555        // four members: the workflow cannot reach a completed terminal while any
2556        // member's work is missing, so "the workflow settled" is not a state that
2557        // silently lost work can also produce.
2558        let settled = wait_for_history(
2559            reader.as_ref(),
2560            &workflow_id,
2561            "the workflow to settle after the severed link",
2562            |events| count_completed(events) == FAN_OUT && count_workflow_completed(events) == 1,
2563        )
2564        .await
2565        .map_err(|error| {
2566            test_error(format!(
2567                "O4 FAILED — the workflow did not settle after the link broke ({severed} \
2568                 socket(s) severed), so the lost completion cost the workflow rather than \
2569                 costing a repeat of the work.\n{error}\ndispatch log: {}",
2570                dispatch_log(seen),
2571            ))
2572        })?;
2573        assert_eq!(
2574            count_completed(&settled),
2575            FAN_OUT,
2576            "every fan-out member must still record a terminal after the link broke"
2577        );
2578        assert_eq!(
2579            count_workflow_completed(&settled),
2580            1,
2581            "the workflow must complete exactly once even though a completion was lost"
2582        );
2583
2584        // O1/O2/O3 — the MECHANISM, reported. O2 is asserted only CONDITIONALLY:
2585        // if a re-delivery happened it must have carried the activity's own
2586        // identity, because a re-delivery under a different identity would be a
2587        // different defect entirely and must not pass quietly. If no re-delivery
2588        // happened, the completion survived the reconnect — which is what a fixed
2589        // #69 looks like, and this pin should report it, not fail on it.
2590        let held = dispatches_of(seen, HELD_ACTIVITY_TYPE)?;
2591        match held.get(1) {
2592            None => println!(
2593                "aion#69 — {HELD_ACTIVITY_TYPE} ({}) was NOT re-dispatched and the workflow \
2594                 still settled, so the held completion survived the break; {settled_before} of \
2595                 {FAN_OUT} members had settled when it broke, {severed} socket(s) severed",
2596                first.activity_id,
2597            ),
2598            Some(second) => {
2599                if second.activity_id != first.activity_id {
2600                    return Err(test_error(format!(
2601                        "O2 FAILED — the re-delivery carried a DIFFERENT activity identity. The \
2602                         first dispatch was {} (attempt {}) and the second was {} (attempt {}), \
2603                         so the work was not re-run under its own identity and #69's framing \
2604                         does not describe what happened here.",
2605                        first.activity_id, first.attempt, second.activity_id, second.attempt,
2606                    )));
2607                }
2608                let recovery = second.at.saturating_duration_since(severed_at);
2609                println!(
2610                    "aion#69 O3 — re-delivery of {} ({}) took {}ms from the link breaking; \
2611                     first attempt {}, second attempt {}; {settled_before} of {FAN_OUT} members \
2612                     had already recorded a terminal when the link broke; {severed} socket(s) \
2613                     severed",
2614                    HELD_ACTIVITY_TYPE,
2615                    first.activity_id,
2616                    recovery.as_millis(),
2617                    first.attempt,
2618                    second.attempt,
2619                );
2620            }
2621        }
2622
2623        let all = seen
2624            .lock()
2625            .map_err(|_| test_error("the pin's dispatch log is poisoned"))?
2626            .len();
2627        println!(
2628            "aion#69 — {all} dispatch(es) served for {FAN_OUT} activities; the transport is \
2629             at-least-once, so the excess is the repeated work a broken link costs"
2630        );
2631        Ok(())
2632    }
2633}