diff --git a/crates/eureka-pipeline/src/lib.rs b/crates/eureka-pipeline/src/lib.rs index 36b1e5b..54c2f43 100644 --- a/crates/eureka-pipeline/src/lib.rs +++ b/crates/eureka-pipeline/src/lib.rs @@ -736,10 +736,29 @@ unit_enums! { RulingAuthority { Operator, Standing, Defaulted, Mind } FindingOrigin { Introduced, PreExisting } RunOperator { Operator, Mind } - RunTurn { PersonaTurn, SelfRun } + RunTurn { PersonaTurn, SelfRun, Sleep } MemorySort { Asked, Decided, Committed, Learned, Did, Idea, Rapport } } +/// The slots of a Mind's runs: one live run of hers per instance and slot. A +/// Persona session and her Sleep run share the Persona slot (unit +/// `mind-persona@`); a Self run holds the other. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RunSlot { + Persona, + SelfRun, +} + +impl RunTurn { + /// The slot this turn holds while live; admission and launch both read it here. + pub fn slot(self) -> RunSlot { + match self { + Self::PersonaTurn | Self::Sleep => RunSlot::Persona, + Self::SelfRun => RunSlot::SelfRun, + } + } +} + value_types! { pub struct CodeLocation { path: Short, line: u32, end_line: Option, symbol: Option = absent } pub struct CommitRange { base: Sha, head: Sha } @@ -1004,6 +1023,8 @@ pub enum MemoryOrigin { Wake, /// A Self run of hers, by its label. SelfRun { run: Short }, + /// Her Sleep run, by its label: a memory she formed in her night. + Sleep { run: Short }, } impl Bounded for MemoryOrigin { @@ -1018,7 +1039,7 @@ impl Bounded for MemoryOrigin { list(&format!("{field}.with"), with, 8) } Self::Wake => Ok(()), - Self::SelfRun { run } => run.validate(&format!("{field}.run")), + Self::SelfRun { run } | Self::Sleep { run } => run.validate(&format!("{field}.run")), } } } diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index 13d6d4a..710b38a 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -31,7 +31,7 @@ use std::collections::BTreeSet; use chrono::{DateTime, Utc}; use cultcache_rs::{CultCache, CultCacheEnvelope}; use eureka_pipeline::{ - ClaimOutcome, FindingConfidence, Line, PipelineDocument, PipelineKind, PipelineRef, PipelineRefusal, + ClaimOutcome, FindingConfidence, Line, PipelineDocument, PipelineKind, PipelineMemory, PipelineRef, PipelineRefusal, PipelineResolution, PipelineRun, PipelineStewardship, ResolutionOutcome, RulingAuthority, Short, Slug, pipeline_key, validate_pipeline_write_envelope, }; @@ -626,7 +626,8 @@ fn check(docs: &Docs<'_>, staged: &Staged, mind: &Slug) -> Result<(), MindRefusa let replay = docs.in_image(K::Run, key) == Some(&staged.document); run_rule(docs, key, run, replay) } - D::FollowUp(_) | D::Instance(_) | D::Pulse(_) | D::Reply(_) | D::Thought(_) | D::Memory(_) | D::Research(_) | D::Idea(_) => Ok(()), + D::Memory(memory) => consolidation_rule(docs, key, memory), + D::FollowUp(_) | D::Instance(_) | D::Pulse(_) | D::Reply(_) | D::Thought(_) | D::Research(_) | D::Idea(_) => Ok(()), } } @@ -648,6 +649,26 @@ fn run_rule(docs: &Docs<'_>, run_key: &str, run: &PipelineRun, replay: bool) -> Ok(()) } +/// A consolidation is a pair: each memory it names is Superseded by it, in +/// this batch. `consolidates` is a citation, never standing; the resolution +/// is what takes the consumed memory out of force, so a consolidation whose +/// resolution is missing would leave both standing. The other direction is +/// `resolution_rule`'s: a memory is Superseded only by memories that name it. +fn consolidation_rule(docs: &Docs<'_>, key: &str, memory: &PipelineMemory) -> Result<(), MindRefusal> { + for consumed in &memory.consolidates { + let paired = docs.batch.iter().any(|staged| { + matches!(&staged.document, PipelineDocument::Resolution(resolution) + if resolution.subject == *consumed + && matches!(&resolution.outcome, ResolutionOutcome::Superseded { by } + if by.iter().any(|supersessor| supersessor.kind == PipelineKind::Memory && supersessor.id.0 == key))) + }); + if !paired { + return Err(MindRefusal::UnpairedConsolidation { memory: key.into(), consumed: consumed.id.0.clone() }); + } + } + Ok(()) +} + /// Revision 1 stands alone; revision N needs a resolution in the batch that /// supersedes revision N-1 by this document. The predecessor's key is the /// leaf's, from the same document one revision back. @@ -827,6 +848,13 @@ fn resolution_rule(docs: &Docs<'_>, resolution: &PipelineResolution) -> Result<( { return Err(incompatible()); } + // A memory leaves force by consolidation only: the memory that + // supersedes it must name it (`consolidation_rule` is the other half). + if subject_kind == K::Memory + && !matches!(docs.find(K::Memory, &supersessor.id.0), Some(D::Memory(next)) if next.consolidates.contains(&resolution.subject)) + { + return Err(MindRefusal::UnpairedConsolidation { memory: supersessor.id.0.clone(), consumed: subject_id.into() }); + } } } ResolutionOutcome::Answered { by } => { @@ -2598,10 +2626,10 @@ mod tests { ResolutionOutcome::Deferred { to: follow() }, ResolutionOutcome::Recorded { reason: "done".into() }, withdrawn(), - superseded(&[r(K::Memory, &memory_key("m2"))]), ] { committed(admit(&mut open(), vec![resolution(subject(), outcome)])); } + committed(admit(&mut open(), consolidation("m3", &["m1"]))); let refused = [ (ResolutionOutcome::Deferred { to: r(K::CutSpec, &id("cut_spec", "cut-1.r1")) }, "Deferred"), (ResolutionOutcome::Fixed { commit: sha(), by: None }, "Fixed"), @@ -2633,7 +2661,62 @@ mod tests { refusal(admit(&mut mind, vec![consolidating(r(K::Memory, &elsewhere))])), MindRefusal::MissingReference { kind: K::Memory, id: elsewhere } ); - committed(admit(&mut mind, vec![consolidating(r(K::Memory, &memory_key("m1")))])); + let mut paired = vec![consolidating(r(K::Memory, &memory_key("m1")))]; + paired.push(resolution(r(K::Memory, &memory_key("m1")), superseded(&[r(K::Memory, &memory_key("m2"))]))); + committed(admit(&mut mind, paired)); + } + + /// A consolidation is a pair: the memory that names a consumed memory and the Superseded + /// resolution of that memory by it come in one batch, and neither half stands alone. + #[test] + fn a_consolidation_is_paired_both_ways() { + let key = |label: &str| memory_key(label); + let m = |label: &str| r(K::Memory, &key(label)); + let open = || { + let mut mind = with_specs(); + committed(admit(&mut mind, vec![memory("m1", MemorySort::Idea, "One.", &[]), memory("m2", MemorySort::Idea, "Two.", &[])])); + mind + }; + let unpaired = |consumed: &str| MindRefusal::UnpairedConsolidation { memory: key("m3"), consumed: key(consumed) }; + // The consolidating memory without the resolution of what it consumes. + let mut lone = consolidation("m3", &["m1"]); + lone.pop(); + assert_eq!(refusal(admit(&mut open(), lone)), unpaired("m1")); + // One of two consumed memories left standing. + let mut half = consolidation("m3", &["m1", "m2"]); + half.pop(); + assert_eq!(refusal(admit(&mut open(), half)), unpaired("m2")); + // A resolution naming a consolidation that does not name the memory back. + let D::Memory(mut stranger) = memory("m3", MemorySort::Idea, "Not a consolidation.", &[]) else { unreachable!() }; + stranger.consolidates = vec![]; + let refused = refusal(admit(&mut open(), vec![D::Memory(stranger), resolution(m("m1"), superseded(&[m("m3")]))])); + assert_eq!(refused, MindRefusal::UnpairedConsolidation { memory: key("m3"), consumed: key("m1") }); + // A resolution by a consolidation of other memories is no pairing for this one. + let mut other = consolidation("m3", &["m2"]); + other.push(resolution(m("m1"), superseded(&[m("m3")]))); + assert_eq!(refusal(admit(&mut open(), other)), unpaired("m1")); + // The pair commits: the consumed memories out of force, the consolidation in. + let mut mind = open(); + committed(admit(&mut mind, consolidation("m3", &["m1", "m2"]))); + // A consumed memory already resolved cannot be consumed again. + let mut again = consolidation("m4", &["m1"]); + again.pop(); + again.push(resolution_n(m("m1"), 2, superseded(&[m("m4")]))); + assert_eq!(refusal(admit(&mut mind, again)), MindRefusal::AlreadyResolved { subject: key("m1") }); + } + + /// A Sleep run and a Persona run are one slot of hers; a Self run is the other. + #[test] + fn a_sleep_run_and_a_persona_run_share_one_slot() { + let live = |label: &str| MindRefusal::AlreadyLive { run: crate::refusal::RunId::held(&format!("{INSTANCE}:run:{label}")) }; + let mut mind = with_specs(); + committed(admit(&mut mind, vec![mind_run("p", RunTurn::PersonaTurn, &[])])); + assert_eq!(refusal(admit(&mut mind, vec![mind_run("s", RunTurn::Sleep, &[])])), live("p"), "a night while she is awake"); + committed(admit(&mut mind, vec![mind_run("self", RunTurn::SelfRun, &[])])); + committed(admit(&mut mind, vec![recorded("p")])); + committed(admit(&mut mind, vec![mind_run("s", RunTurn::Sleep, &[])])); + assert_eq!(refusal(admit(&mut mind, vec![mind_run("p2", RunTurn::PersonaTurn, &[])])), live("s"), "a session while she sleeps"); + assert_eq!(refusal(admit(&mut mind, vec![mind_run("s2", RunTurn::Sleep, &[])])), live("s"), "one night at a time"); } fn research_key(pass: &str, label: &str) -> String { diff --git a/crates/huginn-mind/src/docs.rs b/crates/huginn-mind/src/docs.rs index 3af2bf7..eab5479 100644 --- a/crates/huginn-mind/src/docs.rs +++ b/crates/huginn-mind/src/docs.rs @@ -255,8 +255,9 @@ impl<'i> Docs<'i> { } /// The in-force run of hers, other than `run_key`, that holds the slot of - /// the turn `run` is of: ruling one-live-self-run's one live run per - /// instance and turn, asked by a run's opening and by the withdrawal that + /// the turn `run` is of (a Persona session and a Sleep run share the + /// Persona slot): ruling one-live-self-run's one live run per + /// instance and slot, asked by a run's opening and by the withdrawal that /// would put a closed run back in force. An operator's run neither holds /// the slot nor is refused for it. pub(crate) fn live_holder(&self, run_key: &str, run: &PipelineRun) -> Option<&str> { @@ -267,7 +268,7 @@ impl<'i> Docs<'i> { PipelineDocument::Run(other) if other_key != run_key && other.instance == run.instance - && other.turn == run.turn + && other.turn.slot() == run.turn.slot() && other.operated_by == RunOperator::Mind && self.in_force(PipelineKind::Run, other_key) => { diff --git a/crates/huginn-mind/src/fixtures.rs b/crates/huginn-mind/src/fixtures.rs index 6ce7b07..f80064c 100644 --- a/crates/huginn-mind/src/fixtures.rs +++ b/crates/huginn-mind/src/fixtures.rs @@ -397,6 +397,17 @@ pub(crate) fn memory(label: &str, sort: MemorySort, text: &str, about: &[&str]) }) } +/// A consolidation as it is admitted: the memory `label` consolidating the memories `consumed`, and a +/// Superseded resolution of each by it. The pair is one batch. +pub(crate) fn consolidation(label: &str, consumed: &[&str]) -> Vec { + let key = |label: &str| PipelineRef { kind: PipelineKind::Memory, id: Short(format!("{INSTANCE}:memory:{label}")) }; + let PipelineDocument::Memory(mut memory) = memory(label, MemorySort::Idea, "Consolidated.", &[]) else { unreachable!() }; + memory.consolidates = consumed.iter().map(|consumed| key(consumed)).collect(); + std::iter::once(PipelineDocument::Memory(memory)) + .chain(consumed.iter().map(|consumed| resolution(key(consumed), superseded(&[key(label)])))) + .collect() +} + /// One research note of pass `pass`: a claim standing on one observed source, serving nothing. pub(crate) fn research(pass: &str, label: &str, claim: &str) -> PipelineDocument { PipelineDocument::Research(eureka_pipeline::PipelineResearch { diff --git a/crates/huginn-mind/src/query.rs b/crates/huginn-mind/src/query.rs index b20629a..a3a43b0 100644 --- a/crates/huginn-mind/src/query.rs +++ b/crates/huginn-mind/src/query.rs @@ -1795,9 +1795,7 @@ mod tests { committed(admit(&mut mind, vec![crate::fixtures::thought("t1", "A drift.", &[r(K::Finding, &f1), r(K::Question, &q1)])])); let (older, newer) = (format!("{INSTANCE}:memory:m1"), format!("{INSTANCE}:memory:m2")); committed(admit(&mut mind, vec![memory("m1", MemorySort::Idea, "one", &[])])); - let D::Memory(mut consolidating) = memory("m2", MemorySort::Idea, "two", &[]) else { unreachable!() }; - consolidating.consolidates = vec![r(K::Memory, &older)]; - committed(admit(&mut mind, vec![D::Memory(consolidating)])); + committed(admit(&mut mind, crate::fixtures::consolidation("m2", &["m1"]))); let (note, way) = (id("research", "p.a"), id("idea", "p.i")); let D::Research(mut note_document) = research("p", "a", "A fact.") else { unreachable!() }; note_document.serves = vec![r(K::Question, &q1)]; @@ -1816,6 +1814,7 @@ mod tests { id("resolution", &format!("finding.{}.n1", f2.rsplit(':').next().unwrap())), ); let spec_closed = id("resolution", "cut_spec.cut-1.r1.n1"); + let older_closed = format!("{INSTANCE}:resolution:memory.m1.n1"); let spec_r2 = id("cut_spec", "cut-1.r2"); let r1 = id("ruling", "R1"); let expected: Vec<(CitationRole, &str, &str)> = vec![ @@ -1835,7 +1834,9 @@ mod tests { (CitationRole::Subject, &f1_closed, &f1), (CitationRole::Subject, &f2_closed, &f2), (CitationRole::Subject, &spec_closed, &spec), + (CitationRole::Subject, &older_closed, &older), (CitationRole::SupersededBy, &spec_closed, &spec_r2), + (CitationRole::SupersededBy, &older_closed, &newer), (CitationRole::ResolvedBy, &n1, &r1), (CitationRole::ResolvedBy, &f1_closed, &report), (CitationRole::DeferredTo, &f2_closed, &fu), diff --git a/crates/huginn-mind/src/refusal.rs b/crates/huginn-mind/src/refusal.rs index 150f652..da447df 100644 --- a/crates/huginn-mind/src/refusal.rs +++ b/crates/huginn-mind/src/refusal.rs @@ -36,6 +36,8 @@ pub enum MindRefusal { CitesResolvedDocument { kind: PipelineKind, id: String }, EmptySupersession, UnknownSupersessor { id: String }, + /// A consolidation and the supersession of the memory it consumes come together: `memory` consolidates `consumed`, or is named as superseding it, without the other half in the batch. + UnpairedConsolidation { memory: String, consumed: String }, RevisionWithoutSupersession { kind: PipelineKind, revision: u32 }, DuplicateLabel { field: String, label: String }, InvalidOptions { question: String }, diff --git a/crates/huginn-mind/src/rows.rs b/crates/huginn-mind/src/rows.rs index 941780e..6daec9c 100644 --- a/crates/huginn-mind/src/rows.rs +++ b/crates/huginn-mind/src/rows.rs @@ -66,7 +66,7 @@ pub(crate) enum Alias { const FACULTIES: [&str; 7] = ["SelfFaculty", "Imagination", "Hands", "Soul", "Life", "Eyes", "Operator"]; const OUTCOMES: [&str; 6] = ["Superseded", "Answered", "Fixed", "Deferred", "Recorded", "Withdrawn"]; const BOOLEANS: [&str; 2] = ["true", "false"]; -const MEMORY_ORIGINS: [&str; 4] = ["Room", "Direct", "Wake", "SelfRun"]; +const MEMORY_ORIGINS: [&str; 5] = ["Room", "Direct", "Wake", "SelfRun", "Sleep"]; /// The one value a presence alias answers with; a row without it answers nothing. const PRESENT: [&str; 1] = ["true"]; @@ -288,6 +288,7 @@ impl SelectionRow { MemoryOrigin::Direct { .. } => "Direct", MemoryOrigin::Wake => "Wake", MemoryOrigin::SelfRun { .. } => "SelfRun", + MemoryOrigin::Sleep { .. } => "Sleep", }.to_string()]); } D::Research(research) => put(Alias::Pass, vec![research.pass.0.clone()]), diff --git a/crates/mind-body/src/launch.rs b/crates/mind-body/src/launch.rs index 688d232..b954134 100644 --- a/crates/mind-body/src/launch.rs +++ b/crates/mind-body/src/launch.rs @@ -25,7 +25,7 @@ use chrono::{DateTime, Duration, Utc}; use cultnet_rs::Selection; use eureka_pipeline::{ Date, Label, PipelineDocument, PipelineKind, PipelineQuestion, PipelineRef, PipelineResolution, PipelineRun, QuestionOption, ResolutionOutcome, - RunOperator, RunTurn, Slug, pipeline_key, + RunOperator, RunSlot, RunTurn, Slug, pipeline_key, }; use huginn_mind::{ MindRefusal, PipelineAdmissionOutcome, PipelineDocumentSummary, PipelineFacts, @@ -115,12 +115,12 @@ impl Default for SystemdLauncher { } } -/// The unit template of one turn, named only through `unit()`, which start, +/// The unit template of one slot, named only through `unit()`, which start, /// alive and stop all call. fn template(turn: RunTurn) -> &'static str { - match turn { - RunTurn::PersonaTurn => "mind-persona", - RunTurn::SelfRun => "mind-self", + match turn.slot() { + RunSlot::Persona => "mind-persona", + RunSlot::SelfRun => "mind-self", } } @@ -340,7 +340,7 @@ pub(crate) fn holders(mind: &dyn MindPort, instance: &Slug, turn: RunTurn) -> Re let runs = headers(mind, &in_force(any_of(of_kind(PipelineKind::Run), "root", &[&instance.0])))?; Ok(runs .into_iter() - .filter(|run| matches!(run.facts, PipelineFacts::Run { turn: held, operated_by: RunOperator::Mind, .. } if held == turn)) + .filter(|run| matches!(run.facts, PipelineFacts::Run { turn: held, operated_by: RunOperator::Mind, .. } if held.slot() == turn.slot())) .collect()) } @@ -384,7 +384,7 @@ pub fn open_and_launch(ports: &Ports, request: LaunchRequest) -> Result Vec let origin: &[eureka_pipeline::Short] = match &memory.origin { MemoryOrigin::Room { heard_from, .. } => heard_from, MemoryOrigin::Direct { with, .. } => with, - MemoryOrigin::Wake | MemoryOrigin::SelfRun { .. } => &[], + MemoryOrigin::Wake | MemoryOrigin::SelfRun { .. } | MemoryOrigin::Sleep { .. } => &[], }; memory.about.iter().chain(origin).map(|id| id.0.clone()).collect() } @@ -206,6 +206,7 @@ pub(crate) fn origin_phrase(origin: &MemoryOrigin, names: &BTreeMap<&str, &str>) MemoryOrigin::Direct { with, .. } => format!("privately, with {}", who(with.iter().map(|id| id.0.as_str()), names)), MemoryOrigin::Wake => "on a wake".to_string(), MemoryOrigin::SelfRun { run } => format!("in your Self run {}", run.0), + MemoryOrigin::Sleep { run } => format!("in your sleep {}", run.0), } } diff --git a/crates/mind-body/src/testkit.rs b/crates/mind-body/src/testkit.rs index cc76fea..33e9aa9 100644 --- a/crates/mind-body/src/testkit.rs +++ b/crates/mind-body/src/testkit.rs @@ -55,12 +55,17 @@ pub(crate) struct Recording { pub(crate) fail: Cell, /// One named instance whose unit never starts. pub(crate) fail_for: RefCell>, - running: RefCell>, + running: RefCell>, } impl Recording { pub(crate) fn kill(&self, turn: RunTurn) { - self.running.borrow_mut().retain(|unit| *unit != (Slug(INSTANCE.into()), turn)); + self.running.borrow_mut().retain(|unit| *unit != (Slug(INSTANCE.into()), turn.slot())); + } + + /// `kill` for a named instance. + pub(crate) fn kill_as(&self, instance: &Slug, turn: RunTurn) { + self.running.borrow_mut().retain(|unit| *unit != (instance.clone(), turn.slot())); } pub(crate) fn revive(&self, turn: RunTurn) { @@ -69,7 +74,7 @@ impl Recording { /// `revive` for a named instance. pub(crate) fn revive_as(&self, instance: &Slug, turn: RunTurn) { - self.running.borrow_mut().push((instance.clone(), turn)); + self.running.borrow_mut().push((instance.clone(), turn.slot())); } } @@ -79,12 +84,12 @@ impl Launcher for Recording { return Err(anyhow!("refused")); } self.started.borrow_mut().push((instance.clone(), turn)); - self.running.borrow_mut().push((instance.clone(), turn)); + self.running.borrow_mut().push((instance.clone(), turn.slot())); Ok(()) } fn alive(&self, instance: &Slug, turn: RunTurn) -> bool { - self.running.borrow().contains(&(instance.clone(), turn)) + self.running.borrow().contains(&(instance.clone(), turn.slot())) } } diff --git a/schemas/cultnet/epiphany.pipeline.follow_up.v2.schema.json b/schemas/cultnet/epiphany.pipeline.follow_up.v2.schema.json index 4fde353..d956521 100644 --- a/schemas/cultnet/epiphany.pipeline.follow_up.v2.schema.json +++ b/schemas/cultnet/epiphany.pipeline.follow_up.v2.schema.json @@ -131,6 +131,27 @@ "SelfRun" ], "type": "object" + }, + { + "additionalProperties": false, + "description": "Her Sleep run, by its label: a memory she formed in her night.", + "properties": { + "Sleep": { + "properties": { + "run": { + "$ref": "#/$defs/Short" + } + }, + "required": [ + "run" + ], + "type": "object" + } + }, + "required": [ + "Sleep" + ], + "type": "object" } ] }, diff --git a/schemas/cultnet/epiphany.pipeline.memory.v2.schema.json b/schemas/cultnet/epiphany.pipeline.memory.v2.schema.json index e21595e..a2b2f66 100644 --- a/schemas/cultnet/epiphany.pipeline.memory.v2.schema.json +++ b/schemas/cultnet/epiphany.pipeline.memory.v2.schema.json @@ -96,6 +96,27 @@ "SelfRun" ], "type": "object" + }, + { + "additionalProperties": false, + "description": "Her Sleep run, by its label: a memory she formed in her night.", + "properties": { + "Sleep": { + "properties": { + "run": { + "$ref": "#/$defs/Short" + } + }, + "required": [ + "run" + ], + "type": "object" + } + }, + "required": [ + "Sleep" + ], + "type": "object" } ] }, diff --git a/schemas/cultnet/epiphany.pipeline.run.v2.schema.json b/schemas/cultnet/epiphany.pipeline.run.v2.schema.json index 36d9de8..70c79e9 100644 --- a/schemas/cultnet/epiphany.pipeline.run.v2.schema.json +++ b/schemas/cultnet/epiphany.pipeline.run.v2.schema.json @@ -61,7 +61,8 @@ "RunTurn": { "enum": [ "PersonaTurn", - "SelfRun" + "SelfRun", + "Sleep" ], "type": "string" }, diff --git a/schemas/cultnet/huginn.mind_request.v1.schema.json b/schemas/cultnet/huginn.mind_request.v1.schema.json index a36dc46..df6bb55 100644 --- a/schemas/cultnet/huginn.mind_request.v1.schema.json +++ b/schemas/cultnet/huginn.mind_request.v1.schema.json @@ -663,6 +663,27 @@ "required": [ "SelfRun" ] + }, + { + "description": "Her Sleep run, by its label: a memory she formed in her night.", + "type": "object", + "properties": { + "Sleep": { + "type": "object", + "properties": { + "run": { + "$ref": "#/$defs/Short" + } + }, + "required": [ + "run" + ] + } + }, + "additionalProperties": false, + "required": [ + "Sleep" + ] } ] }, @@ -2611,7 +2632,8 @@ "type": "string", "enum": [ "PersonaTurn", - "SelfRun" + "SelfRun", + "Sleep" ] }, "SemanticQuery": { diff --git a/schemas/cultnet/huginn.mind_response.v1.schema.json b/schemas/cultnet/huginn.mind_response.v1.schema.json index f53e1b2..93c58c7 100644 --- a/schemas/cultnet/huginn.mind_response.v1.schema.json +++ b/schemas/cultnet/huginn.mind_response.v1.schema.json @@ -791,6 +791,27 @@ "required": [ "SelfRun" ] + }, + { + "description": "Her Sleep run, by its label: a memory she formed in her night.", + "type": "object", + "properties": { + "Sleep": { + "type": "object", + "properties": { + "run": { + "$ref": "#/$defs/Short" + } + }, + "required": [ + "run" + ] + } + }, + "additionalProperties": false, + "required": [ + "Sleep" + ] } ] }, @@ -1184,6 +1205,31 @@ "UnknownSupersessor" ] }, + { + "description": "A consolidation and the supersession of the memory it consumes come together: `memory` consolidates `consumed`, or is named as superseding it, without the other half in the batch.", + "type": "object", + "properties": { + "UnpairedConsolidation": { + "type": "object", + "properties": { + "consumed": { + "type": "string" + }, + "memory": { + "type": "string" + } + }, + "required": [ + "memory", + "consumed" + ] + } + }, + "additionalProperties": false, + "required": [ + "UnpairedConsolidation" + ] + }, { "type": "object", "properties": { @@ -5335,7 +5381,8 @@ "type": "string", "enum": [ "PersonaTurn", - "SelfRun" + "SelfRun", + "Sleep" ] }, "Sha": {