From 9c58d5620f21d16cfd1fcaf1b3bed55df9c488a3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 1 Nov 2026 12:29:55 +0100 Subject: [PATCH] The gate reads the hold itself at admission; the heartbeat no longer writes resolutions on an answer open_and_launch decides what an open question holds, once, after the breaker reads and just before the admission: LaunchRequest.held and Held's Default are gone, so no caller can launch a held item and a caller's earlier read (claimable) is advisory only. launch::retire_answered and its heartbeat call are deleted: whoever answers retire resolves the claims, and the hold persists until they do. The retire option text is restored to the r4 wording. Co-Authored-By: Claude Opus 5.5 --- crates/mind-body/src/control/tests.rs | 2 +- crates/mind-body/src/heartbeat.rs | 27 +-- crates/mind-body/src/heartbeat/tests.rs | 31 +--- crates/mind-body/src/launch.rs | 208 +++++++++------------- crates/mind-body/src/persona.rs | 4 +- crates/mind-body/src/persona/tests.rs | 2 +- crates/mind-body/src/queue.rs | 8 +- crates/mind-body/tests/public_surface.txt | 4 +- 8 files changed, 110 insertions(+), 176 deletions(-) diff --git a/crates/mind-body/src/control/tests.rs b/crates/mind-body/src/control/tests.rs index 12d6429..eee43a8 100644 --- a/crates/mind-body/src/control/tests.rs +++ b/crates/mind-body/src/control/tests.rs @@ -131,7 +131,7 @@ mod grant { fn launch_as(mind_instance: &str, grant: Grant, turn: RunTurn) -> Result { let mind = SilentMind(slug(mind_instance)); let ports = Ports { mind: &mind, launcher: &NoUnits, clock: &Now, host: "host" }; - open_and_launch(&ports, LaunchRequest { turn, claims: vec![], agent: "agent".into(), grant, held: crate::queue::Held::default() }) + open_and_launch(&ports, LaunchRequest { turn, claims: vec![], agent: "agent".into(), grant }) } #[test] diff --git a/crates/mind-body/src/heartbeat.rs b/crates/mind-body/src/heartbeat.rs index 21de31e..4c655bc 100644 --- a/crates/mind-body/src/heartbeat.rs +++ b/crates/mind-body/src/heartbeat.rs @@ -41,8 +41,8 @@ use huginn_mind::{PipelineDocumentSummary, PipelineFacts, PipelinePageItems}; use crate::chat_client::{Chat, ChatDelivery}; use crate::control::{ControlSource, Effective, FileSource, HeldReason, SwarmEffective, read_effective, read_envelope, set_mode_world_readable}; -use crate::launch::{Clock, LaunchOutcome, LaunchRequest, Launcher, Ports, holders, open_and_launch, retire_answered, slot}; -use crate::queue::{Held, MindPort, any_of, citing, claims_of, headers, of_kind, on_hold}; +use crate::launch::{Clock, LaunchOutcome, LaunchRequest, Launcher, Ports, holders, open_and_launch, slot}; +use crate::queue::{MindPort, any_of, citing, claims_of, headers, of_kind, on_hold}; use crate::self_run::inbox; use crate::tempo::{self, Standing}; @@ -574,7 +574,6 @@ fn launch( row: &Row, turn: RunTurn, claims: Vec, - held: Held, ) -> Result { let Effective::Released(grant) = organs.control.effective(row.slug()) else { return Ok(Launched::Refused("the brake is held".into())); @@ -584,7 +583,6 @@ fn launch( claims, agent: AGENT.into(), grant, - held, }; let ports = ports(organs, row.seat.mind); Ok(match open_and_launch(&ports, request) { @@ -608,8 +606,6 @@ struct Claimable { claims: Vec, /// How many briefs an open question holds. held: usize, - /// The hold they were chosen by, which the launch consults and reads no second time. - hold: Held, /// Self runs in force whose unit is gone: the launch closes them. dead: Vec, } @@ -621,11 +617,10 @@ struct Claimable { /// run, not the Mind, until the operator answers or withdraws its question, and /// the count starts afresh. Whether a holder lives is `launch::slot`'s answer /// (a dead one is closed by the launch, so it does not stop the successor), and -/// what is held is `queue::on_hold`'s, read once here and carried into the -/// launch; nothing here decides either. +/// what is held is `queue::on_hold`'s. The read here only chooses the claims; +/// the launch's gate reads the hold again at admission and decides. fn claimable(organs: &Organs, row: &Row) -> Result { let mind = row.seat.mind; - let hold = on_hold(mind)?; let found = slot( &ports(organs, mind), row.slug(), @@ -636,11 +631,10 @@ fn claimable(organs: &Organs, row: &Row) -> Result { return Ok(Claimable { claims: Vec::new(), held: 0, - hold, dead: Vec::new(), }); } - let (free, held) = inbox::derive_counting_held(mind, &hold, &HashSet::new())?; + let (free, held) = inbox::derive_counting_held(mind, &on_hold(mind)?, &HashSet::new())?; let claims = free .into_iter() .map(|brief| PipelineRef { kind: PipelineKind::FollowUp, id: brief.id.as_str().into() }) @@ -649,7 +643,6 @@ fn claimable(organs: &Organs, row: &Row) -> Result { Ok(Claimable { claims, held, - hold, dead: found.dead, }) } @@ -718,7 +711,7 @@ pub fn poll(organs: &Organs, config: &Config) -> Result { if mentioned.is_none() && !self_ended { continue; } - match launch(organs, row, RunTurn::PersonaTurn, vec![], Held::default())? { + match launch(organs, row, RunTurn::PersonaTurn, vec![])? { Launched::Run(label) => { if let Some(key) = mentioned { state.handled_mentions.push(key); @@ -757,21 +750,15 @@ pub fn poll(organs: &Organs, config: &Config) -> Result { let mut passed: Vec = standings.clone(); while due && let Some(winner) = tempo::pick(&passed, beat_s) { // Her Self holds briefs: the beat is a Self run for them; otherwise a Persona turn. - // An answer of retire is written before the briefs are read, so a retired brief is resolved, not claimed. - if let Err(error) = retire_answered(rows[winner].seat.mind, AGENT, now) { - rows[winner].note = format!("retire: {error}"); - } let Claimable { claims, held, - hold, dead, } = claimable(organs, &rows[winner]).unwrap_or_else(|error| { rows[winner].note = format!("briefs: {error}"); Claimable { claims: Vec::new(), held: 0, - hold: Held::default(), dead: Vec::new(), } }); @@ -794,7 +781,7 @@ pub fn poll(organs: &Organs, config: &Config) -> Result { } else { RunTurn::SelfRun }; - match launch(organs, &rows[winner], turn, claims, hold)? { + match launch(organs, &rows[winner], turn, claims)? { Launched::Run(label) => { state.last_beat_at = now; rows[winner].busy = turn == RunTurn::PersonaTurn; diff --git a/crates/mind-body/src/heartbeat/tests.rs b/crates/mind-body/src/heartbeat/tests.rs index 8ef4e43..bab1bb3 100644 --- a/crates/mind-body/src/heartbeat/tests.rs +++ b/crates/mind-body/src/heartbeat/tests.rs @@ -1209,35 +1209,20 @@ fn one_held_brief_says_so_in_the_trace() { } #[test] -fn an_answer_of_retire_resolves_the_bundle_and_nothing_relaunches() { +fn an_answer_of_retire_resolves_nothing_and_the_hold_stays_until_the_answerer_resolves() { let (rig, source) = rig_with_a_persona_turn(); let items = failed_bundle(&rig, &source); beats(&rig, 3); - let question = rig.minds[ANN] - .ids(K::Question) - .pop() - .expect("the trip asked"); + let question = rig.minds[ANN].ids(K::Question).pop().expect("the trip asked"); assert_eq!(self_runs(&rig, ANN).len(), 3); - rig.minds[ANN].committed(vec![crate::testkit::ruling( - "answer", - Some((&question, "retire")), - )]); + rig.minds[ANN].committed(vec![crate::testkit::ruling("answer", Some((&question, "retire")))]); beats(&rig, 6); - assert_eq!( - self_runs(&rig, ANN).len(), - 3, - "no Self run after the answer: retire does not relaunch" - ); + assert_eq!(self_runs(&rig, ANN).len(), 3, "no Self run after the answer: retire does not relaunch"); for item in &items { let view = rig.minds[ANN].view(K::FollowUp, &item.id.0).unwrap(); - assert!( - matches!(view.status, huginn_mind::PipelineStatus::Resolved { record, .. } if matches!(record.outcome, eureka_pipeline::ResolutionOutcome::Withdrawn { .. })), - "{item:?} is resolved Withdrawn" - ); + assert!(matches!(view.status, huginn_mind::PipelineStatus::InForce), "{item:?}: the heartbeat resolves nothing on an answer"); } - assert_eq!( - rig.minds[ANN].ids(K::Question).len(), - 1, - "and no second question" - ); + assert!(rig.minds[ANN].ids(K::Resolution).is_empty(), "no machine writer of a resolution"); + assert_eq!(rig.minds[ANN].ids(K::Question).len(), 1, "and no second question"); + assert!(inbox::derive(&rig.minds[ANN], &HashSet::new()).unwrap().is_empty(), "the retired claims stay held and untold"); } diff --git a/crates/mind-body/src/launch.rs b/crates/mind-body/src/launch.rs index 8ca14b9..30c1db5 100644 --- a/crates/mind-body/src/launch.rs +++ b/crates/mind-body/src/launch.rs @@ -3,7 +3,7 @@ //! `open_and_launch` is the whole path, in this order: (0) the in-force runs of //! hers of the request's turn are read through `slot`, which tells a live one //! (its unit active, or admitted within `START_GRACE_S`: it answers Busy) from a -//! dead one (to be closed Recorded); (1) every cut-spec claim must be in the queue; (2) no claim may be held by an open question (the `Held` snapshot the request carries), and none may +//! dead one (to be closed Recorded); (1) every cut-spec claim must be in the queue; (2) no claim may be held by an open question (the gate reads the hold itself, once, after the breaker reads and just before the admission, and takes no caller's word for it), and none may //! trip the repetition breaker (a trip raises one question in the run whose end //! tripped it, which holds that run's claims); (3) the run is admitted, in the one batch that //! closes the dead holders, so liveness is decided by the in-force record alone @@ -27,13 +27,12 @@ use eureka_pipeline::{ RunOperator, RunTurn, Slug, pipeline_key, }; use huginn_mind::{ - BATCH_MAX, MindRefusal, PipelineAdmissionOutcome, PipelineDocumentSummary, PipelineFacts, + MindRefusal, PipelineAdmissionOutcome, PipelineDocumentSummary, PipelineFacts, }; use crate::control::Grant; use crate::queue::{ - Breaker, Held, MindPort, any_of, breaker, citing, claims_of, headers, holds, in_force, of_kind, - queue, + Breaker, MindPort, any_of, breaker, citing, claims_of, headers, in_force, of_kind, on_hold, queue, }; use eureka_state::hold::RETIRE; @@ -227,10 +226,6 @@ pub struct LaunchRequest { /// The reading of a released instance brake and a released swarm brake. /// Taken by value, so one reading opens at most one run. pub grant: Grant, - /// The hold the caller chose its claims by, read once with `on_hold`: the - /// gate refuses against it and reads it no second time. A Persona turn - /// claims nothing, so it carries the empty one. - pub held: Held, } /// Why admission did not open the run: its own refusal, or one of the two @@ -365,7 +360,6 @@ pub fn open_and_launch(ports: &Ports, request: LaunchRequest) -> Result Result = None; + for item in &claims { + if let Breaker::Trip { run } = breaker(ports.mind, item, &ending)? { + tripped = Some((item, run)); + break; + } + } + let held = on_hold(ports.mind)?; let open = queue(ports.mind, &instance, &ending, &held)?; if let Some(item) = claims.iter().find(|claim| { held.contains(claim) || (claim.kind == PipelineKind::CutSpec && !open.contains(claim)) }) { return Ok(LaunchOutcome::NotQueued { item: item.clone() }); } - for item in &claims { - if let Breaker::Trip { run } = breaker(ports.mind, item, &ending)? { - let count = claims_of(ports.mind, &run)?.len(); - let question = - PipelineDocument::Question(breaker_question(item, &run, count, &label, now)); - let id = PipelineRef { - kind: PipelineKind::Question, - id: key_of(&question)?.as_str().into(), - }; - batch.push(question); - return Ok(match committed(ports.mind.admit(&agent, &label.0, batch)?) { - Ok(()) => LaunchOutcome::Tripped { run, question: id }, - Err(declined) => LaunchOutcome::Refused(declined), - }); - } + if let Some((item, run)) = tripped { + let count = claims_of(ports.mind, &run)?.len(); + let question = + PipelineDocument::Question(breaker_question(item, &run, count, &label, now)); + let id = PipelineRef { + kind: PipelineKind::Question, + id: key_of(&question)?.as_str().into(), + }; + batch.push(question); + return Ok(match committed(ports.mind.admit(&agent, &label.0, batch)?) { + Ok(()) => LaunchOutcome::Tripped { run, question: id }, + Err(declined) => LaunchOutcome::Refused(declined), + }); } } @@ -478,34 +481,6 @@ fn committed(outcome: PipelineAdmissionOutcome) -> Result<(), Declined> { } } -/// Resolves, Withdrawn, every item an answer `retire` keeps held that is still -/// in force, in the admissions of one batch each: the one writer of a -/// retirement, so that a retired claim leaves the follow-up lists and the queue -/// instead of waiting held for ever. Returns how many it resolved. -pub(crate) fn retire_answered(mind: &dyn MindPort, agent: &str, now: DateTime) -> Result { - let retired: Vec = holds(mind)? - .into_iter() - .filter(|hold| hold.retired) - .flat_map(|hold| hold.items) - .collect(); - let label = run_label(now); - let mut resolved = 0; - for chunk in retired.chunks(BATCH_MAX) { - let mut batch = Vec::new(); - for item in chunk { - let outcome = ResolutionOutcome::Withdrawn { - reason: "retired by the answer to a breaker question".into(), - }; - batch.push(closure(mind, item, now, outcome, "The operator answered the breaker question for the run that claimed this item with retire, so it is no longer worked.")?); - } - match committed(mind.admit(agent, &format!("retire-{}", label.0), batch)?) { - Ok(()) => resolved += chunk.len(), - Err(_) => return Err(anyhow!("the retired claims could not be resolved")), - } - } - Ok(resolved) -} - /// `breaker-`: one question per trip, since each trip is a launch /// with a label of its own. fn breaker_label(label: &Label) -> Label { @@ -520,14 +495,14 @@ fn breaker_question(claim: &PipelineRef, run: &PipelineRef, held: usize, label: label: breaker_label(label), title: "Repeated runs on the same work admitted nothing".into(), question: format!( - "Run {} and the runs before it left nothing that cites the work it held (claims held: {held}), so none of it launches again. Answering with reopen, or withdrawing this, reopens every claim of that run with a fresh count; answering with retire resolves every claim of it Withdrawn.", + "Run {} and the runs before it left nothing that cites the work it held (claims held: {held}), so none of it launches again. Answering with reopen, or withdrawing this, reopens every claim of that run with a fresh count. To stop working them, resolve the claims, then answer with retire.", run.id.0 ) .as_str() .into(), options: vec![ QuestionOption { label: "reopen".into(), text: "Answer to reopen every claim of the run with a fresh count.".into() }, - QuestionOption { label: RETIRE.into(), text: "Answer to retire every claim of the run: each is resolved Withdrawn and none is worked again.".into() }, + QuestionOption { label: RETIRE.into(), text: "Resolve the claims so they are no longer worked, and answer with this option.".into() }, ], recommended: "reopen".into(), depends: vec![], @@ -575,7 +550,6 @@ mod tests { claims: claims.to_vec(), agent: "agent-x".into(), grant, - held: crate::queue::on_hold(mind)?, }, ) } @@ -956,6 +930,15 @@ mod tests { let PipelineDocument::Question(asked) = &view.document else { panic!("not a question") }; assert_eq!((asked.raised_in.clone(), asked.campaign.0.as_str()), (Some(run_ref("h2")), CAMPAIGN), "raised in the run whose end tripped it"); assert!(asked.question.0.contains(&run_ref("h2").id.0) && asked.question.0.contains("claims held: 1"), "{}", asked.question.0); + let texts: Vec<(&str, &str)> = asked.options.iter().map(|option| (option.label.0.as_str(), option.text.0.as_str())).collect(); + assert_eq!( + texts, + [ + ("reopen", "Answer to reopen every claim of the run with a fresh count."), + ("retire", "Resolve the claims so they are no longer worked, and answer with this option.") + ], + "retire is resolved by its answerer: no option promises the machine will" + ); assert_eq!(view.admission.provenance.session.0, "mind-20261009T131500000Z"); // Open, the question blocks the item. assert_eq!(rig.launch(RunTurn::SelfRun, &[spec_ref("x")]), LaunchOutcome::NotQueued { item: spec_ref("x") }); @@ -1021,6 +1004,34 @@ mod tests { assert_eq!((rig.runs(), rig.started()), (3, 0)); } + /// A caller's earlier read of the hold (the heartbeat's, choosing its claims) + /// is advisory: a question that lands after it and before the gate is seen. + #[test] + fn a_question_landing_before_the_gate_reads_is_seen_by_the_run_it_would_have_opened() { + let rig = Rig::new(); + rig.mind.committed(vec![spec("x"), follow_up("brief", &spec_ref("x"))]); + let brief = reference(K::FollowUp, &id("follow_up", "brief")); + let landing = Landing { + inner: &rig.mind, + question: RefCell::new(Some(vec![question_in("fork", &brief)])), + hold_reads: Cell::new(0), + lands_on: reads_anything, + }; + let outcome = rig.launch_through(&landing, RunTurn::SelfRun, std::slice::from_ref(&brief)); + assert_eq!(outcome, LaunchOutcome::NotQueued { item: brief }); + assert_eq!((landing.hold_reads.get(), rig.runs(), rig.started()), (1, 0, 0), "one read, nothing admitted"); + } + + #[test] + fn a_brief_an_open_question_is_raised_in_cannot_be_launched_on() { + let rig = Rig::new(); + rig.mind.committed(vec![spec("x"), follow_up("brief", &spec_ref("x"))]); + let brief = reference(K::FollowUp, &id("follow_up", "brief")); + rig.mind.committed(vec![question_in("fork", &brief)]); + assert_eq!(rig.launch(RunTurn::SelfRun, std::slice::from_ref(&brief)), LaunchOutcome::NotQueued { item: brief }); + assert_eq!((rig.runs(), rig.started()), (0, 0)); + } + #[test] fn the_gate_refuses_a_claim_an_open_question_holds_whatever_its_kind() { let rig = Rig::new(); @@ -1038,11 +1049,21 @@ mod tests { assert!(matches!(rig.launch(RunTurn::SelfRun, &[brief]), LaunchOutcome::Launched { .. }), "withdrawn, the hold lifts"); } - /// A mind where a question lands the moment the hold has been read once. + /// A mind where a question lands the moment `lands_on` reads a selection. struct Landing<'a> { inner: &'a TestMind, question: RefCell>>, hold_reads: Cell, + lands_on: fn(&Selection) -> bool, + } + + fn reads_the_hold(selection: &Selection) -> bool { + selection.schemas.as_deref() == Some(&[K::Question.type_id().to_string()][..]) + && selection.fields.iter().flatten().any(|predicate| predicate.index == "in_force") + } + + fn reads_anything(_: &Selection) -> bool { + true } impl MindPort for Landing<'_> { @@ -1052,18 +1073,13 @@ mod tests { fn query(&self, selection: &Selection) -> Result { let answer = self.inner.query(selection); - let reads_the_hold = selection.schemas.as_deref() - == Some(&[K::Question.type_id().to_string()][..]) - && selection - .fields - .iter() - .flatten() - .any(|predicate| predicate.index == "in_force"); - if reads_the_hold { + if reads_the_hold(selection) { self.hold_reads.set(self.hold_reads.get() + 1); - if let Some(documents) = self.question.take() { - self.inner.committed(documents); - } + } + if (self.lands_on)(selection) + && let Some(documents) = self.question.take() + { + self.inner.committed(documents); } answer } @@ -1087,8 +1103,9 @@ mod tests { inner: &rig.mind, question: RefCell::new(Some(vec![question_in("fork", &spec_ref("x"))])), hold_reads: Cell::new(0), + lands_on: reads_the_hold, }; - // The request's snapshot is read first (the question lands behind it); the queue and the gate use that one. + // The gate's own snapshot is read first (the question lands behind it); the queue and the gate use that one. let outcome = rig.launch_through(&landing, RunTurn::SelfRun, &[spec_ref("x")]); assert!( matches!(outcome, LaunchOutcome::Launched { .. }), @@ -1106,60 +1123,6 @@ mod tests { ); } - #[test] - fn retiring_resolves_every_claim_an_answer_of_retire_holds_once() { - let rig = Rig::new(); - rig.mind - .committed(vec![spec("x"), follow_up("brief", &spec_ref("x"))]); - let brief = reference(K::FollowUp, &id("follow_up", "brief")); - rig.mind.committed(vec![run( - "failed", - RunTurn::SelfRun, - RunOperator::Mind, - &[brief.clone(), spec_ref("x")], - )]); - rig.mind - .committed(vec![close(run_ref("failed"), recorded())]); - rig.mind - .committed(vec![question_in("fork", &run_ref("failed"))]); - let question = id("question", "fork"); - assert_eq!( - retire_answered(&rig.mind, "agent-x", rig.clock.now()).unwrap(), - 0, - "an open question retires nothing" - ); - rig.mind - .committed(vec![ruling("answer", Some((&question, "reopen")))]); - assert_eq!( - retire_answered(&rig.mind, "agent-x", rig.clock.now()).unwrap(), - 0, - "reopen retires nothing" - ); - rig.mind - .committed(vec![question_in("fork2", &run_ref("failed"))]); - rig.mind.committed(vec![ruling( - "answer2", - Some((&id("question", "fork2"), "retire")), - )]); - assert_eq!( - retire_answered(&rig.mind, "agent-x", rig.clock.now()).unwrap(), - 2 - ); - for item in [&brief, &spec_ref("x")] { - let view = rig.mind.view(item.kind, &item.id.0).unwrap(); - assert!( - matches!(view.status, PipelineStatus::Resolved { record, .. } if matches!(record.outcome, ResolutionOutcome::Withdrawn { .. })), - "{item:?}" - ); - } - assert_eq!( - retire_answered(&rig.mind, "agent-x", rig.clock.now()).unwrap(), - 0, - "resolved, nothing is left to retire" - ); - assert!(rig.mind.ids(K::Run).len() == 1, "and nothing launched"); - } - /// The one source of every drawn case: splitmix64 from a fixed seed, so a /// failure names a case that runs again. struct Draw(u64); @@ -1291,7 +1254,6 @@ mod tests { claims, agent: "agent-x".into(), grant, - held: Held::default(), }, ); assert!(launcher.started.borrow().is_empty()); diff --git a/crates/mind-body/src/persona.rs b/crates/mind-body/src/persona.rs index 0e94ed9..e72c8ef 100644 --- a/crates/mind-body/src/persona.rs +++ b/crates/mind-body/src/persona.rs @@ -59,7 +59,7 @@ use crate::connector::render_plan; use crate::control::{ControlSource, Effective, HeldReason, derive}; use crate::heartbeat::{PollTrace, TempoView}; use crate::launch::{Clock, LaunchOutcome, LaunchRequest, Launcher, Ports, holders, open_and_launch}; -use crate::queue::{MindPort, any_of, citing, headers, in_force, live_claims, of_kind, on_hold}; +use crate::queue::{MindPort, any_of, citing, headers, in_force, live_claims, of_kind}; use crate::recall; /// The admitting agent's name on everything this organ admits. @@ -955,7 +955,7 @@ fn start_self_run( } let Effective::Released(grant) = read(control, swarm, organs.mind.instance()) else { return Ok(Err(Failure::SelfRun)) }; let ports = Ports { mind: organs.mind, launcher: organs.launcher, clock: organs.clock, host: organs.host }; - let request = LaunchRequest { turn: RunTurn::SelfRun, claims: vec![claim], agent: AGENT.into(), grant, held: on_hold(organs.mind)? }; + let request = LaunchRequest { turn: RunTurn::SelfRun, claims: vec![claim], agent: AGENT.into(), grant }; Ok(match open_and_launch(&ports, request)? { LaunchOutcome::Launched { run } | LaunchOutcome::Busy { run } => Ok(run), _ => Err(Failure::SelfRun), diff --git a/crates/mind-body/src/persona/tests.rs b/crates/mind-body/src/persona/tests.rs index 9c5dac9..566a3eb 100644 --- a/crates/mind-body/src/persona/tests.rs +++ b/crates/mind-body/src/persona/tests.rs @@ -227,7 +227,7 @@ impl Rig { let effective = read(&FileSource::new(self.store()), &FileSource::new(self.swarm_store()), self.mind.instance()); if let Effective::Released(grant) = effective { let ports = Ports { mind: &self.mind, launcher: &Recording::default(), clock: &self.clock, host: "yggdrasil-host" }; - let request = LaunchRequest { turn: RunTurn::PersonaTurn, claims: vec![], agent: "mind-heartbeat".into(), grant, held: crate::queue::Held::default() }; + let request = LaunchRequest { turn: RunTurn::PersonaTurn, claims: vec![], agent: "mind-heartbeat".into(), grant }; assert!(matches!(open_and_launch(&ports, request).unwrap(), LaunchOutcome::Launched { .. })); } } diff --git a/crates/mind-body/src/queue.rs b/crates/mind-body/src/queue.rs index 87a4329..e717f16 100644 --- a/crates/mind-body/src/queue.rs +++ b/crates/mind-body/src/queue.rs @@ -151,10 +151,10 @@ pub(crate) fn holds(mind: &dyn MindPort) -> Result> { hold::holds(&|asking| mind.query(asking)) } -/// A snapshot of the items an open question holds, read once by whoever -/// launches and passed to everything that launch consults, so the gate, the -/// queue and the claims they chose cannot answer differently within one launch. -#[derive(Clone, Debug, Default, PartialEq, Eq)] +/// A snapshot of the items an open question holds. Only `on_hold` makes one +/// (there is no empty default), and the launch gate reads its own, once, at +/// admission; a reader that chooses claims by one is advisory. +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Held(Vec); impl Held { diff --git a/crates/mind-body/tests/public_surface.txt b/crates/mind-body/tests/public_surface.txt index cf8fc6c..ecc435b 100644 --- a/crates/mind-body/tests/public_surface.txt +++ b/crates/mind-body/tests/public_surface.txt @@ -128,7 +128,7 @@ mind_body::launch: impl SystemdLauncher: fn stop_all (& self , instance : & Slug mind_body::launch: pub trait Clock { fn now (& self) -> DateTime < Utc > ; } mind_body::launch: pub trait Launcher { fn start (& self , instance : & Slug , turn : RunTurn) -> Result < () > ; fn alive (& self , instance : & Slug , turn : RunTurn) -> bool ; } mind_body::launch: pub trait Systemctl { fn run (& self , args : & [& str]) -> Result < SystemctlReply > ; } -mind_body::launch: struct LaunchRequest pub-fields[turn: RunTurn, claims: Vec < PipelineRef >, agent: String, grant: Grant, held: Held] +mind_body::launch: struct LaunchRequest pub-fields[turn: RunTurn, claims: Vec < PipelineRef >, agent: String, grant: Grant] mind_body::launch: struct Ports< 'a > pub-fields[mind: & 'a dyn MindPort, launcher: & 'a dyn Launcher, clock: & 'a dyn Clock, host: & 'a str] mind_body::launch: struct SystemClock pub-fields[] mind_body::launch: struct SystemctlReply pub-fields[success: bool, code: Option < i32 >, stdout: String] @@ -210,7 +210,7 @@ mind_body::queue: impl HuginnMind < 'static >: fn new (client : HuginnClient) -> 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 query_near (& self , _selection : & Selection , _text : & str , _top_k : u32) -> Result < PipelineSelectionPage > { bail ! ("this mind port has no semantic index") } fn admit (& self , agent : & str , session : & str , documents : Vec < PipelineDocument >) -> Result < PipelineAdmissionOutcome > ; } -mind_body::queue: struct Held # [derive (Clone , Debug , Default , PartialEq , Eq)] pub-fields[] +mind_body::queue: struct Held # [derive (Clone , Debug , PartialEq , Eq)] pub-fields[] mind_body::queue: struct HuginnMind< 'a > pub-fields[] mind_body::recall: impl Lane mind_body::recall: impl Recalled