From 937293d351217ab9bb3a46035bd00fbcb2604c71 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 12 Oct 2026 08:16:10 +0200 Subject: [PATCH 001/342] mind-body: the waker, one cycle that reads the brake once, waits out the cadence since the newest Persona turn, and hands its Grant to open_and_launch wake::cycle reads the brake and dial through the one derivation behind read_effective and the unit gate, returns Held before any query, admission or unit call, finds the newest Persona turn of hers (any status) with one descending single-row query, and launches through open_and_launch. The mind-wake binary prints the outcome. control_path no longer echoes the instance it refuses. Does not build the public_surface snapshot yet (the next commit updates it). Co-Authored-By: Claude Sonnet 5.5 --- crates/mind-body/Cargo.toml | 6 +- crates/mind-body/src/bin/mind-wake.rs | 76 +++++++ crates/mind-body/src/control.rs | 2 +- crates/mind-body/src/lib.rs | 3 + crates/mind-body/src/wake.rs | 117 ++++++++++ crates/mind-body/src/wake/tests.rs | 308 ++++++++++++++++++++++++++ crates/mind-body/tests/wake.rs | 51 +++++ 7 files changed, 561 insertions(+), 2 deletions(-) create mode 100644 crates/mind-body/src/bin/mind-wake.rs create mode 100644 crates/mind-body/src/wake.rs create mode 100644 crates/mind-body/src/wake/tests.rs create mode 100644 crates/mind-body/tests/wake.rs diff --git a/crates/mind-body/Cargo.toml b/crates/mind-body/Cargo.toml index 3825871..14da03e 100644 --- a/crates/mind-body/Cargo.toml +++ b/crates/mind-body/Cargo.toml @@ -11,7 +11,7 @@ publish.workspace = true # binary) and the one fail-closed reader every acting organ calls # (`control::read_effective`). It also holds the launch primitive (`launch`, # `queue`): the one way her organs open a run and start its unit. The waker -# joins this crate in cut mind-wake. +# (`wake`, the `mind-wake` binary) is one cycle over them. # # `rust_decimal` is the dial's number type: heat, cadence and the run cap are # money-adjacent and the tests pin exact products, which a float would blur. It @@ -46,3 +46,7 @@ path = "src/lib.rs" [[bin]] name = "mind-control" path = "src/main.rs" + +[[bin]] +name = "mind-wake" +path = "src/bin/mind-wake.rs" diff --git a/crates/mind-body/src/bin/mind-wake.rs b/crates/mind-body/src/bin/mind-wake.rs new file mode 100644 index 0000000..761c086 --- /dev/null +++ b/crates/mind-body/src/bin/mind-wake.rs @@ -0,0 +1,76 @@ +//! `mind-wake --instance [--endpoint rudp://:]`: one waker +//! cycle for one Mind. All behaviour is `mind_body::wake::cycle`; this reads the +//! process's arguments, environment and clock, the things the library never +//! reads, and prints the outcome. It exits 0 for every outcome and 2 when the +//! mind cannot be reached or the arguments are wrong. Without `--endpoint` the +//! daemon is `HUGINN_ENDPOINT`. + +use std::net::SocketAddr; +use std::path::Path; +use std::time::Duration; + +use anyhow::{Result, anyhow, bail}; +use eureka_pipeline::Slug; +use eureka_state::HuginnClient; +use mind_body::control::{CONTROL_ROOT, FileSource, control_path}; +use mind_body::launch::{Ports, SystemClock, SystemdLauncher}; +use mind_body::queue::HuginnMind; +use mind_body::wake::cycle; + +/// How long one call to the mind may take. +const TIMEOUT: Duration = Duration::from_secs(15); + +const USAGE: &str = "usage: mind-wake --instance [--endpoint rudp://:]"; + +/// The instance and the endpoint as given. A flag given twice or an unknown +/// argument is refused without printing it. +fn parse(args: &[String]) -> Result<(Slug, Option)> { + let (mut instance, mut endpoint) = (None, None); + let mut rest = args.iter(); + while let Some(flag) = rest.next() { + let slot = match flag.as_str() { + "--instance" => &mut instance, + "--endpoint" => &mut endpoint, + _ => bail!("{USAGE}"), + }; + let value = rest.next().ok_or_else(|| anyhow!("{USAGE}"))?; + if slot.replace(value.clone()).is_some() { + bail!("{USAGE}"); + } + } + Ok((Slug(instance.ok_or_else(|| anyhow!("{USAGE}"))?), endpoint)) +} + +/// The host a run records: the kernel's name for this machine. +fn host() -> String { + std::fs::read_to_string("/proc/sys/kernel/hostname").map(|name| name.trim().to_string()).ok().filter(|name| !name.is_empty()).unwrap_or_else(|| "unknown".into()) +} + +// The binary is the one place that reads the process: its arguments and the +// daemon's address. The library reads neither (clippy.toml, `#![forbid]` in lib.rs). +#[allow(clippy::disallowed_methods, reason = "the binary reads its own arguments and the daemon's address, and hands them to the library")] +fn run() -> Result { + let args: Vec = std::env::args().skip(1).collect(); + let (instance, endpoint) = parse(&args)?; + let source = FileSource::new(control_path(Path::new(CONTROL_ROOT), &instance)?); + let endpoint = endpoint.or_else(|| std::env::var("HUGINN_ENDPOINT").ok()).ok_or_else(|| anyhow!("no daemon endpoint: --endpoint or HUGINN_ENDPOINT"))?; + let address = endpoint + .strip_prefix("rudp://") + .and_then(|address| address.parse::().ok()) + .ok_or_else(|| anyhow!("the endpoint is not rudp://:"))?; + let mind = HuginnMind::new(HuginnClient::new(address, instance, TIMEOUT)); + let launcher = SystemdLauncher::new(); + let host = host(); + let ports = Ports { mind: &mind, launcher: &launcher, clock: &SystemClock, host: &host }; + Ok(cycle(&ports, &source)?.to_string()) +} + +fn main() { + match run() { + Ok(outcome) => println!("{outcome}"), + Err(error) => { + eprintln!("mind-wake: {error:#}"); + std::process::exit(2); + } + } +} diff --git a/crates/mind-body/src/control.rs b/crates/mind-body/src/control.rs index 0792c44..9d0cac0 100644 --- a/crates/mind-body/src/control.rs +++ b/crates/mind-body/src/control.rs @@ -48,7 +48,7 @@ pub const DIAL_KEY: &str = "dial"; /// `//control.cc`, after the leaf's slug grammar has passed, so /// an instance name cannot walk out of the root. pub fn control_path(root: &Path, instance: &Slug) -> Result { - instance.validate_slug().map_err(|refusal| anyhow::anyhow!("instance {:?} is not a slug: {refusal:?}", instance.0))?; + instance.validate_slug().map_err(|_| anyhow::anyhow!("the instance is not a slug"))?; Ok(root.join(&instance.0).join("control.cc")) } diff --git a/crates/mind-body/src/lib.rs b/crates/mind-body/src/lib.rs index 598c6f0..4a86f31 100644 --- a/crates/mind-body/src/lib.rs +++ b/crates/mind-body/src/lib.rs @@ -8,6 +8,8 @@ //! `launch` and `queue` are the launch primitive: the work queue, the //! repetition breaker, the one-live-Self-run rule, and the single path that //! opens a run in the mind and only then starts its unit. +//! +//! `wake` is the waker's one cycle over them: the `mind-wake` binary. // The control root is a constant, so the library reads no part of the process // environment (clippy.toml lists the readers); `forbid` keeps an `allow` from @@ -21,6 +23,7 @@ pub mod cli; pub mod control; pub mod launch; pub mod queue; +pub mod wake; #[cfg(test)] mod testkit; diff --git a/crates/mind-body/src/wake.rs b/crates/mind-body/src/wake.rs new file mode 100644 index 0000000..40ceb91 --- /dev/null +++ b/crates/mind-body/src/wake.rs @@ -0,0 +1,117 @@ +//! The waker: one cycle, no memory between cycles. +//! +//! A cycle is three steps in a fixed order. (1) The brake and the dial are read +//! once, through `control::derive`, the derivation behind `read_effective` and +//! the unit gate; held ends the cycle before the mind, the clock or a unit is +//! touched. (2) The newest Persona turn of hers is found, whatever became of +//! it, and her cadence (the Grant's, from the dial) is waited out since its +//! admission. (3) `open_and_launch` is called once for a Persona turn with no +//! claims and the Grant, and its answer is the cycle's. +//! +//! The waker owns no file, no timer state and no cursor: the run in the mind is +//! the grant (target invariant run-is-the-grant), and whether she is awake is +//! decided by admission and answered by the launch primitive. The timer that +//! calls a cycle is a fixed poll; it decides nothing. The waker starts no Self +//! run and ends no run or unit. + +use std::fmt; + +use anyhow::{Result, anyhow, bail}; +use chrono::{DateTime, Duration, Utc}; +use cultnet_rs::Selection; +use eureka_pipeline::{PipelineKind, PipelineRef, RunOperator, RunTurn, Slug}; +use huginn_mind::PipelinePageItems; + +use crate::control::{ControlSource, Effective, HeldReason, derive}; +use crate::launch::{Declined, LaunchOutcome, LaunchRequest, Ports, open_and_launch}; +use crate::queue::{MindPort, any_of, of_kind}; + +/// The admitting agent's name on the receipt of every run the waker opens. +pub const AGENT: &str = "mind-wake"; + +/// What one cycle did. Every outcome is an answer; only a mind that cannot be +/// reached is an error. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum WakeOutcome { + /// The brake or the dial did not allow a launch; nothing else was touched. + Held { reason: HeldReason }, + /// Her cadence since the newest Persona turn has not passed; `until` is when it has. + NotYet { until: DateTime }, + /// A Persona turn of hers is live. + Busy { run: PipelineRef }, + Launched { run: PipelineRef }, + Refused(Declined), +} + +impl fmt::Display for WakeOutcome { + /// One line, with ids and times the mind gave and never a value a refusal + /// carries. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Held { reason } => write!(f, "held: {reason:?}"), + Self::NotYet { until } => write!(f, "not yet: until {}", until.to_rfc3339()), + Self::Busy { run } => write!(f, "busy: {}", run.id.0), + Self::Launched { run } => write!(f, "launched: {}", run.id.0), + Self::Refused(declined) => write!(f, "refused: {}", declined_code(declined)), + } + } +} + +fn declined_code(declined: &Declined) -> &'static str { + match declined { + Declined::Admission(_) => "admission", + Declined::Conflict => "conflict", + Declined::AlreadyAdmitted => "already_admitted", + Declined::StartFailed => "start_failed", + Declined::GrantForOtherInstance => "grant_for_other_instance", + Declined::PersonaRunTakesNoClaims => "persona_run_takes_claims", + } +} + +/// When the newest Persona turn of hers, of any status, was admitted. The +/// spellings of the two filters are the types' own, and one page of one row is +/// asked for: an idle wake is a query. +fn newest_persona_turn(mind: &dyn MindPort, instance: &Slug) -> Result>> { + let turn = format!("{:?}", RunTurn::PersonaTurn); + let operator = format!("{:?}", RunOperator::Mind); + let runs = any_of(of_kind(PipelineKind::Run), "root", &[&instance.0]); + let selection = Selection { descending: true, limit: Some(1), ..any_of(any_of(runs, "turn", &[&turn]), "operated_by", &[&operator]) }; + let PipelinePageItems::Headers(headers) = mind.query(&selection)?.items else { + bail!("the mind answered documents to a header selection"); + }; + headers + .first() + .map(|newest| { + DateTime::parse_from_rfc3339(&newest.admission.admitted_at) + .map(|at| at.with_timezone(&Utc)) + .map_err(|_| anyhow!("the newest Persona turn's admission time is not a timestamp")) + }) + .transpose() +} + +/// One cycle. `control` is the instance's control store; the reading is taken +/// once and its Grant is the one handed to the launch. +pub fn cycle(ports: &Ports, control: &impl ControlSource) -> Result { + let instance = ports.mind.instance().clone(); + let grant = match derive(control, &instance) { + Effective::Held { reason } => return Ok(WakeOutcome::Held { reason }), + Effective::Released(grant) => grant, + }; + if let Some(newest) = newest_persona_turn(ports.mind, &instance)? { + let cadence = Duration::from_std(grant.cadence()).map_err(|_| anyhow!("the dial's cadence is out of range"))?; + let until = newest + cadence; + if until > ports.clock.now() { + return Ok(WakeOutcome::NotYet { until }); + } + } + let request = LaunchRequest { turn: RunTurn::PersonaTurn, claims: vec![], agent: AGENT.into(), grant }; + match open_and_launch(ports, request)? { + LaunchOutcome::Launched { run } => Ok(WakeOutcome::Launched { run }), + LaunchOutcome::Busy { run } => Ok(WakeOutcome::Busy { run }), + LaunchOutcome::Refused(declined) => Ok(WakeOutcome::Refused(declined)), + LaunchOutcome::NotQueued { .. } | LaunchOutcome::Tripped { .. } => bail!("the launch answered a Persona turn with an answer only a Self run has"), + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/mind-body/src/wake/tests.rs b/crates/mind-body/src/wake/tests.rs new file mode 100644 index 0000000..9dc2aa3 --- /dev/null +++ b/crates/mind-body/src/wake/tests.rs @@ -0,0 +1,308 @@ +//! The waker's tests: a real mind over a temporary directory, the real control +//! store written by the operator's writer and read by the real derivation, a +//! recording launcher and a settable clock. A wait is observed where it is +//! decided, as the `until` the cycle returns, never after a sleep. + +use std::cell::Cell; +use std::str::FromStr; + +use anyhow::{Result, anyhow}; +use chrono::{DateTime, Duration, Utc}; +use cultnet_rs::Selection; +use eureka_pipeline::{PipelineDocument, PipelineKind as K, ResolutionOutcome, RunOperator, RunTurn, Slug}; +use huginn_mind::{MindRefusal, PipelineAdmissionOutcome, PipelineSelectionPage}; +use rust_decimal::Decimal; +use tempfile::TempDir; + +use super::*; +use crate::control::{BurnRate, ControlWriter, FileSource}; +use crate::launch::Launcher; +use crate::testkit::*; + +/// Every call the units port receives, counted over a recording launcher. +struct Units { + rec: Recording, + calls: Cell, +} + +impl Launcher for Units { + fn start(&self, instance: &Slug, turn: RunTurn) -> Result<()> { + self.calls.set(self.calls.get() + 1); + self.rec.start(instance, turn) + } + + fn alive(&self, instance: &Slug, turn: RunTurn) -> bool { + self.calls.set(self.calls.get() + 1); + self.rec.alive(instance, turn) + } +} + +/// A mind that counts the queries and admissions it is asked for. +struct Counting<'a> { + inner: &'a TestMind, + queries: Cell, + admits: Cell, +} + +impl MindPort for Counting<'_> { + fn instance(&self) -> &Slug { + self.inner.instance() + } + + fn query(&self, selection: &Selection) -> Result { + self.queries.set(self.queries.get() + 1); + self.inner.query(selection) + } + + fn admit(&self, agent: &str, session: &str, documents: Vec) -> Result { + self.admits.set(self.admits.get() + 1); + self.inner.admit(agent, session, documents) + } +} + +/// A mind that cannot be reached. +struct Unreachable(Slug); + +impl MindPort for Unreachable { + fn instance(&self) -> &Slug { + &self.0 + } + + fn query(&self, _selection: &Selection) -> Result { + Err(anyhow!("the mind could not be asked")) + } + + fn admit(&self, _agent: &str, _session: &str, _documents: Vec) -> Result { + Err(anyhow!("the mind could not be asked")) + } +} + +struct Rig { + mind: TestMind, + launcher: Units, + clock: Fixed, + dir: TempDir, +} + +fn dec(text: &str) -> Decimal { + Decimal::from_str(text).unwrap() +} + +fn seconds(whole: i64, nanos: i64) -> Duration { + Duration::seconds(whole) + Duration::nanoseconds(nanos) +} + +impl Rig { + fn new() -> Self { + Self { + mind: TestMind::seeded(), + launcher: Units { rec: Recording::default(), calls: Cell::new(0) }, + clock: Fixed(Cell::new(at(0))), + dir: TempDir::new().unwrap(), + } + } + + fn store(&self) -> std::path::PathBuf { + self.dir.path().join(INSTANCE).join("control.cc") + } + + /// The brake released and the dial set, through the operator's writer. + fn release(&self, heat: &str, cooldown_s: u32, run_usd: &str) { + let mut writer = ControlWriter::open(self.store()).unwrap(); + writer.set_brake(true, at(0), "op").unwrap(); + writer.set_dial(BurnRate { heat: dec(heat), base_cooldown_s: cooldown_s, base_run_usd: dec(run_usd), set_at: at(0), set_by: "op".into() }).unwrap(); + } + + fn cycle(&self) -> WakeOutcome { + self.cycle_through(&self.mind).unwrap() + } + + fn cycle_through(&self, mind: &dyn MindPort) -> Result { + let ports = Ports { mind, launcher: &self.launcher, clock: &self.clock, host: "yggdrasil-host" }; + super::cycle(&ports, &FileSource::new(self.store())) + } + + /// A Persona turn of hers, opened and then ended as given. + fn persona(&self, label: &str, ending: ResolutionOutcome) { + self.mind.committed(vec![run(label, RunTurn::PersonaTurn, RunOperator::Mind, &[])]); + self.mind.committed(vec![close(run_ref(label), ending)]); + } + + fn admitted(&self, label: &str) -> DateTime { + let stamp = self.mind.view(K::Run, &run_ref(label).id.0).unwrap().admission.admitted_at; + DateTime::parse_from_rfc3339(&stamp).unwrap().with_timezone(&Utc) + } + + fn runs(&self) -> usize { + self.mind.ids(K::Run).len() + } +} + +#[test] +fn with_the_brake_held_or_the_dial_absent_a_cycle_queries_nothing_admits_nothing_and_calls_no_unit() { + let cases: [(&str, HeldReason, fn(&Rig)); 4] = [ + ("no store", HeldReason::BrakeAbsent, |_| {}), + ("brake held", HeldReason::BrakeHeld, |rig| { + rig.release("1", 600, "5"); + ControlWriter::open(rig.store()).unwrap().set_brake(false, at(1), "op").unwrap(); + }), + ("dial absent", HeldReason::DialAbsent, |rig| { + ControlWriter::open(rig.store()).unwrap().set_brake(true, at(0), "op").unwrap(); + }), + ("store undecodable", HeldReason::Undecodable, |rig| { + rig.release("1", 600, "5"); + std::fs::write(rig.store(), b"junk").unwrap(); + }), + ]; + for (name, reason, prepare) in cases { + // No Persona turn yet, so a released brake would launch one. + let rig = Rig::new(); + prepare(&rig); + let counting = Counting { inner: &rig.mind, queries: Cell::new(0), admits: Cell::new(0) }; + let outcome = rig.cycle_through(&counting).unwrap(); + assert_eq!(outcome, WakeOutcome::Held { reason }, "{name}"); + assert_eq!((counting.queries.get(), counting.admits.get(), rig.launcher.calls.get(), rig.runs()), (0, 0, 0, 0), "{name}"); + } +} + +#[test] +fn the_wait_is_the_grants_cadence_after_the_newest_persona_turn_and_ends_exactly_there() { + // (heat, base cooldown, the cadence base_cooldown / heat): whole, halved, + // and one that carries nanoseconds, so a clamp, offset or scale shows. + let heats = [("1", 600, seconds(600, 0)), ("2", 600, seconds(300, 0)), ("1.5", 1000, seconds(666, 666_666_667)), ("0.05", 3600, seconds(72_000, 0))]; + for (heat, cooldown, cadence) in heats { + let rig = Rig::new(); + rig.release(heat, cooldown, "5"); + rig.persona("p1", recorded()); + let until = rig.admitted("p1") + cadence; + + rig.clock.0.set(until - Duration::milliseconds(1)); + assert_eq!(rig.cycle(), WakeOutcome::NotYet { until }, "heat {heat}, a millisecond early"); + assert_eq!((rig.launcher.calls.get(), rig.runs()), (0, 1), "heat {heat}: waiting touches no unit and admits nothing"); + + rig.clock.0.set(until); + assert!(matches!(rig.cycle(), WakeOutcome::Launched { .. }), "heat {heat}, at the boundary"); + assert_eq!(rig.runs(), 2, "heat {heat}"); + } +} + +#[test] +fn the_same_age_waits_under_one_dial_and_launches_under_another() { + let age = |heat: &str| { + let rig = Rig::new(); + rig.release(heat, 600, "5"); + rig.persona("p1", recorded()); + rig.clock.0.set(rig.admitted("p1") + Duration::seconds(400)); + rig.cycle() + }; + assert!(matches!(age("1"), WakeOutcome::NotYet { .. }), "600 s cadence at 400 s"); + assert!(matches!(age("2"), WakeOutcome::Launched { .. }), "300 s cadence at 400 s"); +} + +#[test] +fn a_newer_self_run_or_operator_turn_does_not_move_the_boundary() { + let rig = Rig::new(); + rig.release("1", 600, "5"); + rig.persona("p1", recorded()); + let until = rig.admitted("p1") + Duration::seconds(600); + rig.mind.committed(vec![run("s1", RunTurn::SelfRun, RunOperator::Mind, &[])]); + rig.mind.committed(vec![run("op1", RunTurn::PersonaTurn, RunOperator::Operator, &[])]); + assert!(rig.admitted("s1") > rig.admitted("p1") && rig.admitted("op1") > rig.admitted("p1")); + + rig.clock.0.set(until - Duration::milliseconds(1)); + assert_eq!(rig.cycle(), WakeOutcome::NotYet { until }); + rig.clock.0.set(until); + assert!(matches!(rig.cycle(), WakeOutcome::Launched { .. }), "the boundary is still her Persona turn's"); +} + +#[test] +fn an_ended_persona_turn_sets_the_boundary_whatever_ended_it_and_the_newest_wins() { + for (name, ending) in [("recorded", recorded()), ("withdrawn", withdrawn())] { + let rig = Rig::new(); + rig.release("1", 600, "5"); + rig.persona("p1", recorded()); + rig.persona("p2", ending); + assert!(rig.admitted("p2") > rig.admitted("p1"), "{name}"); + let until = rig.admitted("p2") + Duration::seconds(600); + rig.clock.0.set(until - Duration::milliseconds(1)); + assert_eq!(rig.cycle(), WakeOutcome::NotYet { until }, "{name}"); + } +} + +#[test] +fn a_live_persona_turn_past_the_cadence_answers_busy_and_the_cycle_admits_nothing_itself() { + let rig = Rig::new(); + rig.release("1", 600, "5"); + rig.mind.committed(vec![run("p1", RunTurn::PersonaTurn, RunOperator::Mind, &[])]); + rig.launcher.rec.revive(RunTurn::PersonaTurn); + rig.clock.0.set(rig.admitted("p1") + Duration::seconds(601)); + let counting = Counting { inner: &rig.mind, queries: Cell::new(0), admits: Cell::new(0) }; + assert_eq!(rig.cycle_through(&counting).unwrap(), WakeOutcome::Busy { run: run_ref("p1") }); + assert_eq!(counting.admits.get(), 0); + assert!(rig.launcher.rec.started.borrow().is_empty()); + assert_eq!(rig.runs(), 1); +} + +#[test] +fn the_first_released_cycle_launches_a_persona_run_with_no_claims_and_the_grants_cap() { + let rig = Rig::new(); + // Decoys: a Self run of hers and an operator's Persona turn are not her Persona turns. + rig.mind.committed(vec![run("s1", RunTurn::SelfRun, RunOperator::Mind, &[])]); + rig.mind.committed(vec![run("op1", RunTurn::PersonaTurn, RunOperator::Operator, &[])]); + rig.release("2", 600, "3.25"); + let WakeOutcome::Launched { run: opened } = rig.cycle() else { panic!("not launched") }; + let view = rig.mind.view(K::Run, &opened.id.0).unwrap(); + let PipelineDocument::Run(held) = &view.document else { panic!("not a run") }; + assert_eq!((held.operated_by, held.turn), (RunOperator::Mind, RunTurn::PersonaTurn)); + assert!(held.claims.is_empty() && held.campaigns.is_empty()); + assert_eq!(held.budget_usd.0, "6.5", "base_run_usd x heat"); + assert_eq!(held.host.0, "yggdrasil-host"); + assert_eq!(view.admission.provenance.agent.0, AGENT); + assert_eq!(*rig.launcher.rec.started.borrow(), vec![(Slug(INSTANCE.into()), RunTurn::PersonaTurn)]); +} + +#[test] +fn two_consecutive_cycles_launch_once_and_the_second_waits_or_answers_busy() { + let rig = Rig::new(); + rig.release("1", 600, "5"); + let WakeOutcome::Launched { run: first } = rig.cycle() else { panic!("not launched") }; + let stamp = rig.mind.view(K::Run, &first.id.0).unwrap().admission.admitted_at; + let until = DateTime::parse_from_rfc3339(&stamp).unwrap().with_timezone(&Utc) + Duration::seconds(600); + assert_eq!(rig.cycle(), WakeOutcome::NotYet { until }, "straight after: within the cadence"); + rig.clock.0.set(until); + assert_eq!(rig.cycle(), WakeOutcome::Busy { run: first }, "past the cadence with her turn still live"); + assert_eq!((rig.runs(), rig.launcher.rec.started.borrow().len()), (1, 1)); +} + +#[test] +fn a_unit_that_does_not_start_and_an_admission_that_is_refused_are_refusals_not_errors() { + let rig = Rig::new(); + rig.release("1", 600, "5"); + rig.launcher.rec.fail.set(true); + assert_eq!(rig.cycle(), WakeOutcome::Refused(Declined::StartFailed)); + + let rig = Rig::new(); + rig.release("1", 600, "5"); + let refusing = Answering { inner: &rig.mind, outcome: PipelineAdmissionOutcome::Conflict { identities: vec![] } }; + assert_eq!(rig.cycle_through(&refusing).unwrap(), WakeOutcome::Refused(Declined::Conflict)); + assert!(rig.launcher.rec.started.borrow().is_empty()); +} + +#[test] +fn a_mind_that_cannot_be_reached_is_an_error_and_starts_nothing() { + let rig = Rig::new(); + rig.release("1", 600, "5"); + assert!(rig.cycle_through(&Unreachable(Slug(INSTANCE.into()))).is_err()); + assert_eq!(rig.launcher.calls.get(), 0); +} + +#[test] +fn an_outcome_prints_ids_and_codes_and_never_a_refusals_values() { + let canary = "canary-8c1f"; + let refused = Declined::Admission(MindRefusal::RepoNotInCampaign { repo: canary.into() }); + assert_eq!(WakeOutcome::Refused(refused).to_string(), "refused: admission"); + assert_eq!(WakeOutcome::Held { reason: HeldReason::BrakeHeld }.to_string(), "held: BrakeHeld"); + assert_eq!(WakeOutcome::Launched { run: run_ref("p1") }.to_string(), format!("launched: {INSTANCE}:run:p1")); + assert_eq!(WakeOutcome::Busy { run: run_ref("p1") }.to_string(), format!("busy: {INSTANCE}:run:p1")); + assert_eq!(WakeOutcome::NotYet { until: at(0) }.to_string(), "not yet: until 2026-10-09T13:15:00+00:00"); +} diff --git a/crates/mind-body/tests/wake.rs b/crates/mind-body/tests/wake.rs new file mode 100644 index 0000000..cbd2ddc --- /dev/null +++ b/crates/mind-body/tests/wake.rs @@ -0,0 +1,51 @@ +//! The `mind-wake` binary as a process: its arguments and its exit codes. The +//! brake's root is a constant, so a process test can only ever see the absent +//! store of an instance no host has; the cycle's behaviour is tested in the +//! library against a real mind and a real store. + +use std::process::{Command, Output}; + +const NO_SUCH_INSTANCE: &str = "wake-test-no-such-instance"; + +fn wake(args: &[&str]) -> Output { + Command::new(env!("CARGO_BIN_EXE_mind-wake")).args(args).env_remove("HUGINN_ENDPOINT").output().unwrap() +} + +#[test] +fn without_an_instance_or_with_a_stray_argument_it_exits_2_with_the_usage_and_echoes_nothing() { + let canary = "canary-argument-3f9a"; + for args in [vec![], vec!["--endpoint", "rudp://127.0.0.1:9"], vec!["--instance", NO_SUCH_INSTANCE, canary], vec!["--instance", NO_SUCH_INSTANCE, "--instance", canary]] { + let output = wake(&args); + assert_eq!(output.status.code(), Some(2), "{args:?}"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("usage: mind-wake"), "{args:?}: {stderr}"); + assert!(!stderr.contains(canary) && output.stdout.is_empty(), "{args:?}"); + } +} + +#[test] +fn an_instance_that_is_not_a_slug_is_exit_2_and_is_not_echoed() { + let canary = "Canary/../Instance-5e2b"; + let output = wake(&["--instance", canary, "--endpoint", "rudp://127.0.0.1:9"]); + assert_eq!(output.status.code(), Some(2)); + assert!(!String::from_utf8_lossy(&output.stderr).contains("anary"), "{}", String::from_utf8_lossy(&output.stderr)); +} + +#[test] +fn a_held_brake_ends_the_cycle_with_exit_0_before_the_mind_is_contacted() { + // The endpoint answers no one (port 9 on loopback), so a cycle that + // reached the mind would exit 2; the absent store holds the cycle first. + let output = wake(&["--instance", NO_SUCH_INSTANCE, "--endpoint", "rudp://127.0.0.1:9"]); + assert_eq!(output.status.code(), Some(0), "{}", String::from_utf8_lossy(&output.stderr)); + assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "held: BrakeAbsent"); +} + +#[test] +fn a_malformed_endpoint_or_none_is_exit_2_and_the_endpoint_is_not_echoed() { + let canary = "canary-endpoint-77c1"; + let bad = wake(&["--instance", NO_SUCH_INSTANCE, "--endpoint", canary]); + assert_eq!(bad.status.code(), Some(2)); + assert!(!String::from_utf8_lossy(&bad.stderr).contains(canary)); + let none = wake(&["--instance", NO_SUCH_INSTANCE]); + assert_eq!(none.status.code(), Some(2)); +} From 6e19e1a4c88a49e83faaf2ec3db64d5e8709049a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 12 Oct 2026 08:17:17 +0200 Subject: [PATCH 002/342] mind-body public surface: wake Co-Authored-By: Claude Sonnet 5.5 --- crates/mind-body/tests/public_surface.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/mind-body/tests/public_surface.txt b/crates/mind-body/tests/public_surface.txt index 701faee..405b5e6 100644 --- a/crates/mind-body/tests/public_surface.txt +++ b/crates/mind-body/tests/public_surface.txt @@ -2,6 +2,7 @@ mind_body: mod cli mind_body: mod control mind_body: mod launch mind_body: mod queue +mind_body: mod wake mind_body::cli: fn operator_name (sudo_user : Option < String > , user : Option < String >) -> String mind_body::cli: fn run (args : & [String] , root : & Path , set_by : & str , now : DateTime < Utc > , out : & mut impl Write , stop_units : & mut dyn FnMut (& Slug) -> Result < () > ,) -> Result < () > mind_body::control: # [derive (Clone , Copy , Debug , PartialEq , Eq)] pub enum HeldReason { BrakeAbsent , BrakeHeld , DialAbsent , DialOutOfBounds , Undecodable , } @@ -70,3 +71,7 @@ mind_body::queue: impl MindPort for HuginnMind < '_ > mind_body::queue: impl< 'a > HuginnMind < 'a > mind_body::queue: pub trait MindPort { fn instance (& self) -> & Slug ; fn query (& self , selection : & Selection) -> Result < PipelineSelectionPage > ; fn admit (& self , agent : & str , session : & str , documents : Vec < PipelineDocument >) -> Result < PipelineAdmissionOutcome > ; } mind_body::queue: struct HuginnMind< 'a > pub-fields[] +mind_body::wake: # [derive (Clone , Debug , PartialEq , Eq)] pub enum WakeOutcome { Held { reason : HeldReason } , NotYet { until : DateTime < Utc > } , Busy { run : PipelineRef } , Launched { run : PipelineRef } , Refused (Declined) , } +mind_body::wake: const AGENT : & str +mind_body::wake: fn cycle (ports : & Ports , control : & impl ControlSource) -> Result < WakeOutcome > +mind_body::wake: impl fmt :: Display for WakeOutcome From 10077f186c3f1cea45b7741d4b5ad6362065fe6a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 12 Oct 2026 08:18:33 +0200 Subject: [PATCH 003/342] mind-body wake tests: a type alias for the held cases (clippy type_complexity) Co-Authored-By: Claude Sonnet 5.5 --- crates/mind-body/src/wake/tests.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/mind-body/src/wake/tests.rs b/crates/mind-body/src/wake/tests.rs index 9dc2aa3..d191338 100644 --- a/crates/mind-body/src/wake/tests.rs +++ b/crates/mind-body/src/wake/tests.rs @@ -140,7 +140,8 @@ impl Rig { #[test] fn with_the_brake_held_or_the_dial_absent_a_cycle_queries_nothing_admits_nothing_and_calls_no_unit() { - let cases: [(&str, HeldReason, fn(&Rig)); 4] = [ + type Prepare = fn(&Rig); + let cases: [(&str, HeldReason, Prepare); 4] = [ ("no store", HeldReason::BrakeAbsent, |_| {}), ("brake held", HeldReason::BrakeHeld, |rig| { rig.release("1", 600, "5"); From 84d3aee02aa343e4b0e1927c34868788887ec4fb Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 12 Oct 2026 08:28:51 +0200 Subject: [PATCH 004/342] mind-body wake tests: finding the newest Persona turn reads one row Co-Authored-By: Claude Sonnet 5.5 --- crates/mind-body/src/wake/tests.rs | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/crates/mind-body/src/wake/tests.rs b/crates/mind-body/src/wake/tests.rs index d191338..c24356f 100644 --- a/crates/mind-body/src/wake/tests.rs +++ b/crates/mind-body/src/wake/tests.rs @@ -10,7 +10,7 @@ use anyhow::{Result, anyhow}; use chrono::{DateTime, Duration, Utc}; use cultnet_rs::Selection; use eureka_pipeline::{PipelineDocument, PipelineKind as K, ResolutionOutcome, RunOperator, RunTurn, Slug}; -use huginn_mind::{MindRefusal, PipelineAdmissionOutcome, PipelineSelectionPage}; +use huginn_mind::{MindRefusal, PipelineAdmissionOutcome, PipelinePageItems, PipelineSelectionPage}; use rust_decimal::Decimal; use tempfile::TempDir; @@ -42,6 +42,8 @@ struct Counting<'a> { inner: &'a TestMind, queries: Cell, admits: Cell, + /// The header rows the queries brought back. + rows: Cell, } impl MindPort for Counting<'_> { @@ -51,7 +53,11 @@ impl MindPort for Counting<'_> { fn query(&self, selection: &Selection) -> Result { self.queries.set(self.queries.get() + 1); - self.inner.query(selection) + let page = self.inner.query(selection)?; + if let PipelinePageItems::Headers(headers) = &page.items { + self.rows.set(self.rows.get() + headers.len()); + } + Ok(page) } fn admit(&self, agent: &str, session: &str, documents: Vec) -> Result { @@ -159,7 +165,7 @@ fn with_the_brake_held_or_the_dial_absent_a_cycle_queries_nothing_admits_nothing // No Persona turn yet, so a released brake would launch one. let rig = Rig::new(); prepare(&rig); - let counting = Counting { inner: &rig.mind, queries: Cell::new(0), admits: Cell::new(0) }; + let counting = Counting { inner: &rig.mind, queries: Cell::new(0), admits: Cell::new(0), rows: Cell::new(0) }; let outcome = rig.cycle_through(&counting).unwrap(); assert_eq!(outcome, WakeOutcome::Held { reason }, "{name}"); assert_eq!((counting.queries.get(), counting.admits.get(), rig.launcher.calls.get(), rig.runs()), (0, 0, 0, 0), "{name}"); @@ -230,6 +236,19 @@ fn an_ended_persona_turn_sets_the_boundary_whatever_ended_it_and_the_newest_wins } } +#[test] +fn finding_the_newest_persona_turn_is_one_query_that_reads_one_row_however_many_there_are() { + let rig = Rig::new(); + rig.release("1", 600, "5"); + for label in ["p1", "p2", "p3"] { + rig.persona(label, recorded()); + } + rig.clock.0.set(rig.admitted("p3") + Duration::seconds(1)); + let counting = Counting { inner: &rig.mind, queries: Cell::new(0), admits: Cell::new(0), rows: Cell::new(0) }; + assert!(matches!(rig.cycle_through(&counting).unwrap(), WakeOutcome::NotYet { .. })); + assert_eq!((counting.queries.get(), counting.rows.get()), (1, 1)); +} + #[test] fn a_live_persona_turn_past_the_cadence_answers_busy_and_the_cycle_admits_nothing_itself() { let rig = Rig::new(); @@ -237,7 +256,7 @@ fn a_live_persona_turn_past_the_cadence_answers_busy_and_the_cycle_admits_nothin rig.mind.committed(vec![run("p1", RunTurn::PersonaTurn, RunOperator::Mind, &[])]); rig.launcher.rec.revive(RunTurn::PersonaTurn); rig.clock.0.set(rig.admitted("p1") + Duration::seconds(601)); - let counting = Counting { inner: &rig.mind, queries: Cell::new(0), admits: Cell::new(0) }; + let counting = Counting { inner: &rig.mind, queries: Cell::new(0), admits: Cell::new(0), rows: Cell::new(0) }; assert_eq!(rig.cycle_through(&counting).unwrap(), WakeOutcome::Busy { run: run_ref("p1") }); assert_eq!(counting.admits.get(), 0); assert!(rig.launcher.rec.started.borrow().is_empty()); From 61766cab173e59f7dc89615aed0922ef35b2b37e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 12 Oct 2026 08:57:24 +0200 Subject: [PATCH 005/342] mind-wake tests: the cadence is pinned below one second and above a day; the agent name and every Declined code are literals Closes the test gaps of findings cadence-clamp-unpinned and codes-and-agent-unpinned: a floor of 1 s or a ceiling of 80000 s on the wait now fails the cadence test, and the receipt's agent and the printed code of every refusal are compared with the spelling, not with the constant that produced them. Co-Authored-By: Claude Sonnet 5.5 --- crates/mind-body/src/wake/tests.rs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/mind-body/src/wake/tests.rs b/crates/mind-body/src/wake/tests.rs index c24356f..3c32cd1 100644 --- a/crates/mind-body/src/wake/tests.rs +++ b/crates/mind-body/src/wake/tests.rs @@ -175,8 +175,17 @@ fn with_the_brake_held_or_the_dial_absent_a_cycle_queries_nothing_admits_nothing #[test] fn the_wait_is_the_grants_cadence_after_the_newest_persona_turn_and_ends_exactly_there() { // (heat, base cooldown, the cadence base_cooldown / heat): whole, halved, - // and one that carries nanoseconds, so a clamp, offset or scale shows. - let heats = [("1", 600, seconds(600, 0)), ("2", 600, seconds(300, 0)), ("1.5", 1000, seconds(666, 666_666_667)), ("0.05", 3600, seconds(72_000, 0))]; + // and one that carries nanoseconds, so an offset or scale shows. The last + // two sit beyond a second below and a day above (the dial allows both), so + // a floor or a ceiling on the wait shows too. + let heats = [ + ("1", 600, seconds(600, 0)), + ("2", 600, seconds(300, 0)), + ("1.5", 1000, seconds(666, 666_666_667)), + ("0.05", 3600, seconds(72_000, 0)), + ("2", 1, seconds(0, 500_000_000)), + ("0.05", 10_000, seconds(200_000, 0)), + ]; for (heat, cooldown, cadence) in heats { let rig = Rig::new(); rig.release(heat, cooldown, "5"); @@ -277,7 +286,7 @@ fn the_first_released_cycle_launches_a_persona_run_with_no_claims_and_the_grants assert!(held.claims.is_empty() && held.campaigns.is_empty()); assert_eq!(held.budget_usd.0, "6.5", "base_run_usd x heat"); assert_eq!(held.host.0, "yggdrasil-host"); - assert_eq!(view.admission.provenance.agent.0, AGENT); + assert_eq!(view.admission.provenance.agent.0, "mind-wake"); assert_eq!(*rig.launcher.rec.started.borrow(), vec![(Slug(INSTANCE.into()), RunTurn::PersonaTurn)]); } @@ -321,6 +330,15 @@ fn an_outcome_prints_ids_and_codes_and_never_a_refusals_values() { let canary = "canary-8c1f"; let refused = Declined::Admission(MindRefusal::RepoNotInCampaign { repo: canary.into() }); assert_eq!(WakeOutcome::Refused(refused).to_string(), "refused: admission"); + for (declined, code) in [ + (Declined::Conflict, "conflict"), + (Declined::AlreadyAdmitted, "already_admitted"), + (Declined::StartFailed, "start_failed"), + (Declined::GrantForOtherInstance, "grant_for_other_instance"), + (Declined::PersonaRunTakesNoClaims, "persona_run_takes_claims"), + ] { + assert_eq!(WakeOutcome::Refused(declined).to_string(), format!("refused: {code}")); + } assert_eq!(WakeOutcome::Held { reason: HeldReason::BrakeHeld }.to_string(), "held: BrakeHeld"); assert_eq!(WakeOutcome::Launched { run: run_ref("p1") }.to_string(), format!("launched: {INSTANCE}:run:p1")); assert_eq!(WakeOutcome::Busy { run: run_ref("p1") }.to_string(), format!("busy: {INSTANCE}:run:p1")); From 6f4f970c1a92c99433ba06edb8fae5bdd390d6d4 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 12 Oct 2026 08:57:34 +0200 Subject: [PATCH 006/342] mind-wake process tests: HUGINN_ENDPOINT is the fallback, and in a container the real bin and daemon record this machine's host The env test pins the variable's name and that the flag wins over it. The ignored container test seeds a mind, serves it with the real huginn-daemon, writes the brake at its real root and runs the real mind-wake with an empty PATH (so no unit is reached): the run it opens must carry /proc/sys/kernel/hostname as host and mind-wake as agent, which kills the three host() mutants of finding host-survivors-reachable. Co-Authored-By: Claude Sonnet 5.5 --- crates/mind-body/tests/wake.rs | 153 ++++++++++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 2 deletions(-) diff --git a/crates/mind-body/tests/wake.rs b/crates/mind-body/tests/wake.rs index cbd2ddc..9a97e3b 100644 --- a/crates/mind-body/tests/wake.rs +++ b/crates/mind-body/tests/wake.rs @@ -1,9 +1,25 @@ //! The `mind-wake` binary as a process: its arguments and its exit codes. The //! brake's root is a constant, so a process test can only ever see the absent //! store of an instance no host has; the cycle's behaviour is tested in the -//! library against a real mind and a real store. +//! library against a real mind and a real store. One test, `#[ignore]`d outside a +//! container, puts the real binary, the real daemon and a real store at the +//! brake's root together, which is where the host a run records can be seen. -use std::process::{Command, Output}; +use std::net::UdpSocket; +use std::path::Path; +use std::process::{Child, Command, Output, Stdio}; +use std::str::FromStr; +use std::time::Duration; + +use chrono::Utc; +use cultnet_rs::Selection; +use eureka_pipeline::{ + Date, DocRef, PipelineCampaign, PipelineDocument, PipelineInstance, PipelineKind, PipelineStewardship, PipelineTarget, Sha, Slug, TargetInvariant, +}; +use huginn_mind::{Faculty, Mind, PipelineAdmissionBatch, PipelineAdmissionOutcome, PipelinePageItems, PipelineProvenance}; +use mind_body::control::{BurnRate, CONTROL_ROOT, ControlWriter, control_path}; +use rust_decimal::Decimal; +use tempfile::TempDir; const NO_SUCH_INSTANCE: &str = "wake-test-no-such-instance"; @@ -11,6 +27,11 @@ fn wake(args: &[&str]) -> Output { Command::new(env!("CARGO_BIN_EXE_mind-wake")).args(args).env_remove("HUGINN_ENDPOINT").output().unwrap() } +/// The binary with `HUGINN_ENDPOINT` set to `endpoint`. +fn wake_with_env_endpoint(args: &[&str], endpoint: &str) -> Output { + Command::new(env!("CARGO_BIN_EXE_mind-wake")).args(args).env("HUGINN_ENDPOINT", endpoint).output().unwrap() +} + #[test] fn without_an_instance_or_with_a_stray_argument_it_exits_2_with_the_usage_and_echoes_nothing() { let canary = "canary-argument-3f9a"; @@ -49,3 +70,131 @@ fn a_malformed_endpoint_or_none_is_exit_2_and_the_endpoint_is_not_echoed() { let none = wake(&["--instance", NO_SUCH_INSTANCE]); assert_eq!(none.status.code(), Some(2)); } + +#[test] +fn without_the_flag_the_daemon_is_found_through_huginn_endpoint_and_a_bad_value_is_not_echoed() { + // With no endpoint at all the cycle exits 2 (above); with the variable set it + // goes on to the brake, which holds an instance no host has, and exits 0. + let held = wake_with_env_endpoint(&["--instance", NO_SUCH_INSTANCE], "rudp://127.0.0.1:9"); + assert_eq!(held.status.code(), Some(0), "{}", String::from_utf8_lossy(&held.stderr)); + assert_eq!(String::from_utf8_lossy(&held.stdout).trim(), "held: BrakeAbsent"); + + let canary = "canary-env-endpoint-4d02"; + let bad = wake_with_env_endpoint(&["--instance", NO_SUCH_INSTANCE], canary); + assert_eq!(bad.status.code(), Some(2)); + assert!(!String::from_utf8_lossy(&bad.stderr).contains(canary) && bad.stdout.is_empty()); + + // The flag wins over the variable: a valid flag with a bad variable still holds. + let flag = wake_with_env_endpoint(&["--instance", NO_SUCH_INSTANCE, "--endpoint", "rudp://127.0.0.1:9"], canary); + assert_eq!(flag.status.code(), Some(0)); +} + +const INSTANCE: &str = "yggdrasil"; +const CAMPAIGN: &str = "eureka-body"; +const REPO: &str = "GameCult/Huginn"; + +fn doc_ref() -> DocRef { + DocRef { path: "docs/eureka-body-target.md".into(), start_line: 1, end_line: 9, commit: Sha("df7507b".into()) } +} + +/// The identity, stewardship, campaign and target a Persona turn's run cites. +fn seed() -> Vec { + let date = || Date("2026-10-09".into()); + vec![ + PipelineDocument::Instance(PipelineInstance { instance: Slug(INSTANCE.into()), display_name: "test mind".into(), created_at: date(), host: "test".into() }), + PipelineDocument::Stewardship(PipelineStewardship { instance: Slug(INSTANCE.into()), repo: REPO.into(), sequence: 1, assigned_on: date(), note: "assigned".into() }), + PipelineDocument::Campaign(PipelineCampaign { + slug: Slug(CAMPAIGN.into()), + title: "Eureka body".into(), + repos: vec![REPO.into()], + working_branch: "main".into(), + target_doc: doc_ref(), + }), + PipelineDocument::Target(PipelineTarget { + campaign: Slug(CAMPAIGN.into()), + revision: 1, + invariants: vec![TargetInvariant { label: "run-is-the-grant".into(), statement: "A run is the grant.".into() }], + not_in_scope: vec![], + canonical_implementations: vec![], + doc: doc_ref(), + }), + ] +} + +/// The daemon, ended with the test whether it passes or not. +struct Daemon(Child); + +impl Drop for Daemon { + fn drop(&mut self) { + let _ = self.0.kill(); + let _ = self.0.wait(); + } +} + +/// The real binary against the real daemon and a real store at the brake's +/// root. It writes `/etc/gamecult/minds`, so it runs in a container only +/// (`/.dockerenv`), as root, after `cargo build -p huginn-daemon` has put the +/// daemon beside the binary. `PATH` is empty so no unit is ever reached: the +/// run opens, its unit fails to start and the run is withdrawn, which is the +/// outcome printed. +#[test] +#[ignore = "container only: root, /etc/gamecult/minds and a built huginn-daemon beside the binary"] +fn the_real_bin_against_the_real_daemon_opens_a_run_that_records_this_machines_host_and_the_agent() { + assert!(Path::new("/.dockerenv").exists(), "this test writes the brake's root and runs in a container only"); + let slug = Slug(INSTANCE.into()); + let state = TempDir::new().unwrap(); + { + let mut mind = Mind::open(state.path(), &slug).unwrap(); + let batch = PipelineAdmissionBatch { + instance: slug.clone(), + provenance: PipelineProvenance { faculty: Faculty::Hands, agent: "test".into(), session: "test".into(), tool: "test".into() }, + documents: seed(), + }; + assert!(matches!(mind.admit(batch, Utc::now()), PipelineAdmissionOutcome::Committed { .. })); + } + let mut writer = ControlWriter::open(control_path(Path::new(CONTROL_ROOT), &slug).unwrap()).unwrap(); + writer.set_brake(true, Utc::now(), "op").unwrap(); + let dial = BurnRate { heat: Decimal::from_str("1").unwrap(), base_cooldown_s: 600, base_run_usd: Decimal::from_str("5").unwrap(), set_at: Utc::now(), set_by: "op".into() }; + writer.set_dial(dial).unwrap(); + + let port = UdpSocket::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port(); + let endpoint = format!("rudp://127.0.0.1:{port}"); + let wake_exe = Path::new(env!("CARGO_BIN_EXE_mind-wake")); + let daemon_exe = wake_exe.with_file_name("huginn-daemon"); + assert!(daemon_exe.exists(), "build huginn-daemon first: {}", daemon_exe.display()); + let daemon = Daemon( + Command::new(&daemon_exe) + .args(["--state-root", state.path().to_str().unwrap(), "--instance", INSTANCE, "--bind", &format!("127.0.0.1:{port}")]) + .args(["--qdrant-url", "http://127.0.0.1:1", "--ollama-url", "http://127.0.0.1:1", "--embedding-model", "x"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .unwrap(), + ); + + // Exit 2 before the daemon answers has opened nothing; try again until it does. + let mut output = None; + for _ in 0..40 { + let attempt = Command::new(wake_exe).args(["--instance", INSTANCE, "--endpoint", &endpoint]).env("PATH", "").output().unwrap(); + if attempt.status.code() != Some(2) { + output = Some(attempt); + break; + } + std::thread::sleep(Duration::from_millis(500)); + } + let output = output.expect("the daemon never answered"); + assert_eq!(output.status.code(), Some(0), "{}", String::from_utf8_lossy(&output.stderr)); + assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "refused: start_failed"); + drop(daemon); + + let hostname = std::fs::read_to_string("/proc/sys/kernel/hostname").unwrap().trim().to_string(); + assert!(!hostname.is_empty()); + let mind = Mind::open(state.path(), &slug).unwrap(); + let runs = Selection { schemas: Some(vec![PipelineKind::Run.type_id().to_string()]), ..Selection::default() }; + let PipelinePageItems::Headers(headers) = mind.query(&runs).unwrap().items else { panic!("documents to a header selection") }; + assert_eq!(headers.len(), 1); + let view = mind.view(&headers[0].id).unwrap().unwrap(); + let PipelineDocument::Run(run) = &view.document else { panic!("not a run") }; + assert_eq!(run.host.0, hostname); + assert_eq!(view.admission.provenance.agent.0, "mind-wake"); +} From 0c5669ed85a8302ae8accc712036c1abb3acbaeb Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 20 Oct 2026 13:53:14 +0200 Subject: [PATCH 007/342] mind-wake: pin the cadence at the dial's maximum and document the container lane Adds the row (heat 0.05, base cooldown u32::MAX, 85,899,345,900 s) to the wait test so any ceiling on the wait fails, and gives the ignored host test its command in a crate README (the lane that kills the host() mutants). Tests and docs only; builds. Co-Authored-By: Claude Opus 5.5 --- crates/mind-body/README.md | 26 ++++++++++++++++++++++++++ crates/mind-body/src/wake/tests.rs | 7 +++++-- crates/mind-body/tests/wake.rs | 4 ++++ 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 crates/mind-body/README.md diff --git a/crates/mind-body/README.md b/crates/mind-body/README.md new file mode 100644 index 0000000..598ed78 --- /dev/null +++ b/crates/mind-body/README.md @@ -0,0 +1,26 @@ +# mind-body + +The Mind's body: the control store, the launch primitive and the `mind-wake` +one-shot. + +## The container lane + +`tests/wake.rs` holds one `#[ignore]`d test that runs the real `mind-wake` +binary against the real `huginn-daemon` and a real store at the brake's root, +and asserts that the host a run records is the kernel's hostname. It writes +`/etc/gamecult/minds`, so it runs in a container only (`/.dockerenv`), as root, +with the daemon built beside the binary. A plain `cargo test` skips it, and so +does `cargo mutants`, which therefore reports the three `host()` mutants in +`src/wake.rs` as missed. The lane that kills them: + +```sh +cargo build -p huginn-daemon -p mind-body --all-targets \ + && cargo test -p mind-body --test wake -- --include-ignored +``` + +On Yggdrasil, run it through the verify stopgap in the Rust image +(`ygg-verify.sh rust ''`). + +Mutation testing of the waker needs the glob form, because `-f src/wake.rs` +finds no mutants in this workspace: `cargo mutants -p mind-body -f +"**/src/wake.rs"`. diff --git a/crates/mind-body/src/wake/tests.rs b/crates/mind-body/src/wake/tests.rs index 3c32cd1..5e0e05c 100644 --- a/crates/mind-body/src/wake/tests.rs +++ b/crates/mind-body/src/wake/tests.rs @@ -176,8 +176,8 @@ fn with_the_brake_held_or_the_dial_absent_a_cycle_queries_nothing_admits_nothing fn the_wait_is_the_grants_cadence_after_the_newest_persona_turn_and_ends_exactly_there() { // (heat, base cooldown, the cadence base_cooldown / heat): whole, halved, // and one that carries nanoseconds, so an offset or scale shows. The last - // two sit beyond a second below and a day above (the dial allows both), so - // a floor or a ceiling on the wait shows too. + // three sit beyond a second below, a day above and at the dial's maximum + // (the dial allows all three), so a floor or a ceiling on the wait shows too. let heats = [ ("1", 600, seconds(600, 0)), ("2", 600, seconds(300, 0)), @@ -185,6 +185,9 @@ fn the_wait_is_the_grants_cadence_after_the_newest_persona_turn_and_ends_exactly ("0.05", 3600, seconds(72_000, 0)), ("2", 1, seconds(0, 500_000_000)), ("0.05", 10_000, seconds(200_000, 0)), + // The dial's maximum (base cooldown u32::MAX at heat 0.05): a ceiling + // anywhere below it, a week or a year included, changes the wait. + ("0.05", 4_294_967_295, seconds(85_899_345_900, 0)), ]; for (heat, cooldown, cadence) in heats { let rig = Rig::new(); diff --git a/crates/mind-body/tests/wake.rs b/crates/mind-body/tests/wake.rs index 9a97e3b..c535391 100644 --- a/crates/mind-body/tests/wake.rs +++ b/crates/mind-body/tests/wake.rs @@ -137,6 +137,10 @@ impl Drop for Daemon { /// daemon beside the binary. `PATH` is empty so no unit is ever reached: the /// run opens, its unit fails to start and the run is withdrawn, which is the /// outcome printed. +/// +/// `cargo test` and `cargo mutants` skip it, so the `host()` mutants survive +/// there. Run it in a container as root with the README's lane command +/// (`crates/mind-body/README.md`). #[test] #[ignore = "container only: root, /etc/gamecult/minds and a built huginn-daemon beside the binary"] fn the_real_bin_against_the_real_daemon_opens_a_run_that_records_this_machines_host_and_the_agent() { From c51a6043d259b6cb336c79192949b138ab197df2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 20 Oct 2026 14:09:36 +0200 Subject: [PATCH 008/342] mind-body README: host() lives in src/bin/mind-wake.rs; glob reaches it Fixes eureka-body:finding:cut-mind-wake.s3.readme-places-host-in-wake-rs. Co-Authored-By: Claude Opus 5.5 --- crates/mind-body/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/mind-body/README.md b/crates/mind-body/README.md index 598ed78..5838632 100644 --- a/crates/mind-body/README.md +++ b/crates/mind-body/README.md @@ -11,7 +11,7 @@ and asserts that the host a run records is the kernel's hostname. It writes `/etc/gamecult/minds`, so it runs in a container only (`/.dockerenv`), as root, with the daemon built beside the binary. A plain `cargo test` skips it, and so does `cargo mutants`, which therefore reports the three `host()` mutants in -`src/wake.rs` as missed. The lane that kills them: +`src/bin/mind-wake.rs` as missed. The lane that kills them: ```sh cargo build -p huginn-daemon -p mind-body --all-targets \ @@ -23,4 +23,5 @@ On Yggdrasil, run it through the verify stopgap in the Rust image Mutation testing of the waker needs the glob form, because `-f src/wake.rs` finds no mutants in this workspace: `cargo mutants -p mind-body -f -"**/src/wake.rs"`. +"**/src/wake.rs" -f "**/src/bin/mind-wake.rs"`. The second glob reaches +`host()`, whose mutants only the container lane kills. From 70d0cef421e9146ca05dc7e95d921724b1a95ec1 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 20 Oct 2026 15:33:51 +0200 Subject: [PATCH 009/342] Research: fork desk prior art and substrate (Eyes facts) Serves eureka-body:ruling:fork-desk-web-ui-on-huginn (session sp-reentry-2026-10-20). Co-Authored-By: Claude Opus 5.5 --- docs/research/fork-desk-prior-art.md | 205 +++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 docs/research/fork-desk-prior-art.md diff --git a/docs/research/fork-desk-prior-art.md b/docs/research/fork-desk-prior-art.md new file mode 100644 index 0000000..08f433f --- /dev/null +++ b/docs/research/fork-desk-prior-art.md @@ -0,0 +1,205 @@ +# Eureka fork desk: a web UI on Huginn where the operator answers forks + +Eyes pass, 2026-10-20, session `sp-reentry-2026-10-20`. Facts and evidence pointers only; no conclusion, no recommendation. + +Serves `eureka-body:ruling:fork-desk-web-ui-on-huginn` and `eureka-body:ruling:first-autonomous-minds`, which the coordinator reports is superseded by `eureka-body:ruling:first-autonomous-minds-shortlist` (admitted 2026-10-20T13:31:00Z, ordinal 3110, viewed 2026-10-20; the supersession Resolution document itself was not viewed). Rulings viewed at ordinals 3109 and 3110, authority Operator. Revision note: corrected the same day to cover all eight shortlisted Personas, not three. + +Repo state read: Huginn `0b3cac3` (2026-10-10, working tree on disk), CultLib `e9c563eb`, Heimdall `5952cce`, gamecult-ops `5c7dfc8`, gamecult-site `9bd39bb` (brand doc and quartz config), Eureka skill `979b190`. Web pages: "accessed 2026-10-20". Mind facts cite `eureka-state view` or `query` on instance `eureka`, `as_of` 3109. + +Not established, listed once here: (a) the content of the live `questions`/`answers` collections of the two artifacts (the data model below is read from the page code, not from stored rows); (b) whether the deployed Huginn daemon matches the on-disk Huginn `0b3cac3` (the on-disk schema index lists `epiphany.pipeline.*.v2`; `whoami` reports `schema_epoch epiphany.pipeline.epoch.v2`); (c) any design for an operator credential beyond the follow-up text; (d) any route from a browser to Huginn; (e) desk-ready pose sets for the six Personas other than Libby and Nibu (none in either desk artifact; branch cells exist for most, status unknown); (f) a campaign-to-Persona mapping, and a Huginn instance for any shortlisted Persona other than `eureka`; (g) operator verdicts on rounds 2-4 of the Flux busts (blank in the README). + +## 1. What exists today + +### 1.1 The two artifact desks + +Both read with the Artifact tool `read` action, 2026-10-20. + +- Nibu's desk: https://claude.ai/artifact/HccfYaXb84FMEPAWRyf8aY, artifact id `868b3ac5-762d-47f6-b1ae-c4ab1100d3c5`, version `1792103971-6b36`, title "Nibu's Fork Desk", 10 files (`index.html` 20340 bytes, `poses/nibu/{concerned,determined,happy,idle,neutral,note,surprised,thinking,triumph}.webp`), capabilities `{"db":{}}`, contract 0.2.81. +- CultLib desk: https://claude.ai/artifact/LBZMjcWDLjQtwkqJTLzfR4, artifact id `9b570364-a424-474f-853b-5243f8378363`, version `1792103848-7827`, title "Fork Desk", 19 files (`index.html` 20333 bytes, `poses/libby/*` and `poses/nibu/*`, the same nine moods each), capabilities `{"db":{}}`. +- The two `index.html` pages are the same program (a 7-byte size difference); the persona is chosen at runtime by the `desk/config` document, with a hard-coded default `libby` (script: `let persona = "libby"`, `PERSONAS = {libby, nibu}`). + +Data model, from the page script (both artifacts; page-owned `db` capability via `window.claude.use("db")`): + +| Collection / doc | Fields read or written | Writer | +|---|---|---| +| `desk/config` (single doc) | `persona` (key into `PERSONAS`), `kicker`, `title` | Self | +| `questions/` | `qid` (display id), `title`, `body` (pre-line text), `depends`, `flag` (provisional note), `rec` (recommended option key), `lean` (Self leans; shown only when different from `rec`), `options[]` of `{key, label, text}` (needs >= 2 or the doc is hidden), `status` (`open` default, `closed`), `ruling` (id shown in the "recorded" list), `order` (number sort key) | Self | +| `answers/` | `choice` (option key or null), `note` (free text), `question` (the qid), `updatedAt` (ISO string) | the page, as the viewer | + +Page behaviour (same script): radio choice plus an optional free-text note per question; the answer saves on pick and 900 ms after typing stops; a viewer who gets `invalid_argument` or `permission_denied` on save sees the desk go read-only; a single persona portrait sticks in a side booth (fixed bottom corner under 760 px) and changes among nine moods on events (pick equals `rec` shows `happy`, off-script pick shows `surprised`, all answered shows `triumph`, note focus shows `note`, save failure shows `concerned`); closed questions fold into a "recorded as rulings" list of `qid -> ruling`; the page states that changing an answer is fine "until it's admitted as a ruling", after which it becomes a new ruling in the session. Source: the `index.html` of each artifact, read 2026-10-20. + +How answers flow back (what Self does by hand), from memory note `C:\Users\Meta\.claude\projects\F--Projects-CultLib\memory\forks-go-on-the-desk.md` (modified 2026-10-17T16:25:53Z): +1. Self writes each open question to a desk once it is admitted and has had its prior-art pass. Doc id is the question label on Nibu's desk and the full question id on the CultLib desk; fields `rec`, `status: open`, plus `lean` and `flag` on the CultLib desk. +2. Self reads the operator's answers from the `answers` collection (the note says "read both desks' `answers`" when she says she answered). +3. Self admits each answer as a ruling in the mind (admission derives the `Answered` resolution of the question). +4. Self closes the desk doc with `status: closed` and the ruling id. +5. Before posting, Self searches the mind's rulings for the topic (semantic `query`), because a question was posted that a ruling had already retired (2026-10-17, `minor-power-geonames`). +The artifact store is therefore a second, page-local copy of each question and answer; the mind holds the typed `question` and `ruling` documents. Nothing in the artifact links back to the mind except the `qid` string and the `ruling` id Self writes by hand. + +Related in-house notes: +- `forks-for-understanding.md` (modified 2026-10-03, same directory): one fork at a time with context line, options, recommendation and what depends; never an "accept all" bundle; catch-up briefings read options from the `question` document, never from a handoff one-liner. Operator quote, 2026-09-29: "Even if I end up agreeing, I still have to understand the decisions being made. My decisions are the only reason anything gets done." +- `option-text-is-not-her-words.md` (2026-10-17): a pick endorses the core choice; a side clause in option text we wrote ("no galaxy presence") was built into a flag across three spec revisions and was not hers. Operator, 2026-10-17: "This is not the same as banning factions from appearing." +- `observation-is-not-a-ruling.md` (2026-10-14): a remark is not a ruling; `relay-operator-words-verbatim.md`: a ruling's `ruling` text says only what she said. +- The Eureka skill at `C:\Users\Meta\.claude\skills\eureka` (`979b190`) contains no match for "fork desk" (grep, 2026-10-20). + +### 1.2 The three rulings (full text, viewed 2026-10-20) + +`eureka-substrate:ruling:fork-desk-reusable` (admitted 2026-10-15T22:14:02Z, ordinal 2415, ruled_on 2026-10-16, authority Operator, session `self-2026-10-15-eureka`). +- operator_quote: "It would be a good idea to turn this into a reusable flow so we don't have to reinvent the wheel every time. As a Eureka feature it fits into the UX nicely, it's much more friendly than answering a list of forks in prose" +- ruling: After Self put five forks to her as a claude.ai artifact (cards with options, Libby's poses, answers saved to the page's store), the operator directed that this become a reusable Eureka flow. Her words decide that it is built and that it is a Eureka feature; the shape (one persistent desk Self feeds, persona poses, answers read back into rulings) is Self's design. + +`eureka-substrate:ruling:fork-desk-nibu-format` (admitted 2026-10-15T22:32:54Z, ordinal 2421, ruled_on 2026-10-16). +- operator_quote: "Nibu's fork desk actually turned out a bit more stylish than Libby's (build by the other Self), I'd prefer this format for the standardized version we're shipping, with a single character that follows the viewport and changes expressions on interaction" +- ruling: The operator chose the Aetheria Self's Nibu fork desk (artifact `HccfYaXb84FMEPAWRyf8aY` v2) over Self's Libby desk as the format: a single character that follows the viewport (sticky booth on desktop, fixed corner on phones) and changes expression on interaction, rather than one persona image per card. Relayed verbatim by the Aetheria/Ghostlight Self (session `self-2026-10-15-ag3`) on 2026-10-16; Self did not hear it directly. Refines `fork-desk-reusable`, which stays in force. + +`eureka-substrate:ruling:fork-desk-presenter-follows-work` (admitted 2026-10-15T22:38:24Z, ordinal 2422, ruled_on 2026-10-16). +- operator_quote: "That's not to say that the questions should be coming from Nibu. She was the one presenting because the work was Aetheria-related, and here the work was CultLib-related" +- ruling: Clarifying `fork-desk-nibu-format`: the standard takes Nibu's desk format, not Nibu as the presenter everywhere. Nibu presented because that work was Aetheria-related; CultLib work is presented by Libby. The presenter is chosen by the work's domain; the two names are her examples, not a closed mapping. + +The two rulings this pass serves: + +`eureka-body:ruling:fork-desk-web-ui-on-huginn` (admitted 2026-10-20T13:27:11Z, ordinal 3109, session `sp-reentry-2026-10-20`). +- operator_quote: "the fork desk thing is cool but we can do better than the artifact we have if this is gonna be a Eureka feature which I think it should be ... Thing 2.0 is still under construction, so I think a web UI that talks to Huginn is an acceptable compromise" +- ruling: Operator forks are answered through one Eureka fork desk, a web UI that reads and writes the mind through Huginn, rather than per-area artifacts. A web UI is accepted as a compromise because Thing 2.0, the intended interface substrate, is still under construction. Each fork is presented by the Persona whose domain it belongs to (`fork-desk-presenter-follows-work`). + +`eureka-body:ruling:first-autonomous-minds` (same admission). +- operator_quote: "Kiko is the StreamPixels persona, one of the few that made the short list to be in the first batch of autonomous Eureka minds. Libby and Nibu made the list, too, and the two fork desks are an improvised representation of the stewardship they're planned to have." +- ruling: Kiko is the StreamPixels Persona. Kiko, Libby and Nibu made the short list for the first batch of autonomous Eureka minds, each with its own Huginn instance under persona-per-mind. The two existing desks are an improvised representation of the stewardship those minds are planned to hold. + +`eureka-body:ruling:first-autonomous-minds-shortlist` (admitted 2026-10-20T13:31:00Z, ordinal 3110, session `sp-reentry-2026-10-20`; supersedes `first-autonomous-minds` per the coordinator's message). +- operator_quote: "Kiko is the StreamPixels persona, one of the few that made the short list to be in the first batch of autonomous Eureka minds. Libby and Nibu made the list, too, and the two fork desks are an improvised representation of the stewardship they're planned to have. ... Find the rest of the short list, we generated avatars for them, this is not a new thing" +- ruling: The first batch is the swarm shortlist that already has Flux avatars (`site-masthead:follow_up:cast-art-flux-portraits`; gamecult-site branch `self/cast-portraits`): Libby, Heimdall, Kiko, Aqua, Weksa, Nibu, Eureka and Void, each its own Huginn instance under persona-per-mind. Kiko is the StreamPixels Persona. The two existing desks are an improvised representation of the stewardship those minds are planned to hold. +- The superseded ruling's three-name list (Kiko, Libby, Nibu) is a subset of the eight. + +`site-masthead:ruling:cast-expression-library` (admitted 2026-10-13T23:47:52Z, ordinal 2104, ruled_on 2026-10-13). operator_quote: "the stickers I mentioned for the artifact are the avatars we generated with flux, I'd like to have a library of expressions for each short listed swarm character so they can use them in communications". Ruling: the stickers for the decision-page artifact are the Flux avatars of the swarm shortlist, not gamecult-site sprites; each shortlisted character gets a library of expressions for use in communications; generation path per `~/.claude/doctrine/images.md`. + +Adjacent rulings (viewed 2026-10-20): +- `eureka-body:ruling:persona-per-mind` (ruled 2026-10-09): a Persona that pushes its own domain gets its own Huginn instance, with its own dial share and waker turns; minds cooperate by `ForeignRef` and `HandOff`. Operator quote: "Second mind makes sense, it's the shape we were planning for already." First case named: Weksa (follow-up `eureka-body:follow_up:weksa-mind`). Kiko, Libby and Nibu instances: no instance or follow-up for them was found in the queries run (semantic query "persona-per-mind each Persona own Huginn instance stewardship autonomous", top 10 returned only the Weksa follow-up and Persona-state cuts). +- `eureka-substrate:ruling:mind-rules-which-forks` (ruled 2026-10-01, quote "On the others I take your recommendations."): the Mind's Self rules only forks whose consequences stay inside Huginn, the leaf, Eureka and its runbooks; consent, doctrine, spending, publishing, operator projects and live-mind upgrades stay the operator's, raised as questions. +- `persona-poses:ruling:reports-and-artifacts-consume` (2026-10-14): generated reports and operator-interaction artifacts consume the pose library as well as chat. `persona-poses:ruling:pose-home-ruled` (2026-10-14, choice `asset-catalogue`): each Persona's poses live in a typed asset catalogue that Persona state references by id. `persona-poses:target:r2` invariants: canon-is-source, one-vocabulary, no-edit-chains, provenance-beside-image, one-cutout-pipeline, persona-plus-label, one-catalogue, stable-hosting, report-and-chat-sizes. + +## 2. Huginn's surface for a web UI + +### 2.1 How a client reaches the mind + +- Transport: CultNet RUDP over UDP only. `crates/huginn-daemon/src/serve.rs:190-197` binds one `UdpSocket` and a `CultNetRudpServerHub`; no HTTP listener, no WebSocket, no TLS in the daemon (`serve.rs:120`: "this build carries no TLS", about the Qdrant/Ollama URLs). Grep of `serve.rs`, `daemon.rs`, `main.rs` for http/tcp finds only the outbound Qdrant/Ollama URLs. +- Live deployment: `docs/eureka-body-map.md:76-81` (F4): `huginn.service` on Yggdrasil, `--instance eureka --bind 10.77.0.1:17872`; UFW admits 17872/udp on `wg-gamecult` from 10.77.0.2 only (runbook `huginn-yggdrasil.md:38,124`). `mcp__eureka-state__whoami` on 2026-10-20: endpoint `rudp://10.77.0.1:17872`, `reachable: true`, 8951 documents, 3109 receipts, index `Current`, epoch `epiphany.pipeline.epoch.v2`. +- Client: `crates/eureka-state/src/lib.rs:1-22`: `HuginnClient::call` sends one `HuginnMindRequest` in the CultNet operation envelope over RUDP, one session per call, fetches a `Deferred` body on the same session; holds no state. MCP config: `C:\Users\Meta\.claude.json:1427` sets `HUGINN_ENDPOINT=rudp://10.77.0.1:17872`. CLI: `huginn` binary (`crates/eureka-state/src/main.rs`): whoami, view, query (raw and named recipes), admit from a file with read-back, schema. +- Operations (`crates/huginn-mind/src/wire.rs:63-100`): `Whoami`, `Admit(PipelineAdmissionBatch)`, `View{instance,id}`, `Query{instance,selection,semantic}`, plus Persona get/put (`HuginnMindResponse::Persona`, wire.rs:129). +- Limits that shape a client (`serve.rs:30-53`): a session idle 30 s is removed server-side; one send carries at most 1,228,800 encoded bytes, larger answers up to `MAX_DEFERRED_BODY_BYTES` go as a `Deferred` manifest fetched through `cultmesh.content_chunk_request.v1`; two large reads pipelined on one session are not both answered; client request limit `MAX_REQUEST_BYTES = 65,507 - 36 - 6` (`eureka-state/src/lib.rs:36`). +- No peer authentication: `docs/eureka-body-map.md:81`: "The daemon has no peer authentication (`serve.rs`: one session per peer, `max_peers = 256`); any reachable peer admits as any faculty." Admission provenance (agent, faculty, session, tool) is declared by the writer (the `provenance` object on every `view` result above is client-supplied; `huginn-mind/src/admission.rs:227` shows one provenance authority check on persona state only). +- Browser-side CultLib pieces that exist: `F:\Projects\CultLib\packages\cultmesh-browser` (README: one WebSocket session to a Verse/authority runtime resolved through configured Odin WebSocket endpoints; leases provider-owned documents; "does not expose direct document writes"; invokes provider-owned operations) and `packages\cultnet-ts` (README line 8: not an HTTP wrapper; RUDP socket transport for non-browser runtimes). Whether Odin or any other component bridges a browser WebSocket to the Huginn RUDP port: not found in the files read. Huginn publishes no Eve/Thing surface and no CultMesh provider document that was found (README and AGENTS.md describe only the RUDP operation envelope and the schema catalog). +- The TS CultMesh content plane (HTTPS provider and verifying reader) is released per the StreamPixels memory index (`cultnet-ts-v0.3.0`, `cultmesh-ts-v0.2.0`, `cultmesh-browser-v0.4.0`, CultLib main `b9497d35`); not read in this pass beyond the memory index line. + +### 2.2 The `question` and `ruling` schemas and admission rules + +Leaf definitions, `F:\Projects\Huginn\crates\eureka-pipeline\src\lib.rs`, Huginn `0b3cac3`: +- `PipelineQuestion` (lib.rs:634-636): `campaign: Slug`, `label: Label` (`[A-Za-z0-9_-]{1,64}`), `title: Title`, `question: Para` (<= 4000 bytes), `options: Vec` (max 8), `recommended: Label`, `depends: Vec` (max 8, Line <= 1000 bytes), `raised_in: Option`, `asked_on: Date`. `QuestionOption { label: Label, text: Line }` (lib.rs:585). There is no field for a presenter, Persona, `lean`, `flag` or display order; the campaign slug is the only domain key on the document. `eureka-state` query headers return `options` as labels only; `view` returns the full document. +- `PipelineRuling` (lib.rs:637-639): `campaign`, `label`, `title`, `answers: Option` (question id), `choice: Option