Allow bounded colocated gestalt interaction
This commit is contained in:
parent
c4906b531c
commit
bcb03e4647
8 changed files with 228 additions and 25 deletions
|
|
@ -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<String, KernelError> {
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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<String> {
|
||||
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::<BTreeSet<_>>();
|
||||
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<BTreeSet<String>> {
|
||||
|
|
@ -2271,6 +2296,7 @@ pub fn member_activity_targets(campaign: &Campaign, member_id: &str) -> Result<B
|
|||
dormant_member_location(campaign, member_id)?;
|
||||
let mut targets = strategic_activity_targets(campaign, &member.gestalt_id);
|
||||
targets.insert(member.gestalt_id.clone());
|
||||
targets.remove(&format!("member:{member_id}"));
|
||||
Ok(targets)
|
||||
}
|
||||
|
||||
|
|
@ -3116,8 +3142,31 @@ pub(crate) mod tests {
|
|||
else {
|
||||
unreachable!()
|
||||
};
|
||||
*target_subject_ids = vec!["player".into()];
|
||||
*target_subject_ids = vec!["unseen-stranger".into()];
|
||||
assert!(validate_and_resolve_wave(&value, &make_wave(invented_target)).is_err());
|
||||
|
||||
assert!(strategic_activity_targets(&value, "dockers").contains("member:mira"));
|
||||
let address_mira = CellActionProposal {
|
||||
subject_id: "dockers".into(),
|
||||
intent: "offer Mira paid work unloading the next boat".into(),
|
||||
intended_effect: "make the offer without deciding Mira's response".into(),
|
||||
priority: 75,
|
||||
state_references: vec!["subject:dockers".into()],
|
||||
public_channels: vec![],
|
||||
effect: StrategicCellEffect::GestaltActivity {
|
||||
gestalt_id: "dockers".into(),
|
||||
activity: StrategicActivityKind::Communicate,
|
||||
target_subject_ids: vec!["member:mira".into()],
|
||||
location_ids: vec!["center".into()],
|
||||
},
|
||||
};
|
||||
let address_plan = validate_and_resolve_wave(&value, &make_wave(address_mira)).unwrap();
|
||||
assert_eq!(address_plan.gestalt_activities.len(), 1);
|
||||
assert_eq!(address_plan.gestalt_activities[0].gestalt_id, "dockers");
|
||||
assert_eq!(
|
||||
address_plan.gestalt_activities[0].target_subject_ids,
|
||||
vec!["member:mira"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -382,11 +382,26 @@ fn resolution_demand_context(campaign: &Campaign) -> (Vec<String>, serde_json::V
|
|||
}
|
||||
|
||||
fn cell_slice(campaign: &Campaign, cell: &SimulationCell) -> Result<PermittedCellSlice> {
|
||||
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::<BTreeSet<_>>();
|
||||
let mut constituents = cell
|
||||
.subject_ids
|
||||
.iter()
|
||||
.map(|id| constituent_slice(campaign, id))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
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<PermittedCel
|
|||
.map(|subject| &subject.capabilities)
|
||||
.collect(),
|
||||
);
|
||||
let member_exceptions = member_exceptions(campaign, cell)?;
|
||||
let perceived_events = cell_perceived_events(campaign, &constituents, &member_exceptions);
|
||||
Ok(PermittedCellSlice {
|
||||
cell_id: cell.id.clone(),
|
||||
|
|
@ -934,6 +948,11 @@ mod tests {
|
|||
campaign
|
||||
.gestalt_members
|
||||
.insert("other".into(), member("other", false));
|
||||
for member_id in ["third", "fourth", "fifth", "sixth", "seventh"] {
|
||||
campaign
|
||||
.gestalt_members
|
||||
.insert(member_id.into(), member(member_id, false));
|
||||
}
|
||||
crate::resolution::ensure_agency_profiles(&mut campaign);
|
||||
campaign.agency_relations.insert(
|
||||
"migration".into(),
|
||||
|
|
@ -973,6 +992,31 @@ mod tests {
|
|||
.activity_target_ids
|
||||
.contains("refugees")
|
||||
);
|
||||
let selected_member_ids = slice
|
||||
.member_exceptions
|
||||
.iter()
|
||||
.map(|member| member.subject_id.clone())
|
||||
.collect::<BTreeSet<_>>();
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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:<id>` 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
|
||||
|
|
|
|||
|
|
@ -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:<id>` 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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue