memory: origin replaces learned_in and direct (eureka-pipeline MemoryOrigin, huginn-mind memory_origin alias and facts)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-31 17:46:20 +01:00
commit a746374708
4 changed files with 80 additions and 28 deletions

View file

@ -615,6 +615,11 @@ fn memory_is_well_formed(memory: &PipelineMemory, at: &str) -> Result<(), Pipeli
if memory.text.0.trim().is_empty() {
return Err(format_error(&format!("{at}.text"), "an empty memory"));
}
if let MemoryOrigin::Direct { with, .. } = &memory.origin {
if with.is_empty() {
return Err(format_error(&format!("{at}.origin.with"), "a direct memory that names no one she heard it from"));
}
}
if memory.salience_permille > SALIENCE_MAX_PERMILLE {
return Err(format_error(&format!("{at}.salience_permille"), &memory.salience_permille.to_string()));
}
@ -778,15 +783,14 @@ value_types! {
/// One thing a Mind keeps for herself, in her own mind: what she was asked,
/// decided, committed to, learned or did, an idea, or the rapport between
/// her and another Mind or person. `text` is her own words. `about` names
/// the Matrix users it concerns (Minds included), `learned_in` where she
/// learned it (a room alias, a DM's room id, or `self run <label>`),
/// `direct` whether it came in a direct message, and `sources` the event
/// ids, commits or documents it rests on; the organ fills the last three,
/// the Matrix users it concerns (Minds included) and nothing about where it
/// came from; `origin` is where and from whom she learned it, and `sources`
/// the event ids, commits or documents it rests on; the organ fills both,
/// not the model. `salience_permille` ranks it and decides nothing.
/// `consolidates` names the memories a consolidating memory replaces.
/// Instance-rooted: the key is `<instance>:memory:<label>`.
pub struct PipelineMemory {
instance: Slug, label: Label, sort: MemorySort, text: Line, about: Vec<Short>[8], learned_in: Short, direct: bool,
instance: Slug, label: Label, sort: MemorySort, text: Line, about: Vec<Short>[8], origin: MemoryOrigin,
sources: Vec<Short>[8], salience_permille: u16, consolidates: Vec<PipelineRef>[16],
} => memory_is_well_formed
}
@ -827,6 +831,47 @@ impl PipelineRef {
}
}
/// Where a memory came from and whom she heard it from. The organ derives it
/// from the batch of the turn that wrote the memory; the model never writes it.
/// `about` names whom a memory concerns, this names the room and the voices.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum MemoryOrigin {
/// A turn in rooms: their aliases comma-joined, and the batch's other senders.
Room {
rooms: Short,
#[schemars(extend("maxItems" = 8))]
heard_from: Vec<Short>,
},
/// A direct message: the DM's room id, and the others who spoke in it (whom
/// she heard it from in confidence).
Direct {
room: Short,
#[schemars(extend("maxItems" = 8))]
with: Vec<Short>,
},
/// A wake: no one spoke.
Wake,
/// A Self run of hers, by its label.
SelfRun { run: Short },
}
impl Bounded for MemoryOrigin {
fn validate(&self, field: &str) -> Result<(), PipelineRefusal> {
match self {
Self::Room { rooms, heard_from } => {
rooms.validate(&format!("{field}.rooms"))?;
list(&format!("{field}.heard_from"), heard_from, 8)
}
Self::Direct { room, with } => {
room.validate(&format!("{field}.room"))?;
list(&format!("{field}.with"), with, 8)
}
Self::Wake => Ok(()),
Self::SelfRun { run } => run.validate(&format!("{field}.run")),
}
}
}
/// How a subject was resolved. Every referent is a parsed `PipelineRef`, so a
/// resolution names its records by ids of the kinds they declare, validated
/// where every other referent is; a `Fixed` commit is a `Sha` whose referent
@ -1462,7 +1507,7 @@ mod tests {
(D::Memory(PipelineMemory {
instance: slug(INSTANCE), label: l("2026-10-31t041200-t0-m0"), sort: MemorySort::Committed,
text: "I take the background shaders; Libby holds the sounds.".into(), about: vec![s("@libby:chat")],
learned_in: s("swarm"), direct: false, sources: vec![s("$e1")], salience_permille: 700, consolidates: vec![],
origin: MemoryOrigin::Room { rooms: s("swarm"), heard_from: vec![s("@libby:chat")] }, sources: vec![s("$e1")], salience_permille: 700, consolidates: vec![],
}), format!("{INSTANCE}:memory:2026-10-31t041200-t0-m0")),
]
}

View file

@ -6,7 +6,7 @@ use chrono::{DateTime, TimeZone, Utc};
use cultcache_rs::CultCacheEnvelope;
use eureka_pipeline::{
AuthorityMap, ClaimOutcome, CodeLocation, CommitRange, CutVerification, Date, DocRef, Evidence, EvidenceKind,
FindingConfidence, FindingOrigin, FindingSeverity, Label, Line, MemorySort, MutationRecord, OrgRepo, PipelineCampaign,
FindingConfidence, FindingOrigin, FindingSeverity, Label, Line, MemoryOrigin, MemorySort, MutationRecord, OrgRepo, PipelineCampaign,
PipelineCutReport, PipelineCutSpec, PipelineDocument, PipelineFinding, PipelineFollowUp, PipelineHandOff,
PipelineInstance, PipelineKind, PipelineMemory, PipelineQuestion, PipelineRef, PipelineResolution, PipelineRuling,
PipelineRun, PipelineStewardship, PipelineTarget, PipelineVerdict, Promise, QuestionOption, ReportCommit,
@ -362,8 +362,7 @@ pub(crate) fn memory(label: &str, sort: MemorySort, text: &str, about: &[&str])
sort,
text: text.into(),
about: about.iter().map(|who| s(who)).collect(),
learned_in: s("swarm"),
direct: false,
origin: MemoryOrigin::Room { rooms: s("swarm"), heard_from: vec![] },
sources: vec![],
salience_permille: 500,
consolidates: vec![],

View file

@ -27,7 +27,7 @@ use cultnet_rs::{
Cursor, EdgeAnchor, EdgeMatch, Evaluation, LIMIT_MAX, LIMIT_MIN, PROJECTION_DOCUMENT, Row, Selection, select, validate,
};
use eureka_pipeline::{
ClaimOutcome, CommitRange, Date, FindingConfidence, FindingOrigin, FindingSeverity, Label, Line, MemorySort, OrgRepo,
ClaimOutcome, CommitRange, Date, FindingConfidence, FindingOrigin, FindingSeverity, Label, Line, MemoryOrigin, MemorySort, OrgRepo,
PipelineDocument, PipelineKind, PipelineRef, PipelineResolution, ResolutionOutcome, RulingAuthority, RunOperator, RunTurn, Sha,
Short, Slug, Title, pipeline_key,
};
@ -142,7 +142,7 @@ pub enum PipelineFacts {
HandOff { from_instance: Slug, to_instance: Slug, repo: OrgRepo, handed_on: Date },
Run { label: Label, operated_by: RunOperator, turn: RunTurn, started_on: Date, claims: Vec<PipelineRef> },
Pulse { label: Label, tempo_vote_beat_s: Option<u32>, #[serde(default)] tempo_vote_withdrawn: bool, urgency_permille: Option<u16> },
Memory { label: Label, sort: MemorySort, about: Vec<Short>, direct: bool, learned_in: Short, salience_permille: u16 },
Memory { label: Label, sort: MemorySort, about: Vec<Short>, origin: MemoryOrigin, salience_permille: u16 },
}
impl PipelineFacts {
@ -251,8 +251,7 @@ impl PipelineFacts {
label: value.label.clone(),
sort: value.sort,
about: value.about.clone(),
direct: value.direct,
learned_in: value.learned_in.clone(),
origin: value.origin.clone(),
salience_permille: value.salience_permille,
},
}
@ -758,7 +757,7 @@ mod tests {
use crate::store::test_stores::MemoryStore;
use chrono::{TimeZone, Utc};
use eureka_pipeline::{
ClaimOutcome, FindingConfidence, MemorySort, PipelineDocument as D, PipelineKind as K, PipelineRefusal, ResolutionOutcome,
ClaimOutcome, FindingConfidence, MemoryOrigin, MemorySort, PipelineDocument as D, PipelineKind as K, PipelineRefusal, ResolutionOutcome,
};
fn view(mind: &Mind<MemoryStore>, kind: K, key: &str) -> PipelineDocumentView {
@ -1496,13 +1495,13 @@ mod tests {
}
/// A memory is selected by its sort, by each user it is about (a memory about two users answers
/// for both) and by whether it was learned in a direct message; a value outside the domain is
/// for both) and by the kind of place it was learned in; a value outside the domain is
/// refused, and its header carries the triage facts but no text.
#[test]
fn memories_are_selected_by_sort_about_and_direct() {
fn memories_are_selected_by_sort_about_and_origin() {
let mut mind = seeded();
let D::Memory(mut direct) = memory("dm", MemorySort::Asked, "Prioritise the fork.", &["@op:chat"]) else { panic!() };
direct.direct = true;
direct.origin = MemoryOrigin::Direct { room: s("!dm:chat"), with: vec![s("@op:chat")] };
committed(admit(&mut mind, vec![
D::Memory(direct),
memory("plan", MemorySort::Committed, "I take the shaders.", &["@aqua:chat", "@libby:chat"]),
@ -1518,8 +1517,10 @@ mod tests {
assert_eq!(asking("sort", &["Committed", "Rapport"]), vec![key("bond"), key("plan")]);
assert_eq!(asking("about", &["@libby:chat"]), vec![key("bond"), key("plan")]);
assert_eq!(asking("about", &["@aqua:chat", "@op:chat"]), vec![key("dm"), key("plan")]);
assert_eq!(asking("direct", &["true"]), vec![key("dm")]);
assert_eq!(asking("direct", &["false"]), vec![key("bond"), key("plan")]);
assert_eq!(asking("memory_origin", &["Direct"]), vec![key("dm")]);
assert_eq!(asking("memory_origin", &["Wake", "SelfRun"]), Vec::<String>::new());
assert_eq!(asking("memory_origin", &["Room"]), vec![key("bond"), key("plan")]);
assert!(invalid("fields[0].values", "true")(&refused(&mind, &with(of_kinds(&[K::Memory]), any_of("memory_origin", &["true"])))));
for sort in MemorySort::NAMES {
assert!(mind.query(&with(of_kinds(&[K::Memory]), any_of("sort", &[sort]))).is_ok(), "{sort}");
}
@ -1527,7 +1528,7 @@ mod tests {
assert!(invalid("fields[0].values", "")(&refused(&mind, &with(of_kinds(&[K::Memory]), any_of("about", &[""])))));
let page = mind.query(&with(of_kinds(&[K::Memory]), any_of("sort", &["Rapport"]))).unwrap();
let PipelinePageItems::Headers(found) = page.items else { panic!("headers") };
assert!(matches!(&found[0].facts, PipelineFacts::Memory { sort: MemorySort::Rapport, direct: false, salience_permille: 500, .. }), "{:?}", found[0].facts);
assert!(matches!(&found[0].facts, PipelineFacts::Memory { sort: MemorySort::Rapport, origin: MemoryOrigin::Room { .. }, salience_permille: 500, .. }), "{:?}", found[0].facts);
}
/// A withdrawal pulse admitted into the mind is read back as a withdrawal: its

View file

@ -13,7 +13,7 @@ use std::collections::BTreeMap;
use cultnet_rs::{Row, RowSet, Selection};
use eureka_pipeline::{
FindingConfidence, FindingOrigin, FindingSeverity, Label, MemorySort, OrgRepo, PipelineDocument, PipelineKind, PipelineRef, ResolutionOutcome,
FindingConfidence, FindingOrigin, FindingSeverity, Label, MemoryOrigin, MemorySort, OrgRepo, PipelineDocument, PipelineKind, PipelineRef, ResolutionOutcome,
RulingAuthority, RunOperator, RunTurn, Slug,
};
@ -56,13 +56,15 @@ pub(crate) enum Alias {
Sort,
/// The Matrix users a memory is about, one value each.
About,
/// `true` or `false` on a memory: whether it was learned in a direct message.
Direct,
/// A memory's origin kind: `Room`, `Direct`, `Wake` or `SelfRun`. Named `memory_origin` because
/// `origin` already selects a finding's origin.
MemoryOrigin,
}
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"];
/// The one value a presence alias answers with; a row without it answers nothing.
const PRESENT: [&str; 1] = ["true"];
@ -86,7 +88,7 @@ impl Alias {
Self::Urged,
Self::Sort,
Self::About,
Self::Direct,
Self::MemoryOrigin,
];
fn name(self) -> &'static str {
@ -109,7 +111,7 @@ impl Alias {
Self::Urged => "urged",
Self::Sort => "sort",
Self::About => "about",
Self::Direct => "direct",
Self::MemoryOrigin => "memory_origin",
}
}
@ -131,7 +133,7 @@ impl Alias {
Self::Outcome => kind == K::Resolution,
Self::Turn | Self::OperatedBy | Self::Ran => kind == K::Run,
Self::Voted | Self::Urged => kind == K::Pulse,
Self::Sort | Self::About | Self::Direct => kind == K::Memory,
Self::Sort | Self::About | Self::MemoryOrigin => kind == K::Memory,
}
}
@ -150,7 +152,7 @@ impl Alias {
Self::OperatedBy => Some(RunOperator::NAMES),
Self::Ran | Self::Voted | Self::Urged => Some(&PRESENT),
Self::Sort => Some(MemorySort::NAMES),
Self::Direct => Some(&BOOLEANS),
Self::MemoryOrigin => Some(&MEMORY_ORIGINS),
Self::Root | Self::Repo | Self::Cut | Self::About => None,
}
}
@ -266,7 +268,12 @@ impl SelectionRow {
D::Memory(memory) => {
put(Alias::Sort, vec![format!("{:?}", memory.sort)]);
put(Alias::About, memory.about.iter().map(|who| who.0.clone()).collect());
put(Alias::Direct, vec![memory.direct.to_string()]);
put(Alias::MemoryOrigin, vec![match memory.origin {
MemoryOrigin::Room { .. } => "Room",
MemoryOrigin::Direct { .. } => "Direct",
MemoryOrigin::Wake => "Wake",
MemoryOrigin::SelfRun { .. } => "SelfRun",
}.to_string()]);
}
_ => {}
}