From bcb03e46477014a568ebd87756b6355b8d3214c7 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 18 Aug 2026 00:02:45 +0200 Subject: [PATCH] Allow bounded colocated gestalt interaction --- crates/ghostlight-dungeon/src/kernel.rs | 93 ++++++++++++++++++- crates/ghostlight-dungeon/src/resolution.rs | 55 ++++++++++- crates/ghostlight-dungeon/src/scheduler.rs | 48 +++++++++- .../ghostlight-multiresolution-agency.md | 14 ++- notes/fresh-workspace-handoff.md | 9 ++ notes/ghostlight-current-system-map.md | 18 ++-- notes/ghostlight-dungeon.md | 14 +-- state/map.yaml | 2 +- 8 files changed, 228 insertions(+), 25 deletions(-) diff --git a/crates/ghostlight-dungeon/src/kernel.rs b/crates/ghostlight-dungeon/src/kernel.rs index dad3652..da2cf7d 100644 --- a/crates/ghostlight-dungeon/src/kernel.rs +++ b/crates/ghostlight-dungeon/src/kernel.rs @@ -1421,6 +1421,17 @@ fn apply_strategic_tick_plan( .filter(|target| campaign.institutions.contains_key(*target)) .cloned() .collect(); + let actor_ids = action + .target_subject_ids + .iter() + .filter(|target| { + campaign.actors.contains_key(*target) + || target + .strip_prefix("member:") + .is_some_and(|member_id| campaign.gestalt_members.contains_key(member_id)) + }) + .cloned() + .collect(); let mut gestalt_ids = vec![action.gestalt_id.clone()]; gestalt_ids.extend( action @@ -1437,7 +1448,7 @@ fn apply_strategic_tick_plan( at, kind: "gestalt_activity".into(), summary: strategic_activity_summary(&gestalt.name, &action.activity, &target_names), - actor_ids: vec![], + actor_ids, institution_ids, gestalt_ids, location_ids: locations, @@ -1553,6 +1564,21 @@ fn apply_strategic_tick_plan( .filter(|target| campaign.institutions.contains_key(*target)) .cloned() .collect(); + let mut actor_ids = vec![format!("member:{}", action.member_id)]; + actor_ids.extend( + action + .target_subject_ids + .iter() + .filter(|target| { + campaign.actors.contains_key(*target) + || target.strip_prefix("member:").is_some_and(|member_id| { + campaign.gestalt_members.contains_key(member_id) + }) + }) + .cloned(), + ); + actor_ids.sort(); + actor_ids.dedup(); let mut gestalt_ids = vec![action.source_gestalt_id.clone()]; gestalt_ids.extend( action @@ -1568,7 +1594,7 @@ fn apply_strategic_tick_plan( at, kind: "gestalt_member_activity".into(), summary: strategic_activity_summary(&member.name, &action.activity, &target_names), - actor_ids: vec![format!("member:{}", action.member_id)], + actor_ids, institution_ids, gestalt_ids, location_ids: action.location_ids, @@ -1654,6 +1680,13 @@ fn strategic_activity_summary( } fn agency_subject_name(campaign: &Campaign, subject_id: &str) -> Result { + if let Some(member_id) = subject_id.strip_prefix("member:") { + return campaign + .gestalt_members + .get(member_id) + .map(|value| value.name.clone()) + .ok_or_else(|| KernelError::Invalid("strategic activity target vanished".into())); + } campaign .actors .get(subject_id) @@ -2359,6 +2392,62 @@ mod tests { assert!(events[0].institution_ids.is_empty()); } + #[test] + fn colocated_gestalt_can_address_a_dormant_member_without_absorbing_their_agency() { + let mut value = hierarchical_refugee_campaign(); + value + .gestalt_members + .get_mut("mira") + .unwrap() + .last_location_id = Some("docks".into()); + value.gestalt_members.get_mut("mira").unwrap().gestalt_id = "dock-neighbors".into(); + let before = value.clone(); + let events = apply_strategic_tick_plan( + &mut value, + StrategicTickPlan { + gestalt_activities: vec![StrategicGestaltActivity { + gestalt_id: "dock-neighbors".into(), + activity: StrategicActivityKind::Communicate, + target_subject_ids: vec!["member:mira".into()], + location_ids: vec!["docks".into()], + public_channels: vec![], + }], + ..Default::default() + }, + ) + .unwrap(); + assert_eq!(value, before); + assert_eq!(events.len(), 1); + assert_eq!( + events[0].summary, + "South dock neighbors sends a communication to Mira Venn." + ); + assert_eq!(events[0].actor_ids, vec!["member:mira"]); + assert_eq!(events[0].gestalt_ids, vec!["dock-neighbors"]); + } + + #[test] + fn gestalt_cannot_address_a_dormant_member_at_another_location() { + let mut value = hierarchical_refugee_campaign(); + let before = value.clone(); + let error = apply_strategic_tick_plan( + &mut value, + StrategicTickPlan { + gestalt_activities: vec![StrategicGestaltActivity { + gestalt_id: "dock-neighbors".into(), + activity: StrategicActivityKind::Communicate, + target_subject_ids: vec!["member:mira".into()], + location_ids: vec!["docks".into()], + public_channels: vec![], + }], + ..Default::default() + }, + ) + .unwrap_err(); + assert!(error.to_string().contains("exact graph or location scope")); + assert_eq!(value, before); + } + #[test] fn gestalt_activity_rejects_a_nonadjacent_target_without_mutation() { let mut value = hierarchical_refugee_campaign(); diff --git a/crates/ghostlight-dungeon/src/resolution.rs b/crates/ghostlight-dungeon/src/resolution.rs index 7336ab8..3661f12 100644 --- a/crates/ghostlight-dungeon/src/resolution.rs +++ b/crates/ghostlight-dungeon/src/resolution.rs @@ -2245,7 +2245,7 @@ pub fn subject_state_references(campaign: &Campaign, subject_id: &str) -> Result } pub fn strategic_activity_targets(campaign: &Campaign, subject_id: &str) -> BTreeSet { - campaign + let mut targets = campaign .agency_relations .values() .filter(|relation| relation.active) @@ -2259,7 +2259,32 @@ pub fn strategic_activity_targets(campaign: &Campaign, subject_id: &str) -> BTre } }) .filter(|target| target != subject_id && campaign.agency_profiles.contains_key(target)) - .collect() + .collect::>(); + if let Some(source) = campaign.agency_profiles.get(subject_id) { + targets.extend( + campaign + .agency_profiles + .values() + .filter(|target| { + target.subject_id != subject_id + && target.active_leaf + && target.simulation_eligible + && !source.location_ids.is_disjoint(&target.location_ids) + }) + .map(|target| target.subject_id.clone()), + ); + targets.extend(campaign.gestalt_members.values().filter_map(|member| { + if member.materialized_actor_id.is_some() { + return None; + } + let location = dormant_member_location(campaign, &member.id).ok()?; + source + .location_ids + .contains(&location) + .then(|| format!("member:{}", member.id)) + })); + } + targets } pub fn member_activity_targets(campaign: &Campaign, member_id: &str) -> Result> { @@ -2271,6 +2296,7 @@ pub fn member_activity_targets(campaign: &Campaign, member_id: &str) -> Result (Vec, serde_json::V } fn cell_slice(campaign: &Campaign, cell: &SimulationCell) -> Result { - let constituents = cell + let mut member_exceptions = member_exceptions(campaign, cell)?; + let selected_member_ids = member_exceptions + .iter() + .map(|member| member.subject_id.clone()) + .collect::>(); + let mut constituents = cell .subject_ids .iter() .map(|id| constituent_slice(campaign, id)) .collect::>>()?; + for subject in &mut constituents { + subject.activity_target_ids.retain(|target| { + !target.starts_with("member:") || selected_member_ids.contains(target) + }); + } + for member in &mut member_exceptions { + member.activity_target_ids.retain(|target| { + !target.starts_with("member:") || selected_member_ids.contains(target) + }); + } let shared_knowledge = intersection( constituents .iter() @@ -399,7 +414,6 @@ fn cell_slice(campaign: &Campaign, cell: &SimulationCell) -> Result>(); + assert!(selected_member_ids.contains("member:mira")); + assert!(slice.constituents.iter().all(|constituent| { + constituent + .activity_target_ids + .iter() + .filter(|target| target.starts_with("member:")) + .all(|target| selected_member_ids.contains(target)) + })); + assert!(slice.member_exceptions.iter().all(|member| { + member + .activity_target_ids + .iter() + .filter(|target| target.starts_with("member:")) + .all(|target| selected_member_ids.contains(target)) + })); + assert!( + !serde_json::to_string(&slice) + .unwrap() + .contains("member:seventh") + ); assert!( !serde_json::to_string(&slice) .unwrap() diff --git a/docs/architecture/ghostlight-multiresolution-agency.md b/docs/architecture/ghostlight-multiresolution-agency.md index 06a6d3b..91d8fb2 100644 --- a/docs/architecture/ghostlight-multiresolution-agency.md +++ b/docs/architecture/ghostlight-multiresolution-agency.md @@ -95,8 +95,12 @@ is capped so a large population does not dump its roster into the prompt. A member may migrate only along an explicit active `migration` relation to a reachable destination leaf. At any active leaf, the same member may instead emit a `member_activity` from their exact location toward their current -population or a graph-adjacent subject. Migration and ordinary activity share -one per-member strategic action slot. This lets a refugee who mattered during +population, an explicitly related subject, or another selected named person or +active subject at that exact location. Only the capped salient member +exceptions exposed to the cell may appear as named targets in its prompt; +canonical validation still checks the target's identity and current location. +Migration and ordinary activity share one per-member strategic action slot. +This lets a refugee who mattered during a crisis choose to travel, offer work after settling, and later rematerialize as the same person without forcing every refugee to consume an active Persona cell or giving their agency to the destination Gestalt. @@ -275,8 +279,10 @@ prose is not a pressure. Populations may instead propose a bounded `gestalt_activity`: prepare, coordinate, investigate, recruit, obstruct, trade, or communicate. Its source is an exact active gestalt leaf; its targets must be adjacent canonical agency -subjects; its locations and channels must belong to the source's permitted -slice. The effect commits only an attributed attempted-activity event. It does +subjects through an explicit active relation or exact shared location. A +dormant named person is addressable as `member:` without becoming gestalt +state. Its locations and channels must belong to the source's permitted slice. +The effect commits only an attributed attempted-activity event. It does not claim successful discovery, recruitment, persuasion, delivery, exchange, obstruction, preparation, or target response. Event records carry exact gestalt participant IDs so later demand and perception can attribute the diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 301b0e6..403474f 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -81,6 +81,15 @@ knowledge, and channel scope. It shares the person's resolution key with migration and emits an attempt event without mutating the member delta or letting a containing arena or destination Gestalt steal the choice. +The next live failure exposed a reciprocal reach gap rather than a Persona +problem: South Harbor residents could perceive settled Mira but their typed +target list contained only explicit agency relations. Strategic activity reach +now includes exact shared location as well as explicit relations. Dormant named +people remain exact `member:` targets, and the scheduler filters every cell +slice to its capped salient member exceptions so a crowded location cannot dump +its roster into a prompt. Resolver and WorldKernel independently revalidate +identity, location, attribution, and attempt-only semantics. + The multiresolution body is deployed on Starfire. Typed health matched the immutable release, DeepSeek was ready, and the disposable live strategic-cell smoke committed three offscreen events plus three accessible news items without diff --git a/notes/ghostlight-current-system-map.md b/notes/ghostlight-current-system-map.md index ce1daea..efc3e2f 100644 --- a/notes/ghostlight-current-system-map.md +++ b/notes/ghostlight-current-system-map.md @@ -71,16 +71,20 @@ bounded local validation failures. Gestalt background choices use two distinct typed paths. Pressure transitions change exact unresolved markers. `gestalt_activity` records an attributed preparation, coordination, investigation, recruitment, obstruction, trade, or -communication attempt against graph-adjacent subjects without claiming the -outcome. The kernel derives the event text and exact gestalt participant IDs; -the arena and model prose own neither. +communication attempt against subjects connected by an explicit agency +relation or exact shared location without claiming the outcome. A selected +dormant member can be addressed by their durable ID; this does not union them +into the source population. The kernel derives the event text and exact +participant IDs; the arena and model prose own neither. Salient dormant members have their own `member_activity` path for ordinary local attempts. It uses the same bounded verbs, the member's exact location, -their current leaf plus graph-adjacent targets, and their own state/channel -permissions. Migration and activity conflict on one member key. A destination -population cannot inherit the person's offer, speech, or decision merely -because the cover placed them in the same arena. +their current leaf plus explicitly related or exactly co-located targets, and +their own state/channel permissions. Only the capped salient member exceptions +selected for the cell enter any prompt or can be named there. Migration and +activity conflict on one member key. A destination population cannot inherit +the person's offer, speech, or decision merely because the cover placed them +in the same arena. Population scale uses reversible individuation: diff --git a/notes/ghostlight-dungeon.md b/notes/ghostlight-dungeon.md index 13425e2..e993bb1 100644 --- a/notes/ghostlight-dungeon.md +++ b/notes/ghostlight-dungeon.md @@ -168,12 +168,14 @@ stake prose alone never impersonates world mutation. are bounded per constituent, and population pressure cannot silently become canon knowledge. Gestalts may also emit typed preparation, coordination, investigation, recruitment, obstruction, trade, or communication attempts - against exact graph-adjacent subjects. These commit only an attributed - attempted-activity event; success and target response require later admitted - effects. Durable dematerialized members use the same bounded activity - vocabulary with exact personal attribution; ordinary action and migration - share one per-person slot. Invalid, stale, timed-out, or malformed waves - leave world state unchanged. + against subjects with an explicit agency relation or exact shared location. + A capped salient dormant member may be addressed by durable ID without being + absorbed into a population voice. These commit only an attributed attempted- + activity event; success and target response require later admitted effects. + Durable dematerialized members use the same bounded activity vocabulary with + exact personal attribution; ordinary action and migration share one per- + person slot. Invalid, stale, timed-out, or malformed waves leave world state + unchanged. - The scheduler and return-time catch-up invoke the same `AdvanceStrategicTick` command. Catch-up processes missed ticks before accepting the next player action. - Live play has inference priority; background work stops launching new calls while a player turn is active. - Offscreen events generate information-channel-aware news leads. On return, the player may receive newspapers, reports, messages, or rumors only when the character has access to them; the news layer cannot reveal omniscient state. diff --git a/state/map.yaml b/state/map.yaml index 7bcd5bf..368c679 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -265,5 +265,5 @@ data_flow: current_status: summary: GhostlightDungeon production remains on the verified Starfire release. The acceptance candidate is pressuring multiresolution agency: exact Gestalt activity attempts, typed Gestalt event participation, nested lineage identity preservation, location-bounded presence projection, and automatic refugee callback rematerialization. Production state is untouched while the strict live-fire harness and receipts falsify the candidate. - next_action: Run the strict multiresolution refugee-return live fire. Require meaningful whole-setting background activity, exact rival-arena attribution, migration across unrelated nested lineage trees, and automatic presence-planner rematerialization of the same durable member. Profile stage receipts and repair only the owning context or typed effect seam. + next_action: Run the strict multiresolution refugee-return live fire after the shared-location activity-reach repair. Require meaningful whole-setting background activity, exact rival-arena attribution, migration across unrelated nested lineage trees, and automatic presence-planner rematerialization of the same durable member. Profile stage receipts and repair only the owning context or typed effect seam. gestalt_population: Gestalt Personas own shared population baselines; persistent member deltas own named individuality; WorldKernel materialization and approval-gated fission preserve lineage and identity. Presence projection is limited to nearby active leaves and exact-location dormant members. Simulation cells are derived covers only.