From db50e16a8a1e7f9e3ac83b526650148d2c695069 Mon Sep 17 00:00:00 2001 From: Eureka Date: Tue, 10 Nov 2026 14:46:44 +0000 Subject: [PATCH 01/69] Persona: report the newest placed event per room as chat_read at the echo One gamecult.persona.chat_read.v1 [persona, room, event_id, at_unix_ms] per room per echoed batch, sent over the same RUDP endpoint as chat_ack, from PersonaInbox::echoed (the CLI has taken the line). Newest placed event only; no event ids on earlier lines. Display-only: a failed report is dropped. Co-Authored-By: Claude Sonnet 5.5 --- crates/mind-body/src/chat_client.rs | 19 +++++ crates/mind-body/src/chat_rudp.rs | 6 +- crates/mind-body/src/heartbeat/tests.rs | 4 + crates/mind-body/src/persona.rs | 33 +++++++- crates/mind-body/src/persona/tests.rs | 91 ++++++++++++++++++++++- crates/mind-body/tests/chat_rudp.rs | 16 +++- crates/mind-body/tests/public_surface.txt | 4 +- 7 files changed, 168 insertions(+), 5 deletions(-) diff --git a/crates/mind-body/src/chat_client.rs b/crates/mind-body/src/chat_client.rs index 662232c..d3df8fe 100644 --- a/crates/mind-body/src/chat_client.rs +++ b/crates/mind-body/src/chat_client.rs @@ -22,6 +22,8 @@ pub const PENDING: &str = "gamecult.persona.chat_pending.v2"; /// The pending answer of a mouth that has no image field yet: its deliveries decode with `image: None`. pub const PENDING_V1: &str = "gamecult.persona.chat_pending.v1"; pub const ACK: &str = "gamecult.persona.chat_ack.v1"; +/// Her read receipt: the newest event of a room that went into her model's input. +pub const READ: &str = "gamecult.persona.chat_read.v1"; pub const SEND_REQUEST: &str = "gamecult.persona.chat_send_request.v2"; pub const SEND_RECEIPT: &str = "gamecult.persona.chat_send_receipt.v2"; pub const REFUSAL: &str = "gamecult.persona.chat_refusal.v1"; @@ -84,6 +86,20 @@ pub struct ChatAck { pub through_seq: u64, } +/// One room's read report: `[persona, room, event_id, at_unix_ms]`, positional like `ChatAck`. +/// +/// `room` is the delivery's `room_id` (the Matrix room id), not its alias. The mouth posts `m.read` to the room it delivered +/// the event for and accepts either spelling, but only the id names one room with no lookup, and a direct room has no alias +/// (its alias is its id). `event_id` is the newest event of that room among those the model's input held, and `at_unix_ms` +/// that event's own instant. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ChatRead { + pub persona: String, + pub room: String, + pub event_id: String, + pub at_unix_ms: u64, +} + /// Why the mouth refused a request, as its typed names (Bifrost /// `tools/persona-chat-documents.mjs` `refusals`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -187,6 +203,9 @@ pub trait Chat { /// advances its sync position only now, so a start that dies first has its /// deliveries again. fn ack(&self, through_seq: u64) -> Result<()>; + /// The model's input now holds `read.event_id` and every earlier event of its room. A display-only receipt: it + /// acknowledges nothing and the mouth answers nothing. + fn read(&self, read: &ChatRead) -> Result<()>; } #[cfg(test)] diff --git a/crates/mind-body/src/chat_rudp.rs b/crates/mind-body/src/chat_rudp.rs index a15c09f..c54ffa4 100644 --- a/crates/mind-body/src/chat_rudp.rs +++ b/crates/mind-body/src/chat_rudp.rs @@ -29,7 +29,7 @@ use serde::de::DeserializeOwned; use sha2::{Digest, Sha256}; use crate::chat_client::{ - ACK, Chat, ChatAck, ChatDelivery, ChatPending, ChatPendingRequest, ChatRefusal, ChatSendReceipt, ChatSendRequest, PENDING, PENDING_REQUEST, PENDING_V1, REFUSAL, SEND_RECEIPT, + ACK, Chat, ChatAck, ChatDelivery, ChatPending, ChatPendingRequest, ChatRead, ChatRefusal, ChatSendReceipt, ChatSendRequest, PENDING, PENDING_REQUEST, PENDING_V1, READ, REFUSAL, SEND_RECEIPT, SEND_REQUEST, decode, encode, }; @@ -185,6 +185,10 @@ impl Chat for RudpChat { let ack = ChatAck { persona: self.persona.clone(), through_seq }; self.exchange(ACK, &format!("{}:{through_seq}", self.persona), &ack, None).map(|_| ()) } + + fn read(&self, read: &ChatRead) -> Result<()> { + self.exchange(READ, &format!("{}:{}", self.persona, read.event_id), read, None).map(|_| ()) + } } impl Drop for RudpChat { diff --git a/crates/mind-body/src/heartbeat/tests.rs b/crates/mind-body/src/heartbeat/tests.rs index 1eb5cd7..3762834 100644 --- a/crates/mind-body/src/heartbeat/tests.rs +++ b/crates/mind-body/src/heartbeat/tests.rs @@ -99,6 +99,10 @@ impl Chat for FakeChat { fn ack(&self, _: u64) -> Result<()> { panic!("the heartbeat never acks a delivery") } + + fn read(&self, _: &crate::chat_client::ChatRead) -> Result<()> { + panic!("the heartbeat reads nothing") + } } #[derive(Default)] diff --git a/crates/mind-body/src/persona.rs b/crates/mind-body/src/persona.rs index 3629186..0a1fc01 100644 --- a/crates/mind-body/src/persona.rs +++ b/crates/mind-body/src/persona.rs @@ -84,7 +84,7 @@ use uuid::Uuid; use crate::address::{Addressee, Name, addresses, localpart, project_names}; use crate::chat_image::{self, Image, MAX_PER_LINE}; -use crate::chat_client::{Chat, ChatDelivery, ChatSendRequest, ContextLine, Refusal, SendBasis}; +use crate::chat_client::{Chat, ChatDelivery, ChatRead, ChatSendRequest, ContextLine, Refusal, SendBasis}; use crate::connector::render_plan; use crate::control::{ControlSource, Effective, HeldReason, derive}; use crate::heartbeat::{HeartbeatView, PollTrace}; @@ -1228,6 +1228,9 @@ struct PersonaInbox<'a, C: ControlSource, S: ControlSource> { /// What each room's newest message was among the lines the CLI took in this process: the seq and the event. Filled by `echoed` only: /// a line written and not echoed she has not seen. held: BTreeMap, + /// The seq of the newest event reported read in each room (by room id), so a retold or revised batch never reports an event + /// at or behind one already reported. Advanced only by a report the mouth took. + read_through: BTreeMap, /// The reply a stale send stopped, until it is told back. revision: Option, /// A revision line, or a flush, was echoed since the last result: that result is her final answer, sent without a basis. @@ -1295,6 +1298,7 @@ impl<'a, C: ControlSource, S: ControlSource> PersonaInbox<'a, C, S> { seen: Shown::default(), retell: None, held: BTreeMap::new(), + read_through: BTreeMap::new(), revision: None, echoed_revision: false, echoed_flush: false, @@ -1482,6 +1486,32 @@ impl<'a, C: ControlSource, S: ControlSource> PersonaInbox<'a, C, S> { Polled { pending, other: false, other_first: false, yield_now: !pending && !own_untold && self.replies_elsewhere } } + /// Her read receipts for a batch the CLI just took in: one `chat_read` per room, naming only the newest event of that room in + /// the batch. Every delivery of an echoed batch is a `message` line of her input, and the `earlier` lines before it are part + /// of the input too, so the newest message is the newest event she saw; nothing queued and not yet told is in `batch`, so + /// nothing unread is ever reported. Event ids are deliberately NOT added to the `earlier` or older message lines: a receipt + /// needs only the latest event the model saw, and an id on every line would grow the prompt for no receipt benefit. + /// Display only: a refused or unreachable report is dropped, never ends the session, and the ack stays the delivery + /// guarantee. A room already reported at or past a delivery's seq (a retold batch) is not reported again. + fn report_read(&mut self, batch: &[ChatDelivery]) { + let mut newest: BTreeMap<&str, &ChatDelivery> = BTreeMap::new(); + for delivery in batch { + if newest.get(delivery.room_id.as_str()).is_none_or(|kept| kept.seq < delivery.seq) { + newest.insert(&delivery.room_id, delivery); + } + } + let persona = self.organs.mind.instance().0.clone(); + for (room, delivery) in newest { + if self.read_through.get(room).is_some_and(|through| *through >= delivery.seq) { + continue; + } + let read = ChatRead { persona: persona.clone(), room: room.to_owned(), event_id: delivery.event_id.clone(), at_unix_ms: delivery.at_unix_ms }; + if self.organs.chat.read(&read).is_ok() { + self.read_through.insert(room.to_owned(), delivery.seq); + } + } + } + fn held(&self) -> Option { match read(self.control, self.swarm, self.organs.mind.instance()) { Effective::Held { reason } => Some(reason), @@ -1599,6 +1629,7 @@ impl SessionInbox for PersonaInbox<'_, C, S> self.held.insert(delivery.room_alias.clone(), (delivery.seq, delivery.event_id.clone())); } } + self.report_read(&batch); self.echoed.extend(batch); self.seen.add(shown); self.echoed_revision |= revision; diff --git a/crates/mind-body/src/persona/tests.rs b/crates/mind-body/src/persona/tests.rs index 0be421d..d44a8af 100644 --- a/crates/mind-body/src/persona/tests.rs +++ b/crates/mind-body/src/persona/tests.rs @@ -20,7 +20,7 @@ use serde_json::json; use tempfile::TempDir; use super::*; -use crate::chat_client::{ChatImage, ChatSendReceipt, ContextLine, Refusal}; +use crate::chat_client::{ChatImage, ChatRead, ChatSendReceipt, ContextLine, Refusal}; use base64::Engine; use eureka_pipeline::OrgRepo; use crate::control::{ControlWriter, FileSource, SwarmWriter}; @@ -59,6 +59,8 @@ struct FakeChat { deliveries: Deliveries, sent: RefCell>, acked: RefCell>, + reads: RefCell>, + fail_read: Cell, pending_calls: Cell, fail_send_from: Cell>, /// 0 a delivered receipt, 1 a refused one, 2 one with neither an event nor a refusal. @@ -141,6 +143,14 @@ impl Chat for FakeChat { Ok(ChatSendReceipt { request_id: request.request_id.clone(), event_id, refusal, newer: Vec::new() }) } + fn read(&self, read: &ChatRead) -> Result<()> { + if self.fail_read.get() { + return Err(anyhow!("the mouth is down")); + } + self.reads.borrow_mut().push(read.clone()); + Ok(()) + } + fn ack(&self, through_seq: u64) -> Result<()> { self.log.borrow_mut().push("ack".into()); self.acked.borrow_mut().push(through_seq); @@ -414,6 +424,8 @@ impl Rig { deliveries, sent: RefCell::default(), acked: RefCell::default(), + reads: RefCell::default(), + fail_read: Cell::new(false), pending_calls: Cell::new(0), fail_send_from: Cell::new(None), receipt: Cell::new(0), @@ -7516,3 +7528,80 @@ fn the_dropped_note_names_six_campaigns_and_counts_the_rest_together() { assert_eq!(dropped_note(&counts(6)), "(6 more open follow-ups not shown: c0 1, c1 1, c2 1, c3 1, c4 1, c5 1; `huginn open ` lists them.)\n"); assert_eq!(dropped_note(&counts(7)), "(7 more open follow-ups not shown: c0 1, c1 1, c2 1, c3 1, c4 1, c5 1, 1 more campaigns 1; `huginn open ` lists them.)\n"); } + +fn read_of(rig: &Rig) -> Vec<(String, String)> { + rig.chat.reads.borrow().iter().map(|read| (read.room.clone(), read.event_id.clone())).collect() +} + +#[test] +fn a_read_is_sent_only_at_the_echo_one_per_room_for_the_newest_event_that_went_into_her_input() { + let rig = Rig::new(vec![], &[]); + rig.clock.step.set(0); + let (a1, a2, b1) = (delivery(1, "swarm", "@op:chat", "swarm-one"), delivery(2, "swarm", "@op:chat", "swarm-two"), delivery(3, "ops", "@op:chat", "ops-one")); + let unread = delivery(4, "swarm", "@op:chat", "swarm-queued-unread"); + for each in [&a1, &a2, &b1] { + arrives(&rig, each); + } + with_inbox(&rig, &config(), vec![a1.clone(), a2.clone(), b1.clone()], |inbox| { + let line = inbox.start_line().unwrap(); + assert!(rig.chat.reads.borrow().is_empty(), "a line written and not echoed is not read"); + arrives(&rig, &unread); // in the mouth's queue, never in her input + assert!(inbox.poll().unwrap().pending); + inbox.echoed(); + let reads = rig.chat.reads.borrow().clone(); + assert_eq!(reads.len(), 2, "one per room"); + for read in &reads { + assert_eq!(read.persona, "yggdrasil"); + // The report names an event whose message is in the model's input, and no queued-unread one. + let reported = [&a1, &a2, &b1, &unread].into_iter().find(|delivery| delivery.event_id == read.event_id).expect("a delivered event"); + assert!(line.contains(&reported.text), "the reported event {} is not in her input", read.event_id); + assert_ne!(read.event_id, unread.event_id); + assert_eq!(read.room, reported.room_id, "the room id of the delivery, not its alias"); + assert_eq!(read.at_unix_ms, reported.at_unix_ms); + } + assert_eq!(read_of(&rig), vec![("!ops:chat".to_string(), b1.event_id.clone()), ("!swarm:chat".to_string(), a2.event_id.clone())], "the newest of each room"); + // Each line of the prompt still carries no event id of its own. + assert!(!line.contains(&a1.event_id) && !line.contains(&a2.event_id)); + }); +} + +#[test] +fn a_line_told_and_not_echoed_reports_nothing_and_a_report_is_never_repeated_or_moved_back() { + let rig = Rig::new(vec![], &[]); + rig.clock.step.set(0); + let (first, second) = (delivery(1, "swarm", "@op:chat", "one"), delivery(3, "swarm", "@op:chat", "two")); + arrives(&rig, &first); + with_inbox(&rig, &config(), vec![first.clone()], |inbox| { + inbox.start_line().unwrap(); + inbox.echoed(); + arrives(&rig, &second); + assert!(inbox.poll().unwrap().pending); + inbox.tell(false).unwrap(); + assert_eq!(read_of(&rig).len(), 1, "written, never echoed: not read"); + inbox.report_read(&[first.clone()]); + assert_eq!(read_of(&rig).len(), 1, "the same event is not reported twice"); + inbox.echoed(); + assert_eq!(read_of(&rig), vec![("!swarm:chat".to_string(), first.event_id.clone()), ("!swarm:chat".to_string(), second.event_id.clone())]); + inbox.report_read(&[first.clone()]); + assert_eq!(read_of(&rig).len(), 2, "an older event after a newer one is not reported"); + }); +} + +#[test] +fn a_read_the_mouth_does_not_take_is_dropped_without_ending_the_session_and_is_tried_again_with_the_next_batch() { + let rig = Rig::new(vec![], &[]); + rig.clock.step.set(0); + rig.chat.fail_read.set(true); + let first = delivery(1, "swarm", "@op:chat", "one"); + arrives(&rig, &first); + with_inbox(&rig, &config(), vec![first.clone()], |inbox| { + inbox.start_line().unwrap(); + inbox.echoed(); + inbox.result(&reply_result(&says(&[("swarm", "hi")])), true, false).unwrap(); + assert!(inbox.progress.failure.is_none() && inbox.progress.answered == 1, "a failed receipt is cosmetic"); + assert_eq!(rig.chat.acked.borrow().as_slice(), &[1], "the ack is the delivery guarantee and still goes"); + rig.chat.fail_read.set(false); + inbox.report_read(&[first.clone()]); + assert_eq!(read_of(&rig), vec![("!swarm:chat".to_string(), first.event_id.clone())], "a report that failed did not count as reported"); + }); +} diff --git a/crates/mind-body/tests/chat_rudp.rs b/crates/mind-body/tests/chat_rudp.rs index 8825ea3..0765ab4 100644 --- a/crates/mind-body/tests/chat_rudp.rs +++ b/crates/mind-body/tests/chat_rudp.rs @@ -13,7 +13,7 @@ use std::thread::JoinHandle; use std::time::Duration; use cultnet_rs::{CultNetMessage, CultNetRawDocumentRecord, CultNetRawPayloadEncoding, CultNetRudpSocketTransportConnection, CultNetRudpSocketTransportOptions}; -use mind_body::chat_client::{self, ACK, Chat, ChatAck, ChatPending, ChatPendingRequest, ChatSendReceipt, ChatSendRequest, PENDING, PENDING_REQUEST, PENDING_V1, SEND_RECEIPT, SEND_REQUEST}; +use mind_body::chat_client::{self, ACK, Chat, ChatAck, ChatPending, ChatPendingRequest, ChatRead, ChatSendReceipt, ChatSendRequest, PENDING, PENDING_REQUEST, PENDING_V1, READ, SEND_RECEIPT, SEND_REQUEST}; use mind_body::chat_rudp::{RudpChat, connection_id}; const ACK_BYTES: &[u8] = include_bytes!("fixtures/persona-chat-v1/gamecult.persona.chat_ack.v1.msgpack"); @@ -145,6 +145,20 @@ fn pending_send_and_ack_cross_a_real_rudp_session_in_the_mouths_bytes() { ); } +#[test] +fn a_read_reaches_the_mouth_as_the_positional_tuple_persona_room_event_instant() { + let mouth = mouth(KEY); + let chat = client(&mouth, KEY, "eureka", Duration::from_secs(5)); + let read = ChatRead { persona: "eureka".into(), room: "!abc:gamecult.org".into(), event_id: "$e1".into(), at_unix_ms: 1_761_700_000_000 }; + chat.read(&read).unwrap(); + let seen = mouth.seen.lock().unwrap().clone(); + assert_eq!(seen.len(), 1); + assert_eq!(seen[0].0, READ); + assert_eq!(chat_client::decode::(&seen[0].1).unwrap(), read); + let tuple: serde_json::Value = rmp_serde::from_slice(&seen[0].1).unwrap(); + assert_eq!(tuple, serde_json::json!(["eureka", "!abc:gamecult.org", "$e1", 1_761_700_000_000u64]), "a positional tuple, not a map"); +} + #[test] fn a_peer_without_the_credential_cannot_open_the_session() { let mouth = mouth(KEY); diff --git a/crates/mind-body/tests/public_surface.txt b/crates/mind-body/tests/public_surface.txt index d45017c..be72323 100644 --- a/crates/mind-body/tests/public_surface.txt +++ b/crates/mind-body/tests/public_surface.txt @@ -19,6 +19,7 @@ mind_body::chat_client: const ACK : & str mind_body::chat_client: const PENDING : & str mind_body::chat_client: const PENDING_REQUEST : & str mind_body::chat_client: const PENDING_V1 : & str +mind_body::chat_client: const READ : & str mind_body::chat_client: const REFUSAL : & str mind_body::chat_client: const SEND_RECEIPT : & str mind_body::chat_client: const SEND_REQUEST : & str @@ -28,12 +29,13 @@ mind_body::chat_client: impl Refusal mind_body::chat_client: impl Refusal: fn name (self) -> & 'static str mind_body::chat_client: impl Serialize for Refusal mind_body::chat_client: impl< 'de > Deserialize < 'de > for Refusal -mind_body::chat_client: pub trait Chat { fn pending (& self , after_seq : u64) -> Result < Vec < ChatDelivery > > ; fn send (& self , request : & ChatSendRequest) -> Result < ChatSendReceipt > ; fn ack (& self , through_seq : u64) -> Result < () > ; } +mind_body::chat_client: pub trait Chat { fn pending (& self , after_seq : u64) -> Result < Vec < ChatDelivery > > ; fn send (& self , request : & ChatSendRequest) -> Result < ChatSendReceipt > ; fn ack (& self , through_seq : u64) -> Result < () > ; fn read (& self , read : & ChatRead) -> Result < () > ; } mind_body::chat_client: struct ChatAck # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[persona: String, through_seq: u64] mind_body::chat_client: struct ChatDelivery # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[seq: u64, room_alias: String, room_id: String, event_id: String, sender: String, sender_display: String, text: String, at_unix_ms: u64, context: Vec < ContextLine >, mentions: Vec < String >, is_direct: bool, image: Option < ChatImage >] mind_body::chat_client: struct ChatImage # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[path: String, mime: String, size: u64, sha256: String] mind_body::chat_client: struct ChatPending # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[persona: String, deliveries: Vec < ChatDelivery >] mind_body::chat_client: struct ChatPendingRequest # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[persona: String, after_seq: u64] +mind_body::chat_client: struct ChatRead # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[persona: String, room: String, event_id: String, at_unix_ms: u64] mind_body::chat_client: struct ChatRefusal # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[persona: String, refusal: Refusal] mind_body::chat_client: struct ChatSendReceipt # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[request_id: String, event_id: Option < String >, refusal: Option < Refusal >, newer: Vec < String >] mind_body::chat_client: struct ChatSendRequest # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[request_id: String, persona: String, room_alias: String, text: String, basis: Option < SendBasis >] From be35c72a168f3c39465c218025c0fb0afaf59aee Mon Sep 17 00:00:00 2001 From: Eureka Date: Tue, 10 Nov 2026 15:15:48 +0000 Subject: [PATCH 02/69] eureka-pipeline: the desk_answer kind, tagged DeskOutcome and its key One kind, key :desk_answer:.n, one tagged outcome (Counted, Revision, Retraction, Refused). Published schema, index row, golden line and the PipelineKind enum in sibling schemas regenerated. Co-Authored-By: Claude Sonnet 5.5 --- crates/eureka-pipeline/golden/envelopes.txt | 2 + crates/eureka-pipeline/src/lib.rs | 94 ++++++++- ...iphany.pipeline.desk_answer.v2.schema.json | 187 ++++++++++++++++++ .../epiphany.pipeline.finding.v2.schema.json | 3 +- ...epiphany.pipeline.follow_up.v2.schema.json | 3 +- .../epiphany.pipeline.idea.v2.schema.json | 3 +- .../epiphany.pipeline.memory.v2.schema.json | 3 +- .../epiphany.pipeline.question.v2.schema.json | 3 +- .../epiphany.pipeline.reply.v2.schema.json | 3 +- .../epiphany.pipeline.research.v2.schema.json | 3 +- ...piphany.pipeline.resolution.v2.schema.json | 3 +- .../epiphany.pipeline.ruling.v2.schema.json | 3 +- .../epiphany.pipeline.run.v2.schema.json | 3 +- .../epiphany.pipeline.thought.v2.schema.json | 3 +- schemas/cultnet/index.json | 11 ++ 15 files changed, 314 insertions(+), 13 deletions(-) create mode 100644 schemas/cultnet/epiphany.pipeline.desk_answer.v2.schema.json diff --git a/crates/eureka-pipeline/golden/envelopes.txt b/crates/eureka-pipeline/golden/envelopes.txt index 916532e..01bb38b 100644 --- a/crates/eureka-pipeline/golden/envelopes.txt +++ b/crates/eureka-pipeline/golden/envelopes.txt @@ -11,6 +11,7 @@ # a failing test pass: a diff here is a wire change and needs its own ruling. # cut_spec.reads-and-symbol and finding.location-symbol were appended after 44c010c added CodeLocation.symbol and PipelineCutSpec.reads (ruling context-pack-read-anchors); no earlier line changed. # target.repos was appended after 9298371 added PipelineTarget.repos (ruling campaign-repo-set-home); no earlier line changed. +# sample.desk_answer was inserted after the sample corpus when the desk_answer kind was added (ruling eureka-substrate:ruling:desk-answer-kind-tagged-outcome); no earlier line changed. sample.campaign eureka-state:campaign:self epiphany.pipeline.campaign.v2 EpiphanyPipelineCampaignDocument 9185a4736c7567ac657572656b612d7374617465a57469746c65b5457572656b6120706970656c696e65207374617465a57265706f7391b147616d6543756c742f4570697068616e79ae776f726b696e675f6272616e6368bb636f6465782f657572656b612d706970656c696e652d7374617465aa7461726765745f646f6384a470617468d9256e6f7465732f657572656b612d706970656c696e652d73746174652d7461726765742e6d64aa73746172745f6c696e6501a8656e645f6c696e6509a6636f6d6d6974a83566393832323864 sample.target eureka-state:target:r2 epiphany.pipeline.target.v2 EpiphanyPipelineTargetDocument 9186a863616d706169676eac657572656b612d7374617465a87265766973696f6e02aa696e76617269616e74739182a56c6162656cab6d696e642d61646d697473a973746174656d656e74b64f6e6c792061646d697373696f6e207772697465732eac6e6f745f696e5f73636f706591ac4576652062726f7773696e67b963616e6f6e6963616c5f696d706c656d656e746174696f6e7391a743756c744c6962a3646f6384a470617468d9256e6f7465732f657572656b612d706970656c696e652d73746174652d7461726765742e6d64aa73746172745f6c696e6501a8656e645f6c696e6509a6636f6d6d6974a83566393832323864 sample.question eureka-state:question:Q1 epiphany.pipeline.question.v2 EpiphanyPipelineQuestionDocument 9189a863616d706169676eac657572656b612d7374617465a56c6162656ca25131a57469746c65b357686f206f776e73207468652073746174653fa87175657374696f6eb357686f206f776e73207468652073746174653fa76f7074696f6e739282a56c6162656ca141a474657874ab616e20696e7374616e636582a56c6162656ca142a474657874a661207265706fab7265636f6d6d656e646564a141a7646570656e647391a6437574203361a97261697365645f696e82a46b696e64a743757453706563a26964bf657572656b612d73746174653a6375745f737065633a6375742d33612e7231a861736b65645f6f6eaa323032362d30392d3135 @@ -31,6 +32,7 @@ sample.research eureka-state:research:p1.scope epiphany.pipeline.research.v2 Epi sample.idea eureka-state:idea:p1.i1 epiphany.pipeline.idea.v2 EpiphanyPipelineIdeaDocument 9189a863616d706169676eac657572656b612d7374617465a470617373a27031a56c6162656ca26931a57469746c65b24f6e65206e6f74652070657220636c61696da469646561d9204b65657020612070617373206173206d616e7920736d616c6c206e6f7465732ea974726164656f666673b74d6f726520646f63756d656e747320746f206c6973742ea872657374735f6f6e9182a46b696e64a85265736561726368a26964be657572656b612d73746174653a72657365617263683a70312e73636f7065a6746f776172649182a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131ab70726f706f7365645f6f6eaa323032362d30392d3135 sample.reply yggdrasil:reply:mind-2026-10-09-1-r1 epiphany.pipeline.reply.v2 EpiphanyPipelineReplyDocument 9185a8696e7374616e6365a979676764726173696ca56c6162656cb46d696e642d323032362d31302d30392d312d7231a372756e82a46b696e64a352756ea26964bf79676764726173696c3a72756e3a6d696e642d323032362d31302d30392d31a7616e73776572739182a46b696e64a8466f6c6c6f775570a26964d923657572656b612d73746174653a666f6c6c6f775f75703a61736b2d7468652d73656c66a474657874d93054686520736861646572207061737320697320646f6e653b2074686520736f756e647320617265204c6962627927732e sample.thought yggdrasil:thought:t1793000000 epiphany.pipeline.thought.v2 EpiphanyPipelineThoughtDocument 91de0014a8696e7374616e6365a979676764726173696ca56c6162656cab7431373933303030303030a474657874d93d54686520736f756e647320617265204c6962627927733b2074686520736861646572732077657265206e6576657220746865206861726420706172742ea572656164739182a46b696e64a64d656d6f7279a26964d92879676764726173696c3a6d656d6f72793a323032362d31302d3331743034313230302d74302d6d30af706572736f6e615f72656365697074b06d696e642d636f6d6d69742d30613162a86964656e74697479be457572656b612c207368652f6865722c20706c61696e2d73706f6b656e2ea7636f6e74657874d92056616c7565733a20636f686572656e6365206f7665722076656c6f636974792ea87374696d756c7573bd4c696262792061736b65642061626f75742074686520736f756e64732eaf70726f6a6563746f725f6d6f64656cb070726f6a6563746f723a6c6174657374aa70726f6a656374696f6ed92949206b656570207475726e696e67206f7665722077686f20686f6c64732074686520736f756e64732eaa626173655f6d6f64656cab626173653a6c6174657374b474656d70657261747572655f7065726d696c6c65cd03e8ae6d696e5f705f7065726d696c6c6532aa6d61785f746f6b656e73cca0aa63616e6469646174657392d93d54686520736f756e647320617265204c6962627927733b2074686520736861646572732077657265206e6576657220746865206861726420706172742eaf41207365636f6e642064726966742eb06e6561726573745f7065726d696c6c6592cd0136cd0366a663686f73656e00ac70726f6a6563746f725f6d73cd1068ac636f6e74696e7565725f6d73cd238cae70726f6d70745f76657273696f6ebd67686f73746c696768743d633433656235343b20776f7264733d313830 +sample.desk_answer eureka-state:desk_answer:Q1.n1 epiphany.pipeline.desk_answer.v2 EpiphanyPipelineDeskAnswerDocument 918aa863616d706169676eac657572656b612d7374617465a4666f726bb8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501ac6f7074696f6e735f68617368d94063636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363a663686f696365a161a46e6f7465b853686172652069742c207769746820746865206d61726b2eab616e7377657265645f6174b4323032362d31312d31305431353a30303a30305aa8616e737765726572a6757365722d37a9617574686f72697479a744656369646572a76f7574636f6d6581a85265766973696f6e81a26f66be657572656b612d73746174653a6465736b5f616e737765723a51312e6e31 resolution.superseded eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resolution.v2 EpiphanyPipelineResolutionDocument 9185a77375626a65637482a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501a76f7574636f6d6581aa5375706572736564656481a262799282a46b696e64a652756c696e67a26964b6657572656b612d73746174653a72756c696e673a523882a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a9726174696f6e616c65a852756c656420412eab7265736f6c7665645f6f6eaa323032362d30392d3135 resolution.fixed-by-none eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resolution.v2 EpiphanyPipelineResolutionDocument 9185a77375626a65637482a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501a76f7574636f6d6581a5466978656482a6636f6d6d6974a83566393832323864a26279c0a9726174696f6e616c65a852756c656420412eab7265736f6c7665645f6f6eaa323032362d30392d3135 resolution.fixed-by-some eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resolution.v2 EpiphanyPipelineResolutionDocument 9185a77375626a65637482a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501a76f7574636f6d6581a5466978656482a6636f6d6d6974a83566393832323864a2627982a46b696e64a94375745265706f7274a26964d921657572656b612d73746174653a6375745f7265706f72743a6375742d33612e6831a9726174696f6e616c65a852756c656420412eab7265736f6c7665645f6f6eaa323032362d30392d3135 diff --git a/crates/eureka-pipeline/src/lib.rs b/crates/eureka-pipeline/src/lib.rs index 5b85395..cf4f155 100644 --- a/crates/eureka-pipeline/src/lib.rs +++ b/crates/eureka-pipeline/src/lib.rs @@ -738,6 +738,10 @@ unit_enums! { RunOperator { Operator, Mind } RunTurn { PersonaTurn, SelfRun, Sleep } MemorySort { Asked, Decided, Committed, Learned, Did, Idea, Rapport } + // What a desk answerer is, as the receiver stamped it (never a client's claim). + DeskAuthority { Operator, Decider } + // Why a desk answer was refused and stored: binding spec 1.3 steps 4-7 and R1. + DeskRefusal { StaleFork, NotADecider, ForkClosed, ChoiceInvalid, BadRetraction, AlreadyRetracted } } /// The slots of a Mind's runs: one live run of hers per instance and slot. A @@ -838,6 +842,15 @@ value_types! { structural_delta: StructuralDelta, landed_names: Vec[128], undone: Vec[32], promises: Vec[64], } + /// One answer the fork desk's receiver passed to the Mind, refused ones included. + /// `answerer` and `authority` are stamped by the Mind from the verified caller and + /// are absent from the request type; `answered_at` is the client's claim kept as + /// written, display only: order is `sequence`, the receiver's order. `sequence` is + /// per question and is the last part of the key (the answer id). + pub struct PipelineDeskAnswer { + campaign: Slug, fork: Short, sequence: u32, options_hash: Sha256Hex, choice: Option