From e692e2f5ebb0323924d9bab1a6e93b5be6b14f8d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 26 Aug 2026 16:16:57 +0200 Subject: [PATCH] Project Persona self-authored history --- crates/ghostlight-dungeon/src/persona.rs | 18 ++++++- crates/ghostlight-dungeon/src/turn.rs | 62 ++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/crates/ghostlight-dungeon/src/persona.rs b/crates/ghostlight-dungeon/src/persona.rs index a45757c..85e1a7f 100644 --- a/crates/ghostlight-dungeon/src/persona.rs +++ b/crates/ghostlight-dungeon/src/persona.rs @@ -40,6 +40,9 @@ pub struct PermittedActorSlice { /// from silently impersonating an already-durable person. pub reserved_public_identities: BTreeSet, pub memories: Vec, + /// Exact recent public responses already committed for this stable subject. + /// These are event-history witnesses, not private memory or new world fact. + pub recent_self_authored_turns: Vec, pub perceived_events: Vec, pub perceived_actors: std::collections::BTreeMap, pub relationships: Vec, @@ -588,6 +591,11 @@ fn ground_actor_lived_stream(slice: &PermittedActorSlice, projection: &str) -> S } else { slice.memories[recent_memory_start..].join("; ") }; + let prior_public_response = if slice.recent_self_authored_turns.is_empty() { + "no earlier public response by this exact subject is active in this moment".to_owned() + } else { + slice.recent_self_authored_turns.join("; ") + }; let interaction = match slice.interaction_role { ActorInteractionRole::DirectResponseExpected => { "You are an exact direct addressee of the current speech. A response is expected from you now. You retain the agency to answer, refuse, or remain silent, but refusal or silence must be made observable rather than disappearing from the scene." @@ -619,7 +627,7 @@ fn ground_actor_lived_stream(slice: &PermittedActorSlice, projection: &str) -> S } }; format!( - "{projection}\n\n{subject_grounding} Your active self-identity: {identity}. In your social world, {established_peer_identities}. {interaction} Your reliable footing in this moment is narrow. What you know as external fact: {reliable_knowledge}. What you remember experiencing or being told: {remembered_experience}. These are your attributed recollections, not omniscient proof. What is happening now: {visible_now}. People you can presently perceive: {people_now}. Everything else in your impressions is feeling, inference, uncertainty, or possibility—not a remembered or witnessed fact." + "{projection}\n\n{subject_grounding} Your active self-identity: {identity}. In your social world, {established_peer_identities}. {interaction} Your reliable footing in this moment is narrow. What you know as external fact: {reliable_knowledge}. What you remember experiencing or being told: {remembered_experience}. These are your attributed recollections, not omniscient proof. Your exact recent public response history: {prior_public_response}. This response history records what you previously made observable; it does not prove that the content of those statements was true. What is happening now: {visible_now}. People you can presently perceive: {people_now}. Everything else in your impressions is feeling, inference, uncertainty, or possibility—not a remembered or witnessed fact." ) } @@ -4681,6 +4689,9 @@ mod tests { identity_experience: vec!["A tired navigator".into()], reserved_public_identities: BTreeSet::new(), memories: vec!["Proposed the eastern trail evacuation at dusk.".into()], + recent_self_authored_turns: vec![ + "At world revision 3, your committed public response was exactly: Use the eastern trail.".into(), + ], perceived_events: vec![], perceived_actors: std::collections::BTreeMap::from([( "player".into(), @@ -4701,6 +4712,8 @@ mod tests { assert!(grounded.contains("Your active self-identity: A tired navigator")); assert!(grounded.contains("What you remember experiencing or being told")); assert!(grounded.contains("Proposed the eastern trail evacuation at dusk.")); + assert!(grounded.contains("Use the eastern trail.")); + assert!(grounded.contains("does not prove that the content")); assert!(grounded.contains("not omniscient proof")); let result = engine.execute(slice).await.unwrap(); assert_eq!(result.stage_receipts.len(), 3); @@ -4718,6 +4731,7 @@ mod tests { identity_experience: vec!["You are an unnamed patient.".into()], reserved_public_identities: BTreeSet::new(), memories: vec![], + recent_self_authored_turns: vec![], perceived_events: vec!["The player asks your name.".into()], perceived_actors: BTreeMap::from([("player".into(), "Player".into())]), relationships: vec![], @@ -4766,6 +4780,7 @@ mod tests { identity_experience: vec!["You are an unnamed patient.".into()], reserved_public_identities: BTreeSet::new(), memories: vec![], + recent_self_authored_turns: vec![], perceived_events: vec!["A regulator is inspected.".into()], perceived_actors: BTreeMap::from([("player".into(), "Player".into())]), relationships: vec![], @@ -4861,6 +4876,7 @@ mod tests { identity_experience: vec!["You are an unnamed patient.".into()], reserved_public_identities: BTreeSet::new(), memories: vec![], + recent_self_authored_turns: vec![], perceived_events: vec!["The player asks your name.".into()], perceived_actors: BTreeMap::from([("player".into(), "Player".into())]), relationships: vec![], diff --git a/crates/ghostlight-dungeon/src/turn.rs b/crates/ghostlight-dungeon/src/turn.rs index 6a4ebaa..9bc0cea 100644 --- a/crates/ghostlight-dungeon/src/turn.rs +++ b/crates/ghostlight-dungeon/src/turn.rs @@ -281,6 +281,7 @@ pub async fn appraise_present( let receipts = campaign.branch_origin.evidence_receipt_ids.clone(); let perceived_actors = perceived_actors.clone(); let reserved_public_identities = reserved_public_identities(campaign, &actor); + let recent_self_authored_turns = recent_self_authored_turns(campaign, &actor.id); let semaphore = semaphore.clone(); let interaction_role = if response_expected_actor_ids.contains(&actor.id) { ActorInteractionRole::DirectResponseExpected @@ -301,6 +302,7 @@ pub async fn appraise_present( identity_experience: vec![format!("You are {}.", actor.name)], reserved_public_identities, memories: actor.memories, + recent_self_authored_turns, perceived_events: vec![event], perceived_actors, relationships: actor @@ -329,6 +331,7 @@ pub async fn appraise_present( let event = event_summary.to_owned(); let receipts = campaign.branch_origin.evidence_receipt_ids.clone(); let perceived_actors = perceived_actors.clone(); + let recent_self_authored_turns = recent_self_authored_turns(campaign, &gestalt.id); let semaphore = semaphore.clone(); let interaction_role = if response_expected_actor_ids.contains(&gestalt.id) { ActorInteractionRole::DirectResponseExpected @@ -352,6 +355,7 @@ pub async fn appraise_present( )], reserved_public_identities: perceived_actors.values().cloned().collect(), memories: vec![], + recent_self_authored_turns, perceived_events: vec![event], perceived_actors, relationships: vec![], @@ -400,6 +404,26 @@ pub async fn appraise_present( }) } +fn recent_self_authored_turns(campaign: &Campaign, subject_id: &str) -> Vec { + const LIMIT: usize = 8; + let mut turns = campaign + .transcript + .iter() + .rev() + .filter(|turn| turn.speaker == subject_id) + .take(LIMIT) + .map(|turn| { + format!( + "At world revision {}, your committed public response was exactly: {}", + turn.revision, + turn.text.trim() + ) + }) + .collect::>(); + turns.reverse(); + turns +} + fn reserved_public_identities( campaign: &Campaign, actor: &crate::domain::ActorState, @@ -609,6 +633,44 @@ mod tests { campaign } + #[test] + fn stable_subject_history_survives_folding_and_remains_bounded() { + let mut campaign = addressed_campaign(); + for revision in 1..=10 { + campaign.transcript.push(NarrativeTurn { + revision, + at: Utc::now(), + speaker: "refugee-one".into(), + text: format!("response {revision}"), + persona_response_actor_ids: BTreeSet::new(), + }); + } + campaign.transcript.push(NarrativeTurn { + revision: 11, + at: Utc::now(), + speaker: "refugee-two".into(), + text: "another person's answer".into(), + persona_response_actor_ids: BTreeSet::new(), + }); + + let before_folding = recent_self_authored_turns(&campaign, "refugee-one"); + assert_eq!(before_folding.len(), 8); + assert!(before_folding[0].contains("world revision 3")); + assert!(before_folding[0].ends_with("response 3")); + assert!(before_folding[7].ends_with("response 10")); + assert!( + before_folding + .iter() + .all(|turn| !turn.contains("another person's answer")) + ); + + campaign.actors.remove("refugee-one"); + assert_eq!( + recent_self_authored_turns(&campaign, "refugee-one"), + before_folding + ); + } + #[test] fn materialized_actor_receives_dormant_peer_identities_as_social_context() { let mut campaign = crate::resolution::tests::campaign(0, 8);