diff --git a/crates/ghostlight-dungeon/src/persona.rs b/crates/ghostlight-dungeon/src/persona.rs index 9101e17..742bdd4 100644 --- a/crates/ghostlight-dungeon/src/persona.rs +++ b/crates/ghostlight-dungeon/src/persona.rs @@ -241,6 +241,10 @@ struct CellAppraisalProposal { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] enum CellInterpreterAgentCommand { + InspectActivityTargets { + subject_id: String, + query: String, + }, Submit { decisions: BTreeMap, }, @@ -265,6 +269,12 @@ enum CellInterpreterAgentOutput { #[derive(Clone, Debug, Serialize)] #[serde(tag = "kind", rename_all = "snake_case")] enum CellInterpreterFinding { + ActivityTargets { + subject_id: String, + query: String, + matches: BTreeMap, + total_matches: usize, + }, DraftProgress { decision_subject_ids: Vec, missing_subject_ids: Vec, @@ -851,13 +861,123 @@ fn append_actor_correction( )); } +const INITIAL_ACTIVITY_TARGET_LIMIT: usize = 24; +const ACTIVITY_TARGET_QUERY_LIMIT: usize = 12; + +fn initial_activity_targets( + targets: &BTreeMap, + relationships: &BTreeMap, + relevance_text: impl Iterator, + current_locations: &BTreeSet, +) -> BTreeMap { + let relevance_text = relevance_text.collect::>().join(" ").to_lowercase(); + let mut ranked = targets + .iter() + .map(|(subject_id, target)| { + let mut score = 0u8; + if relationships.contains_key(subject_id) { + score = score.saturating_add(4); + } + if relevance_text.contains(&subject_id.to_lowercase()) + || relevance_text.contains(&target.name.to_lowercase()) + { + score = score.saturating_add(2); + } + if target + .locations + .keys() + .any(|location_id| current_locations.contains(location_id)) + { + score = score.saturating_add(1); + } + (std::cmp::Reverse(score), subject_id, target) + }) + .collect::>(); + ranked.sort_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(right.1))); + ranked + .into_iter() + .take(INITIAL_ACTIVITY_TARGET_LIMIT) + .map(|(_, subject_id, target)| (subject_id.clone(), target.clone())) + .collect() +} + +fn initial_constituent_activity_targets( + subject: &CellConstituentSlice, +) -> BTreeMap { + initial_activity_targets( + &subject.activity_targets, + &subject.relationships, + subject + .goals + .iter() + .chain(&subject.pressures) + .chain(&subject.memories) + .cloned(), + &subject.location_ids, + ) +} + +fn initial_member_activity_targets( + member: &CellMemberSlice, +) -> BTreeMap { + initial_activity_targets( + &member.activity_targets, + &member.relationships, + member + .goals + .iter() + .chain(&member.pressures) + .chain(&member.memories) + .cloned(), + &BTreeSet::from([member.source_location_id.clone()]), + ) +} + +fn bounded_interpreter_slice(slice: &PermittedCellSlice) -> PermittedCellSlice { + let mut bounded = slice.clone(); + let decision_owner_ids = bounded.decision_owner_ids.clone(); + bounded + .constituents + .retain(|subject| decision_owner_ids.contains(&subject.subject_id)); + bounded + .member_exceptions + .retain(|member| decision_owner_ids.contains(&member.subject_id)); + for subject in &mut bounded.constituents { + subject.activity_targets = initial_constituent_activity_targets(subject); + } + for member in &mut bounded.member_exceptions { + member.activity_targets = initial_member_activity_targets(member); + } + bounded +} + +fn decision_owner_perceived_events(slice: &PermittedCellSlice) -> Vec { + slice + .perceived_events + .iter() + .filter_map(|event| { + let perceived_by_subject_ids = event + .perceived_by_subject_ids + .intersection(&slice.decision_owner_ids) + .cloned() + .collect::>(); + (!perceived_by_subject_ids.is_empty()).then(|| CellPerceivedEventSlice { + event_id: event.event_id.clone(), + summary: event.summary.clone(), + perceived_by_subject_ids, + }) + }) + .collect() +} + fn cell_projector_context(slice: &PermittedCellSlice) -> serde_json::Value { serde_json::json!({ "cell_id": slice.cell_id, "mode": slice.mode, "world_revision": slice.world_revision, "resolution_epoch": slice.resolution_epoch, - "constituents": slice.constituents.iter().map(|subject| serde_json::json!({ + "represented_constituent_count":slice.constituents.len(), + "constituents": slice.constituents.iter().filter(|subject| slice.decision_owner_ids.contains(&subject.subject_id)).map(|subject| serde_json::json!({ "subject_id": subject.subject_id, "subject_kind": subject.subject_kind, "name": subject.name, @@ -869,14 +989,16 @@ fn cell_projector_context(slice: &PermittedCellSlice) -> serde_json::Value { "information_channels": subject.information_channels, "reachable_destinations": subject.reachable_destinations, "migration_destinations": subject.migration_destinations, - "activity_targets": subject.activity_targets, + "activity_target_count":subject.activity_targets.len(), + "activity_targets":initial_constituent_activity_targets(subject), "goals": subject.goals, "relationships": subject.relationships, "memories": subject.memories, "already_committed_posture": subject.current_posture, "pressures": subject.pressures, })).collect::>(), - "member_exceptions": slice.member_exceptions.iter().map(|member| serde_json::json!({ + "represented_member_exception_count":slice.member_exceptions.len(), + "member_exceptions": slice.member_exceptions.iter().filter(|member| slice.decision_owner_ids.contains(&member.subject_id)).map(|member| serde_json::json!({ "subject_id": member.subject_id, "member_id": member.member_id, "name": member.name, @@ -886,7 +1008,8 @@ fn cell_projector_context(slice: &PermittedCellSlice) -> serde_json::Value { "capabilities": member.capabilities, "resources": member.resources, "migration_destinations": member.migration_destinations, - "activity_targets": member.activity_targets, + "activity_target_count":member.activity_targets.len(), + "activity_targets":initial_member_activity_targets(member), "goals": member.goals, "pressures": member.pressures, "relationships": member.relationships, @@ -894,7 +1017,7 @@ fn cell_projector_context(slice: &PermittedCellSlice) -> serde_json::Value { })).collect::>(), "shared_knowledge": slice.shared_knowledge, "shared_capabilities": slice.shared_capabilities, - "perceived_events": slice.perceived_events, + "perceived_events":decision_owner_perceived_events(slice), "causal_follow_through": slice.causal_follow_through, "world_clock_pressure": slice.world_clock_pressure, "canonical_locations": slice.canonical_locations, @@ -978,6 +1101,37 @@ fn cell_action_verifier_permission( )) } +fn cell_action_verifier_permission_for_action( + slice: &PermittedCellSlice, + action: &crate::domain::CellActionProposal, +) -> Result { + let target_ids = action + .effects + .iter() + .flat_map(|effect| match effect { + crate::domain::StrategicCellEffect::GestaltActivity { + target_subject_ids, .. + } + | crate::domain::StrategicCellEffect::ActorActivity { + target_subject_ids, .. + } + | crate::domain::StrategicCellEffect::MemberActivity { + target_subject_ids, .. + } => target_subject_ids.iter(), + _ => [].iter(), + }) + .cloned() + .collect::>(); + let mut permission = cell_action_verifier_permission(slice, &action.subject_id)?; + if let Some(targets) = permission + .get_mut("activity_targets") + .and_then(serde_json::Value::as_object_mut) + { + targets.retain(|target_id, _| target_ids.contains(target_id)); + } + Ok(permission) +} + fn coordination_target_contract(exact_subject_permission: &serde_json::Value) -> serde_json::Value { if let Some(source_gestalt_id) = exact_subject_permission .get("source_gestalt_id") @@ -1291,13 +1445,54 @@ struct CellInterpreterWorkbench { lived_stream: String, persona_turn: String, campaign_policy: String, - appraisal_schema: serde_json::Value, + visible_slice: PermittedCellSlice, draft: BTreeMap, repair_subject_ids: BTreeSet, accepted_verifier_bindings: BTreeSet, } impl CellInterpreterWorkbench { + fn inspect_activity_targets( + &mut self, + subject_id: &str, + query: &str, + ) -> Result { + if !self.active_subject_ids.contains(subject_id) { + return Err(anyhow!( + "activity-target inspection subject {subject_id} is not an active decision owner" + )); + } + let query = query.trim(); + if query.is_empty() || query.chars().count() > 160 { + return Err(anyhow!( + "activity-target inspection query must contain 1 to 160 characters" + )); + } + let full_targets = activity_targets_for_subject(&self.slice, subject_id)?; + let needle = query.to_lowercase(); + let matching = full_targets + .iter() + .filter(|(target_id, target)| { + target_id.to_lowercase().contains(&needle) + || target.name.to_lowercase().contains(&needle) + }) + .collect::>(); + let total_matches = matching.len(); + let matches = matching + .into_iter() + .take(ACTIVITY_TARGET_QUERY_LIMIT) + .map(|(target_id, target)| (target_id.clone(), target.clone())) + .collect::>(); + activity_targets_for_subject_mut(&mut self.visible_slice, subject_id)? + .extend(matches.clone()); + Ok(CellInterpreterFinding::ActivityTargets { + subject_id: subject_id.to_owned(), + query: query.to_owned(), + matches, + total_matches, + }) + } + fn progress(&self) -> CellInterpreterFinding { let decision_subject_ids = self.draft.keys().cloned().collect::>(); let decided = decision_subject_ids @@ -1507,7 +1702,15 @@ impl ModelAgentTool for CellInterpreterWorkbench { } else { CellInterpreterSchemaState::Repair(&self.repair_subject_ids) }; - cell_interpreter_agent_schema(&self.appraisal_schema, state) + let mut appraisal_schema = serde_json::to_value(schema_for!(CellAppraisalProposal)) + .map_err(|error| error.to_string())?; + constrain_cell_proposal_schema( + &mut appraisal_schema, + &self.visible_slice, + &self.active_subject_ids, + ) + .map_err(|error| error.to_string())?; + cell_interpreter_agent_schema(&appraisal_schema, state, &self.active_subject_ids) .map_err(|error| error.to_string()) } @@ -1517,6 +1720,21 @@ impl ModelAgentTool for CellInterpreterWorkbench { context: &ModelAgentToolContext, ) -> ModelAgentToolOutcome { match action.command { + CellInterpreterAgentCommand::InspectActivityTargets { subject_id, query } => { + match self.inspect_activity_targets(&subject_id, &query) { + Ok(observation) => ModelAgentToolOutcome::Continue { + observation, + receipts: Vec::new(), + }, + Err(error) => ModelAgentToolOutcome::Rejected { + finding: CellInterpreterFinding::LocalValidation { + diagnostic: error.to_string(), + decision_subject_ids: self.repair_subject_ids.iter().cloned().collect(), + }, + receipts: Vec::new(), + }, + } + } CellInterpreterAgentCommand::Submit { decisions } => { if !self.draft.is_empty() { return ModelAgentToolOutcome::Rejected { @@ -1565,6 +1783,48 @@ fn required_projection_subject_ids(slice: &PermittedCellSlice) -> BTreeSet( + slice: &'a PermittedCellSlice, + subject_id: &str, +) -> Result<&'a BTreeMap> { + if let Some(subject) = slice + .constituents + .iter() + .find(|subject| subject.subject_id == subject_id) + { + return Ok(&subject.activity_targets); + } + slice + .member_exceptions + .iter() + .find(|member| member.subject_id == subject_id) + .map(|member| &member.activity_targets) + .ok_or_else(|| anyhow!("cell has no activity-target permissions for {subject_id}")) +} + +fn activity_targets_for_subject_mut<'a>( + slice: &'a mut PermittedCellSlice, + subject_id: &str, +) -> Result<&'a mut BTreeMap> { + if let Some(index) = slice + .constituents + .iter() + .position(|subject| subject.subject_id == subject_id) + { + return Ok(&mut slice.constituents[index].activity_targets); + } + if let Some(index) = slice + .member_exceptions + .iter() + .position(|member| member.subject_id == subject_id) + { + return Ok(&mut slice.member_exceptions[index].activity_targets); + } + Err(anyhow!( + "cell has no mutable activity-target viewport for {subject_id}" + )) +} + fn allowed_constituent_effect_types(subject: &CellConstituentSlice) -> Vec<&'static str> { match subject.subject_kind { crate::domain::AgencySubjectKind::Actor => { @@ -1677,18 +1937,22 @@ impl CellProjectionEngine { focus.responder_subject_id, focus.anchor_reference, focus.summary ) }) - .chain(slice.perceived_events.iter().map(|event| { - format!( - "Perceived by [{}]: {}", - event - .perceived_by_subject_ids - .iter() - .cloned() - .collect::>() - .join(", "), - event.summary - ) - })) + .chain( + decision_owner_perceived_events(&slice) + .into_iter() + .map(|event| { + format!( + "Perceived by [{}]: {}", + event + .perceived_by_subject_ids + .iter() + .cloned() + .collect::>() + .join(", "), + event.summary + ) + }), + ) .collect::>() .join("\n"); let mode_guidance = cell_projector_mode_guidance(&slice.mode); @@ -1821,20 +2085,19 @@ impl CellProjectionEngine { .require(&slice.cell_id, &slice.snapshot_binding, "cell_interpreter") .await .map_err(|error| cell_pipeline_failure(error, stage_receipts.clone()))?; - let mut schema = serde_json::to_value(schema_for!(CellAppraisalProposal)) - .map_err(|error| cell_pipeline_failure(error.into(), stage_receipts.clone()))?; - constrain_cell_proposal_schema(&mut schema, &slice, &active_subject_ids) - .map_err(|error| cell_pipeline_failure(error, stage_receipts.clone()))?; - let interpreter_context = - serde_json::to_string(&cell_interpreter_context(&slice, &active_subject_ids)) - .map_err(|error| cell_pipeline_failure(error.into(), stage_receipts.clone()))?; + let visible_slice = bounded_interpreter_slice(&slice); + let interpreter_context = serde_json::to_string(&cell_interpreter_context( + &visible_slice, + &active_subject_ids, + )) + .map_err(|error| cell_pipeline_failure(error.into(), stage_receipts.clone()))?; let permission_guidance = format!( concat!( "Emit at most {} exact constituent- or named-member-attributed attempts. Priority is an urgency score from 0 to 100 where higher numbers resolve first. ", "Every subject in exact_permissions owns one schema-keyed decision slot. Fill that exact slot with one action or one inaction. Use undecided only when the Persona turn supplied no explicit choice or hold; runtime will retry the Persona rather than let the Interpreter invent one. Do not let a voiced constituent vanish during interpretation. ", "Each action carries an effects object whose exact subject-specific lane keys are supplied by the schema. Across scalar effects and every expanded activity scope, an action has one to four exact effects total. Scalar lanes contain one typed effect. An activities lane is one object keyed by chosen activity kinds; each non-null kind contains an array of one to four separate exact target-and-location scopes. Preserve repeated uses of one activity kind as separate scopes when their targets or locations differ; never union scopes across distinct locations or audiences. Top-level lane names are stable across subjects: use null for a null-only unavailable lane and for any schema-required optional lane or activity key the subject does not use. A non-null lane or activity key absent from its exact schema is structurally unavailable: do not emit it and do not invent a destination. Pressure and migration lanes each have one slot. Preserve every means of one chosen course in one action. With relocation, activities at the exact snapshot location occur before departure and activities at the exact admitted destination occur after arrival; activity effects inside each location phase are an atomic set. Field and array order is not chronology. Never split one subject's single choice into multiple actions. ", "Use gestalt_activities or member_activities for concrete attempts that do not themselves change pressure. A cohesive Gestalt coordinating its own unnamed internal members uses coordinate with an empty target_subject_ids list; do not invent its containing population, a distant population, or another canonical subject as the target of internal coordination. Cite the smallest exact set of state_references that materially supports each attempt; the permission list is an upper bound, not a checklist to echo. ", - "target_subject_ids and location_ids must come from that exact subject's permissions. activity_targets is the exact canonical target map: each key is the authoritative ID and each value supplies the target's name and current canonical locations. Use an ID only when the Persona addresses that named target, never merely because the ID is permitted. If an addressed person or role has no matching activity_targets entry, it is not a canonical target in this slice. reachable_destinations maps exact actor-movement destination IDs to names. migration_destinations maps exact population destination IDs to names and locations. When the Persona chooses to go to a canonical target, compare the target's current locations with the acting subject's current location and exact reachable destinations; never guess a destination from an opaque ID. Every activity has at most four unique target_subject_ids; choose the four most causally relevant when more permitted subjects are involved. A member activity uses exactly the member's source_location_id. Internal work is prepare with no targets. A local investigate may have no target and use the exact current location to seek information from the environment or an unnamed ordinary role; asking an unnamed clerk or dock master for facts maps here and records only the inquiry, never a reply or discovery. A local communicate may likewise have no target at the exact current location when the Persona speaks, sends, offers, asks permission, or notifies an unnamed ordinary role; it records only the source's outgoing attempt, never a listener, reply, acceptance, or outcome. Communication with a canonical subject requires that exact target ID. When one utterance addresses canonical subjects and an unnamed public audience, emit one communicate activity with those exact target IDs and place its admitted public reach in public_channels; do not invent a second communicate lane. Never substitute a containing population, related institution, or merely permitted ID for an unnamed role. ", + "target_subject_ids and location_ids must come from that exact subject's permissions. activity_targets is a bounded exact viewport: each key is an authoritative ID and each value supplies the target's name and current canonical locations. When the Persona addresses a canonical subject absent from that viewport, call inspect_activity_targets for that decision owner and a distinctive fragment of the target's supplied name before drafting or repairing the decision. Inspection exposes existing permission; it cannot create it. Use an ID only when the Persona addresses that named target, never merely because the ID is permitted. If inspection returns no matching target, the addressed person or role is not a canonical target in this slice. reachable_destinations maps exact actor-movement destination IDs to names. migration_destinations maps exact population destination IDs to names and locations. When the Persona chooses to go to a canonical target, compare the target's current locations with the acting subject's current location and exact reachable destinations; never guess a destination from an opaque ID. Every activity has at most four unique target_subject_ids; choose the four most causally relevant when more permitted subjects are involved. A member activity uses exactly the member's source_location_id. Internal work is prepare with no targets. A local investigate may have no target and use the exact current location to seek information from the environment or an unnamed ordinary role; asking an unnamed clerk or dock master for facts maps here and records only the inquiry, never a reply or discovery. A local communicate may likewise have no target at the exact current location when the Persona speaks, sends, offers, asks permission, or notifies an unnamed ordinary role; it records only the source's outgoing attempt, never a listener, reply, acceptance, or outcome. Communication with a canonical subject requires that exact target ID. When one utterance addresses canonical subjects and an unnamed public audience, emit one communicate activity with those exact target IDs and place its admitted public reach in public_channels; do not invent a second communicate lane. Never substitute a containing population, related institution, or merely permitted ID for an unnamed role. ", "Write intended_effect as the affirmative attempted acts, never the purpose, restraint, condition to preserve, hoped-for outcome, or target response. Keep purpose and restraint in intent. Respecting choice, declining coercion, leaving state unchanged, or waiting for another subject does not add a typed effect unless the Persona separately chooses an observable act. Merely waiting, watching, staying, holding position, or remaining ready is attributed inaction, not prepare. prepare requires concrete work on a bounded arrangement, repair, resource, or capability-backed readiness change. Institution posture must be a specific materially new commitment or withholding of at most {} characters. already_committed_posture is state already in force: maintaining, continuing, or restating it is inaction and must not emit an institution action. Gestalt pressure_resolutions copy exact current_pressures; additions are new unresolved constraints, never completed actions. Use only permitted state references. public_channels means durable publication of this attempt through exact allowed_persistent_publication_channels; it is not a perception method or ordinary local speech. Use [] when that exact list is empty. ", "A population that chooses to board, depart, or relocate together to one supplied migration_destinations key emits gestalt_migration; do not reduce it to prepare. It relocates only that exact population leaf and never implies a named member traveled. A named member who chooses to board, depart, travel, or join a supplied destination emits member_migration; use prepare only while departure remains unchosen. ", "A population or arena cannot migrate a person. Runtime binds identity and effect owner IDs from subject_id. Do not emit institution_id, gestalt_id, actor_id, or member_id inside effect. An inaction means that exact subject takes no strategic action in this horizon. Record only a subject that explicitly holds, waits without making another attempt, or merely continues already_committed_posture in the Persona turn as an inaction, using its exact subject_id and a concrete reason of at most 160 characters. Waiting for the result of an action, withholding a different possible action, or declining one option after choosing another does not make the chosen action an inaction. Never invent an inaction for an unvoiced subject or use absence of a Persona decision as a reason. Inactions share the same count limit stated for actions. A subject cannot appear in both actions and inactions. When nobody acts, actions is empty and inactions must still contain at least one exact attributed decision from the Persona turn." @@ -1860,7 +2123,7 @@ impl CellProjectionEngine { persona_output: &persona.narrative, output_schema: None, domain_guidance: &format!( - "{permission_guidance} Operate the private Interpreter workbench. Its current typed action schema is authoritative and exposes only the transition legal now: one complete submit initially, then exact named upsert_decision repairs after rejection. A rejected submit preserves its draft. Only the deterministic workbench can accept the appraisal." + "{permission_guidance} Operate the private Interpreter workbench. Its current typed action schema is authoritative and exposes only the transition legal now: inspect_activity_targets may expand one decision owner's bounded target viewport, one complete submit creates the draft, and exact named upsert_decision repairs follow rejection. A rejected submit preserves its draft. Only the deterministic workbench can accept the appraisal." ), }); let mut interpreter_causal_receipts = causal_receipts; @@ -1875,7 +2138,7 @@ impl CellProjectionEngine { source_receipt_ids, temperature: Some(0.0), max_output_tokens: Some(1_600), - max_steps: active_subject_ids.len().saturating_add(4).clamp(4, 8), + max_steps: active_subject_ids.len().saturating_add(6).clamp(6, 10), }; let mut workbench = CellInterpreterWorkbench { model: self.model.clone(), @@ -1886,7 +2149,7 @@ impl CellProjectionEngine { lived_stream: lived.text.clone(), persona_turn: persona.narrative.clone(), campaign_policy: campaign_policy.clone(), - appraisal_schema: schema, + visible_slice, draft: BTreeMap::new(), repair_subject_ids: BTreeSet::new(), accepted_verifier_bindings: BTreeSet::new(), @@ -1941,7 +2204,7 @@ async fn run_cell_effect_verifier_wave( let verifier_schema = cell_effect_verifier_schema(1)?; let mut jobs = tokio::task::JoinSet::new(); for (action_index, action) in actions.iter().enumerate() { - let exact_subject_permission = cell_action_verifier_permission(slice, &action.subject_id)?; + let exact_subject_permission = cell_action_verifier_permission_for_action(slice, action)?; let coordination_target_contract = coordination_target_contract(&exact_subject_permission); let verifier_context = serde_json::json!({ "effect_order_contract":{ @@ -3073,6 +3336,7 @@ enum CellInterpreterSchemaState<'a> { fn cell_interpreter_agent_schema( appraisal_schema: &serde_json::Value, state: CellInterpreterSchemaState<'_>, + active_subject_ids: &BTreeSet, ) -> Result { let decisions_schema = appraisal_schema .pointer("/properties/decisions") @@ -3082,7 +3346,7 @@ fn cell_interpreter_agent_schema( .get("properties") .and_then(serde_json::Value::as_object) .ok_or_else(|| anyhow!("cell appraisal schema has no exact decision owners"))?; - let commands = match state { + let mut commands = match state { CellInterpreterSchemaState::Initial => vec![serde_json::json!({ "type":"object", "additionalProperties":false, @@ -3116,6 +3380,16 @@ fn cell_interpreter_agent_schema( .collect::>>()? } }; + commands.push(serde_json::json!({ + "type":"object", + "additionalProperties":false, + "required":["kind","subject_id","query"], + "properties":{ + "kind":{"const":"inspect_activity_targets"}, + "subject_id":{"type":"string","enum":active_subject_ids}, + "query":{"type":"string","minLength":1,"maxLength":160} + } + })); Ok(serde_json::json!({ "$schema":"https://json-schema.org/draft/2020-12/schema", "type":"object", @@ -4180,9 +4454,7 @@ mod tests { calls: AtomicUsize::new(0), faction_seven_calls: AtomicUsize::new(0), }); - let mut appraisal_schema = - serde_json::to_value(schema_for!(CellAppraisalProposal)).unwrap(); - constrain_cell_proposal_schema(&mut appraisal_schema, &slice, &active_subject_ids).unwrap(); + let visible_slice = bounded_interpreter_slice(&slice); let mut workbench = CellInterpreterWorkbench { model: model.clone(), permit: Arc::new(AllowAllPermit), @@ -4192,7 +4464,7 @@ mod tests { lived_stream: "At location forum: Faction Six and Faction Seven.".into(), persona_turn: "Faction Six withholds; Faction Seven publishes.".into(), campaign_policy: "{}".into(), - appraisal_schema, + visible_slice, draft: BTreeMap::new(), repair_subject_ids: BTreeSet::new(), accepted_verifier_bindings: BTreeSet::new(), @@ -4248,7 +4520,21 @@ mod tests { assert!(!repair_schema_text.contains("submit")); assert!(repair_schema_text.contains("upsert_decision")); assert!(repair_schema_text.contains("faction-06")); - assert!(!repair_schema_text.contains("faction-07")); + assert!(repair_schema_text.contains("inspect_activity_targets")); + assert!( + !jsonschema::validator_for(&repair_schema) + .unwrap() + .is_valid(&serde_json::json!({ + "command":{ + "kind":"upsert_decision", + "subject_id":"faction-07", + "decision":{"inaction":{ + "subject_id":"faction-07", + "reason":"This accepted owner is not repairable." + }} + } + })) + ); assert!(!repair_schema_text.contains("inspect_draft")); assert!(!repair_schema_text.contains("remove_decision")); @@ -4336,9 +4622,7 @@ mod tests { slice.decision_owner_ids.insert("faction-07".into()); slice.max_actions = 2; let active_subject_ids = slice.decision_owner_ids.clone(); - let mut appraisal_schema = - serde_json::to_value(schema_for!(CellAppraisalProposal)).unwrap(); - constrain_cell_proposal_schema(&mut appraisal_schema, &slice, &active_subject_ids).unwrap(); + let visible_slice = bounded_interpreter_slice(&slice); let mut workbench = CellInterpreterWorkbench { model: Arc::new(CachingVerifierModel { calls: AtomicUsize::new(0), @@ -4351,7 +4635,7 @@ mod tests { lived_stream: "At location forum: Faction Six and Faction Seven.".into(), persona_turn: "Both factions hold for the count.".into(), campaign_policy: "{}".into(), - appraisal_schema, + visible_slice, draft: BTreeMap::from([ ( "faction-06".into(), @@ -4508,8 +4792,13 @@ mod tests { assert!(schema.pointer("/properties/decisions").is_some()); assert!(schema.pointer("/properties/actions").is_none()); assert!(schema.pointer("/properties/inactions").is_none()); - let initial_schema = - cell_interpreter_agent_schema(&schema, CellInterpreterSchemaState::Initial).unwrap(); + let active_subject_ids = BTreeSet::from(["faction-06".to_owned()]); + let initial_schema = cell_interpreter_agent_schema( + &schema, + CellInterpreterSchemaState::Initial, + &active_subject_ids, + ) + .unwrap(); let initial_schema_text = serde_json::to_string(&initial_schema).unwrap(); assert_eq!(initial_schema["type"], "object"); assert!(initial_schema.get("oneOf").is_none()); @@ -4520,6 +4809,7 @@ mod tests { ); assert!(initial_schema_text.contains("submit")); assert!(!initial_schema_text.contains("upsert_decision")); + assert!(initial_schema_text.contains("inspect_activity_targets")); assert!(!initial_schema_text.contains("inspect_draft")); assert!(!initial_schema_text.contains("remove_decision")); assert!(initial_schema_text.contains("\"maxLength\":240")); @@ -4580,6 +4870,7 @@ mod tests { let repair_schema = cell_interpreter_agent_schema( &schema, CellInterpreterSchemaState::Repair(&repair_subject_ids), + &active_subject_ids, ) .unwrap(); let repair_schema_text = serde_json::to_string(&repair_schema).unwrap(); @@ -4627,6 +4918,110 @@ mod tests { ); } + fn add_many_activity_targets(slice: &mut PermittedCellSlice, count: usize) { + slice.constituents[0].subject_kind = AgencySubjectKind::Actor; + slice.constituents[0].activity_targets = (0..count) + .map(|index| { + ( + format!("target:{index:04}"), + CellActivityTargetSlice { + name: format!("Target {index:04}"), + locations: BTreeMap::from([("forum".into(), "Forum".into())]), + }, + ) + }) + .collect(); + } + + #[test] + fn projector_context_is_bounded_by_decision_owners_not_cell_population() { + let mut slice = fixture_cell_slice(); + add_many_activity_targets(&mut slice, 1_000); + for index in 0..500 { + let mut inactive = slice.constituents[0].clone(); + inactive.subject_id = format!("inactive:{index:04}"); + inactive.name = format!("Inactive {index:04}"); + inactive.activity_targets.clear(); + slice.constituents.push(inactive); + } + slice.perceived_events.push(CellPerceivedEventSlice { + event_id: "event:crowded".into(), + summary: "A public count reaches the forum.".into(), + perceived_by_subject_ids: slice + .constituents + .iter() + .map(|subject| subject.subject_id.clone()) + .collect(), + }); + + let context = cell_projector_context(&slice); + assert_eq!(context["represented_constituent_count"], 501); + assert_eq!(context["constituents"].as_array().unwrap().len(), 1); + assert_eq!( + context["constituents"][0]["activity_targets"] + .as_object() + .unwrap() + .len(), + INITIAL_ACTIVITY_TARGET_LIMIT + ); + assert_eq!( + context["perceived_events"][0]["perceived_by_subject_ids"] + .as_array() + .unwrap() + .len(), + 1 + ); + assert!(serde_json::to_vec(&context).unwrap().len() < 64_000); + } + + #[test] + fn interpreter_inspection_expands_context_without_expanding_authority() { + let mut slice = fixture_cell_slice(); + add_many_activity_targets(&mut slice, 1_000); + let visible_slice = bounded_interpreter_slice(&slice); + let active_subject_ids = slice.decision_owner_ids.clone(); + let model = Arc::new(CachingVerifierModel { + calls: AtomicUsize::new(0), + faction_seven_calls: AtomicUsize::new(0), + }); + let mut workbench = CellInterpreterWorkbench { + model, + permit: Arc::new(AllowAllPermit), + interpreter_model: "flash".into(), + slice, + active_subject_ids, + lived_stream: "Faction Six addresses Target 0999.".into(), + persona_turn: "Tell Target 0999 to attend.".into(), + campaign_policy: "{}".into(), + visible_slice, + draft: BTreeMap::new(), + repair_subject_ids: BTreeSet::new(), + accepted_verifier_bindings: BTreeSet::new(), + }; + + let initial_schema = serde_json::to_string(&workbench.action_schema().unwrap()).unwrap(); + assert!(!initial_schema.contains("target:0999")); + let finding = workbench + .inspect_activity_targets("faction-06", "Target 0999") + .unwrap(); + assert!(matches!( + finding, + CellInterpreterFinding::ActivityTargets { + total_matches: 1, + .. + } + )); + let expanded_schema = serde_json::to_string(&workbench.action_schema().unwrap()).unwrap(); + assert!(expanded_schema.contains("target:0999")); + assert_eq!( + activity_targets_for_subject(&workbench.slice, "faction-06") + .unwrap() + .len(), + 1_000, + "inspection must not truncate or rewrite canonical permission" + ); + } + #[test] fn cell_schema_keeps_unavailable_effect_lanes_null_only() { let mut slice = fixture_cell_slice(); diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index e79bccc..e4024f1 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -25,7 +25,7 @@ own volatile workspace identity and the release witness owns deployed identity. ## Current full-world pressure test -Preserve immutable Runs 75 through 108. Run 108 reaches the exact 2,400-subject +Preserve immutable Runs 75 through 109. Run 108 reaches the exact 2,400-subject scale target at complexity round 43; no complexity round may replay. Bounded parallel title-by-realm compaction and deterministic parallel child-ID @@ -58,24 +58,38 @@ and reproduced the failure without writing another session checkpoint. Pushed source `8dd3147` keeps compaction journals structurally compact. Immutable Run 108 reached exactly 2,400 actionable subjects at round 43. Clock -binding then advanced the campaign to revision 1551 and resolution epoch 1545 -before strategic wave 1. All five wave-1 pulses were rejected and the run -failed terminally; every Nemesis attempt stopped before reasoning with -`connector frame exceeded its bound`. No simulation cell committed and -fictional time did not advance. +binding then advanced the campaign to revision 1551 and resolution epoch 1545. +Pushed source `b61e762` bounds Nemesis with paged anchors and responder dossiers +while retaining complete deterministic eligibility privately. -`propose_causal_follow_through` serializes the complete responder catalog into -instructions, then serializes the initial 24 public anchors plus every current -pressure and clock anchor with every eligible responder view embedded again. -That multiplicative projection is the current defect. The tool already owns the -complete deterministic eligibility catalog and paged world/subject history. -Keep initial anchor summaries bounded; add paged current-anchor retrieval and -paged eligible-responder retrieval for inspected anchors; keep full eligibility -private for admission. Resolution remains cover owner and WorldKernel remains -commit owner. Do not raise the connector bound or truncate canonical authority. -The acceptance launcher retains one 35 MiB content-addressed runner per exact -source commit and deletes disposable compiler output; do not restore a -persistent Cargo target tree. +Immutable Run 109 at +`/var/lib/gamecult/ghostlight-dungeon/acceptance/full-world-delvehold-b61e762-109` +proved that cut and dispatched the exact 240-cell wave. Pulse one completed 214 +cells; four bounded retries raised the durable checkpoint to 226 completed and +14 failed cells. The final partial checkpoint SHA-256 is +`f502ad560e0f23db226c326072e5e8a6d0ed9975f0e63bc67606d7d7cb9b2c27`. +All 14 terminal failures are connector-frame bounds: six Projectors received +hundreds of inactive constituent dossiers, while eight Interpreters received +unbounded exact activity-target maps in context and generated schemas. The +smallest failed Interpreter cells contain only two subjects, so repartitioning +alone cannot fix the defect. No fictional wave committed and time did not +advance. + +The active cut makes context a bounded query projection. Projector receives +only exact decision-owner dossiers and at most 24 deterministically relevant +activity targets. The existing Interpreter agent workbench starts from the +same bounded target viewport and may inspect up to 12 exact name/ID matches; +its dynamic schema expands only with inspected targets. Full cell membership +and full permissions remain private deterministic state. Effect verification +receives only targets cited by the candidate action. Resolution remains cover +owner and WorldKernel remains commit owner. Do not raise the connector bound, +truncate canonical authority, or split cells merely to paper over projection. + +After focused gates, commit and push, then clone Run 109 and resume only the 14 +failed cells from its exact checkpoint. Do not replay complexity, clock binding, +Nemesis, or the 226 completed cell membranes. The acceptance launcher retains +one content-addressed runner per exact source commit and deletes disposable +remote compiler output. ## Mission and current product shape diff --git a/notes/ghostlight-current-system-map.md b/notes/ghostlight-current-system-map.md index d826e9c..2824906 100644 --- a/notes/ghostlight-current-system-map.md +++ b/notes/ghostlight-current-system-map.md @@ -178,7 +178,8 @@ exact campaign revision -> scheduler-owned Nemesis agent binds exact committed event, pressure, or clock anchors to eligible decision owners inside that cover -> deterministic agenda admission checks anchor visibility, simulation authority, player exclusion, and per-cell decision quota -> one private Projector/Persona/Interpreter membrane per cell - -> Interpreter agent submits or incrementally edits one private exact-decision draft + -> Projector receives exact decision-owner dossiers plus a bounded relevant-target viewport, never every inactive constituent dossier + -> Interpreter agent inspects bounded exact target matches, then submits or incrementally edits one private exact-decision draft -> deterministic draft compiler checks exact ownership, permissions, topology, and bounds -> independent per-action effect verifier checks the typed action against the Persona choice -> accepted unchanged actions retain their exact verifier binding across local draft repair @@ -382,12 +383,17 @@ status reconstruction. Resolution-demand focal IDs are salience hints, not partition commands. They cannot create mandatory singleton cells or exceed the configured budget. Cell -Projectors receive decision-relevant situation state; cell Interpreters receive -exact permissions and the narrative products. Membership and revision bindings -are derived by the runtime, so a model is never asked to copy an invariant that -the planner already owns. Stable prompt prefixes are deliberately placed before -dynamic state, and provider receipts expose per-attempt token/cache usage plus -bounded local validation failures. +Projectors receive decision-relevant situation state for the scheduler-selected +decision owners, the count of other represented subjects, and at most 24 exact +activity targets ranked by explicit relationship, mention in current goals or +pressure, co-location, and stable ID. Inactive constituents remain represented +by the exact cell cover but their full dossiers do not enter the model frame. +Cell Interpreters receive exact permissions for active decision owners and the +narrative products. Membership and revision bindings are derived by the +runtime, so a model is never asked to copy an invariant that the planner already +owns. Stable prompt prefixes are deliberately placed before dynamic state, and +provider receipts expose per-attempt token/cache usage plus bounded local +validation failures. `ModelPort` remains Ghostlight's single inference seam. The Codex-backed implementation is an ordinary typed consumer of the independent CodexConnector @@ -433,7 +439,13 @@ subjects: complete submits bind decisions by owner-key, while incremental edits are rebound and validated under the selected owner by the workbench. Target IDs, canonical locations, pressure resolutions, movement destinations, state references, and public channels are enumerated from that subject's permitted -slice. Movement and population-migration variants disappear +slice. Activity targets are the exception to eager projection, not to exact +authority: the initial schema exposes the bounded relevant viewport, and the +same Interpreter workbench may query one active decision owner by a supplied +name/ID fragment for at most 12 exact matches. A successful query expands only +that workbench's dynamic schema. It cannot add a target to canonical permission, +change the cell, or bypass the full deterministic validator. Movement and +population-migration variants disappear when the subject has no exact destination; target-requiring activities cannot emit an empty target list. Semantic correction therefore judges meaning rather than repairing type combinations local code already knows are impossible. @@ -442,12 +454,15 @@ Each permitted activity target is one runtime-derived descriptor keyed by its authoritative ID, with the target's exact name and current canonical locations. Reachable actor destinations map exact location IDs to names; population migration destinations carry both population and location identity. The -Projector, Interpreter, and effect verifier can distinguish a canonical target -actually named by the Persona from an unnamed local role, and can prove whether -“go to Reed” means movement at all. Opaque permitted IDs are not enough reason -to substitute a containing population, related institution, or unrelated -destination. Descriptor prose is disambiguation context, not a new subject or -fuzzy-match authority. +Projector and Interpreter bounded views distinguish a canonical target actually +named by the Persona from an unnamed local role, and can prove whether “go to +Reed” means movement at all. The effect verifier receives only the exact target +descriptors cited by its already structurally valid candidate action; the full +permission map remains private to deterministic validation. Opaque permitted +IDs are not enough reason to substitute a containing population, related +institution, or unrelated destination. Descriptor prose and target queries are +disambiguation context, not a new subject, permission writer, or fuzzy-match +authority. Targetless local activity is explicit rather than encoded through a convenient unrelated subject. Preparation, investigation, communication, and obstruction diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 5a8d8df..bb6a00c 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -242,4 +242,5 @@ {"note": "Run 98 exposed a false global-name uniqueness invariant during serial admission of 109 successful round-12 proposals: distinct canonical people in different Gestalts independently shared Sera Venn and Sera Vey. Exact address resolution already operates on canonical subject IDs. Gestalt individuation now keeps member and Actor ID uniqueness but permits ambiguous public names; no alias registry or reconciliation owner was added. Focused same-name/distinct-ID admission and exact-ID rejection regression, 499 library tests, 18 strategic-smoke tests, and all-target check pass.", "status": "ok", "ts": "2026-08-31T22:52:47+00:00", "type": "correction"} {"note": "Run 100 proved the individuation model boundary still exposed deterministic fields it did not own: the sole retried Veil dispatch repeatedly emitted null last/relevance revisions while 80 round-14 proposals remained frozen. WorldComplexityMemberDraft now contains only semantic member content; WorldComplexityTool constructs schema, exact parent/version/location, materialization state, and relevance revisions before the unchanged validator/kernel path. Two focused tests, 499 library tests, 18 strategic-smoke tests, and all-target check pass.", "status": "ok", "ts": "2026-08-31T23:10:08+00:00", "type": "correction"} {"note": "Runs 101 and 103 proved a structural compaction contract mismatch: the checkpoint/tool required each unresolved lead to contain 1..600 characters, but the generated action schema bounded only the array count, and the tool returned one opaque combined finding. Round17 retains 52 commits and 23 completed local summaries. The existing compaction owner now publishes identical per-item schema bounds and complete indexed findings; focused regression, 500 library tests, 18 strategic-smoke tests, and all-target check pass.", "status": "ok", "ts": "2026-08-31T23:28:51+00:00", "type": "correction"} -{"ts": "2026-08-31T23:41:09+00:00", "type": "correction", "status": "ok", "note": "Run 104 round19 exposed a frozen-batch duplicate person: ordinals1272/1289 proposed the exact same canonical local ID and name Tarin Vel from adjacent Avelune carrier populations with near-identical content; serial admission failed after commits through1288. The round owner now canonicalizes exact local IDs, retains the earliest dispatch, publishes an immutable supersession checkpoint for later duplicates, excludes them from commit/session journals, and lets demand remeasure. Existing-world ID rejection remains with the shared validator. 500 library tests, 19 strategic-smoke tests, and all-target check pass."} +{"note": "Run 104 round19 exposed a frozen-batch duplicate person: ordinals1272/1289 proposed the exact same canonical local ID and name Tarin Vel from adjacent Avelune carrier populations with near-identical content; serial admission failed after commits through1288. The round owner now canonicalizes exact local IDs, retains the earliest dispatch, publishes an immutable supersession checkpoint for later duplicates, excludes them from commit/session journals, and lets demand remeasure. Existing-world ID rejection remains with the shared validator. 500 library tests, 19 strategic-smoke tests, and all-target check pass.", "status": "ok", "ts": "2026-08-31T23:41:09+00:00", "type": "correction"} +{"ts": "2026-09-01T01:29:25+00:00", "type": "verification", "status": "ok", "note": "Immutable Run 109 proved Nemesis paging and the 240-cell dispatch at 2,400 subjects, banked 226 of 240 cell terminals, then isolated 14 connector-frame failures: six Projector frames serialized inactive constituent dossiers and eight Interpreter frames serialized unbounded activity-target permissions. Bounded decision-owner projection plus Interpreter target inspection preserves full private authority while keeping model frames bounded; 505 library tests and 20 strategic-smoke tests pass."} diff --git a/state/ghostlight-state.cultcache.jsonl b/state/ghostlight-state.cultcache.jsonl index 92c3222..abf8bb0 100644 --- a/state/ghostlight-state.cultcache.jsonl +++ b/state/ghostlight-state.cultcache.jsonl @@ -244,3 +244,4 @@ {"key": "2026-08-31T231008Z0000-0242-6fc6f37903e6", "payload": "eyJub3RlIjogIlJ1biAxMDAgcHJvdmVkIHRoZSBpbmRpdmlkdWF0aW9uIG1vZGVsIGJvdW5kYXJ5IHN0aWxsIGV4cG9zZWQgZGV0ZXJtaW5pc3RpYyBmaWVsZHMgaXQgZGlkIG5vdCBvd246IHRoZSBzb2xlIHJldHJpZWQgVmVpbCBkaXNwYXRjaCByZXBlYXRlZGx5IGVtaXR0ZWQgbnVsbCBsYXN0L3JlbGV2YW5jZSByZXZpc2lvbnMgd2hpbGUgODAgcm91bmQtMTQgcHJvcG9zYWxzIHJlbWFpbmVkIGZyb3plbi4gV29ybGRDb21wbGV4aXR5TWVtYmVyRHJhZnQgbm93IGNvbnRhaW5zIG9ubHkgc2VtYW50aWMgbWVtYmVyIGNvbnRlbnQ7IFdvcmxkQ29tcGxleGl0eVRvb2wgY29uc3RydWN0cyBzY2hlbWEsIGV4YWN0IHBhcmVudC92ZXJzaW9uL2xvY2F0aW9uLCBtYXRlcmlhbGl6YXRpb24gc3RhdGUsIGFuZCByZWxldmFuY2UgcmV2aXNpb25zIGJlZm9yZSB0aGUgdW5jaGFuZ2VkIHZhbGlkYXRvci9rZXJuZWwgcGF0aC4gVHdvIGZvY3VzZWQgdGVzdHMsIDQ5OSBsaWJyYXJ5IHRlc3RzLCAxOCBzdHJhdGVnaWMtc21va2UgdGVzdHMsIGFuZCBhbGwtdGFyZ2V0IGNoZWNrIHBhc3MuIiwgInN0YXR1cyI6ICJvayIsICJ0cyI6ICIyMDI2LTA4LTMxVDIzOjEwOjA4KzAwOjAwIiwgInR5cGUiOiAiY29ycmVjdGlvbiJ9", "stored_at": "2026-08-31T23:10:08.143006+00:00", "type": "ghostlight.evidence_record.v0"} {"key": "2026-08-31T232851Z0000-0243-f35fd2b99a9a", "payload": "eyJub3RlIjogIlJ1bnMgMTAxIGFuZCAxMDMgcHJvdmVkIGEgc3RydWN0dXJhbCBjb21wYWN0aW9uIGNvbnRyYWN0IG1pc21hdGNoOiB0aGUgY2hlY2twb2ludC90b29sIHJlcXVpcmVkIGVhY2ggdW5yZXNvbHZlZCBsZWFkIHRvIGNvbnRhaW4gMS4uNjAwIGNoYXJhY3RlcnMsIGJ1dCB0aGUgZ2VuZXJhdGVkIGFjdGlvbiBzY2hlbWEgYm91bmRlZCBvbmx5IHRoZSBhcnJheSBjb3VudCwgYW5kIHRoZSB0b29sIHJldHVybmVkIG9uZSBvcGFxdWUgY29tYmluZWQgZmluZGluZy4gUm91bmQxNyByZXRhaW5zIDUyIGNvbW1pdHMgYW5kIDIzIGNvbXBsZXRlZCBsb2NhbCBzdW1tYXJpZXMuIFRoZSBleGlzdGluZyBjb21wYWN0aW9uIG93bmVyIG5vdyBwdWJsaXNoZXMgaWRlbnRpY2FsIHBlci1pdGVtIHNjaGVtYSBib3VuZHMgYW5kIGNvbXBsZXRlIGluZGV4ZWQgZmluZGluZ3M7IGZvY3VzZWQgcmVncmVzc2lvbiwgNTAwIGxpYnJhcnkgdGVzdHMsIDE4IHN0cmF0ZWdpYy1zbW9rZSB0ZXN0cywgYW5kIGFsbC10YXJnZXQgY2hlY2sgcGFzcy4iLCAic3RhdHVzIjogIm9rIiwgInRzIjogIjIwMjYtMDgtMzFUMjM6Mjg6NTErMDA6MDAiLCAidHlwZSI6ICJjb3JyZWN0aW9uIn0=", "stored_at": "2026-08-31T23:28:51.509927+00:00", "type": "ghostlight.evidence_record.v0"} {"key": "2026-08-31T234109Z0000-0244-ed94fa1db253", "payload": "eyJub3RlIjogIlJ1biAxMDQgcm91bmQxOSBleHBvc2VkIGEgZnJvemVuLWJhdGNoIGR1cGxpY2F0ZSBwZXJzb246IG9yZGluYWxzMTI3Mi8xMjg5IHByb3Bvc2VkIHRoZSBleGFjdCBzYW1lIGNhbm9uaWNhbCBsb2NhbCBJRCBhbmQgbmFtZSBUYXJpbiBWZWwgZnJvbSBhZGphY2VudCBBdmVsdW5lIGNhcnJpZXIgcG9wdWxhdGlvbnMgd2l0aCBuZWFyLWlkZW50aWNhbCBjb250ZW50OyBzZXJpYWwgYWRtaXNzaW9uIGZhaWxlZCBhZnRlciBjb21taXRzIHRocm91Z2gxMjg4LiBUaGUgcm91bmQgb3duZXIgbm93IGNhbm9uaWNhbGl6ZXMgZXhhY3QgbG9jYWwgSURzLCByZXRhaW5zIHRoZSBlYXJsaWVzdCBkaXNwYXRjaCwgcHVibGlzaGVzIGFuIGltbXV0YWJsZSBzdXBlcnNlc3Npb24gY2hlY2twb2ludCBmb3IgbGF0ZXIgZHVwbGljYXRlcywgZXhjbHVkZXMgdGhlbSBmcm9tIGNvbW1pdC9zZXNzaW9uIGpvdXJuYWxzLCBhbmQgbGV0cyBkZW1hbmQgcmVtZWFzdXJlLiBFeGlzdGluZy13b3JsZCBJRCByZWplY3Rpb24gcmVtYWlucyB3aXRoIHRoZSBzaGFyZWQgdmFsaWRhdG9yLiA1MDAgbGlicmFyeSB0ZXN0cywgMTkgc3RyYXRlZ2ljLXNtb2tlIHRlc3RzLCBhbmQgYWxsLXRhcmdldCBjaGVjayBwYXNzLiIsICJzdGF0dXMiOiAib2siLCAidHMiOiAiMjAyNi0wOC0zMVQyMzo0MTowOSswMDowMCIsICJ0eXBlIjogImNvcnJlY3Rpb24ifQ==", "stored_at": "2026-08-31T23:41:09.829172+00:00", "type": "ghostlight.evidence_record.v0"} +{"key": "2026-09-01T012925Z0000-0245-cf212b15c07c", "payload": "eyJub3RlIjogIkltbXV0YWJsZSBSdW4gMTA5IHByb3ZlZCBOZW1lc2lzIHBhZ2luZyBhbmQgdGhlIDI0MC1jZWxsIGRpc3BhdGNoIGF0IDIsNDAwIHN1YmplY3RzLCBiYW5rZWQgMjI2IG9mIDI0MCBjZWxsIHRlcm1pbmFscywgdGhlbiBpc29sYXRlZCAxNCBjb25uZWN0b3ItZnJhbWUgZmFpbHVyZXM6IHNpeCBQcm9qZWN0b3IgZnJhbWVzIHNlcmlhbGl6ZWQgaW5hY3RpdmUgY29uc3RpdHVlbnQgZG9zc2llcnMgYW5kIGVpZ2h0IEludGVycHJldGVyIGZyYW1lcyBzZXJpYWxpemVkIHVuYm91bmRlZCBhY3Rpdml0eS10YXJnZXQgcGVybWlzc2lvbnMuIEJvdW5kZWQgZGVjaXNpb24tb3duZXIgcHJvamVjdGlvbiBwbHVzIEludGVycHJldGVyIHRhcmdldCBpbnNwZWN0aW9uIHByZXNlcnZlcyBmdWxsIHByaXZhdGUgYXV0aG9yaXR5IHdoaWxlIGtlZXBpbmcgbW9kZWwgZnJhbWVzIGJvdW5kZWQ7IDUwNSBsaWJyYXJ5IHRlc3RzIGFuZCAyMCBzdHJhdGVnaWMtc21va2UgdGVzdHMgcGFzcy4iLCAic3RhdHVzIjogIm9rIiwgInRzIjogIjIwMjYtMDktMDFUMDE6Mjk6MjUrMDA6MDAiLCAidHlwZSI6ICJ2ZXJpZmljYXRpb24ifQ==", "stored_at": "2026-09-01T01:29:25.894858+00:00", "type": "ghostlight.evidence_record.v0"} diff --git a/state/map.yaml b/state/map.yaml index 741e56a..8720e4d 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -329,8 +329,8 @@ current_status: provider_acceptance_checkpoint: Exact pushed commit `139b8a6` completed Run 60 under `character-newsroom.v10` without world or per-wave replay. Its acceptance witness and terminal status bind the completed page and exact resume lineage. The independently dispatched blind reviewer, shown only the reader-facing fantasy newspaper, read it as a real left-populist civic broadsheet and did not immediately identify procedural generation, so the narrative reverse-Turing criterion passes. The review's substantive criticism is that the three stories feel like three rewrites of one briefing; accepted Run 60 remains immutable. The run used 96,902 prompt tokens and reported zero cache hits. Its delivered typeset artifact is `output/pdf/canopy-ledger-run-60-front-page.pdf`; frozen-source and visual verification remain accepted. Exact pushed `bc2ca39` then produced frozen-fact newsroom-only Run 61 Issues 13, 15, 16, 17, and 18 without world or wave model replay. Provider output proves `special_issue` convergence in Issues 13 and 15, independent `general_issue` beats in Issues 16 through 18, and assignment-selector cache hits. Blind review of Issues 13, 15, 16, and 17 passes both distinct recurring voices and issue-shape diversity, recognizing Ossan, Mera, Corin, and Lysa's biases plus modest cross-issue evolution. Run 62 exercised v12 without world or wave replay and failed only because two lead articles proposed invalid datelines; recurring reporter calls still recorded zero cache hits. Run 63 used exact pushed v13 `a52d57b`; Issues 17 and 18 completed before root stopped the final weekly recomposition, both repeated bylines again recorded zero cache hits, and live services remained unchanged. Preserve Runs 61 through 63 as stopped or completed immutable evidence. Copy defects remain later deadline-correction work rather than a reason to reopen accepted issue artifacts. implementation_checkpoint: Exact pushed v13 `a52d57b` keeps one stable reporter action schema, places a common stable house charter before recurring identity and dynamic assignment, and projects deterministic `allowed_datelines` from cited records while retaining copy-desk factual authority and deterministic cited-place admission; 480 library tests and the all-target check pass. Run 63 falsifies reporter-cache pursuit as a useful acceptance target for sporadic independent filings. The proposed v14 cache-floor prompt growth was removed. Append-only agent contexts remain the natural cache beneficiary; accepted Run 60 remains immutable. The strategic cut extracts one shared `validate_strategic_individuation_proposals` owner used by resolution validation and by the scheduler before it returns an optional proposal. An invalid optional identity emits a semantic-invalid receipt and produces no individuation instead of aborting the recoverable wave. Focused valid and invalid regressions pass, and exact-source Run 68 exercises the corrected provider path through twelve committed waves; no further validator implementation is pending. The newsroom-only cut adds a navigation-only directory of completed public changes grouped by period and exact source timestamps to new v14 audit citations. The directory derives context from canonical records and gains no fact or world authority. Legacy empty source times are omitted from both rendering and serialization, preserving inherited v13 receipt bytes and digests; new v14 nonempty source times remain rendered and serialized. Exact-source Run 73 proves that compatibility path and final recomposition mechanically. Current source gives the assignment editor bounded full-ledger queries and explicitly asks it to search sideways for opposition and countermoves; Run 73 falsifies the stronger assumption that this access and instruction alone guarantee balanced selection. Exact pushed v15 source `b6787d909bd79b68ed3a4d143b7f6dd438e00fa2` binds accountability, opposition, and counter-narrative pitches to two distinct named parties and selected citations through a typed conflict axis, and preserves canonical ledger order. Run 74 falsifies the stronger v15 assumption: its lone accountability pitch used the same transfer citation for cultivators and a passive Tribunal, while four of five pitches avoided the adversarial axis through lead or consequence functions. Two cited names did not prove two moves. Exact pushed v16 `86fe6ed89d51da26b65ab6a031608075840b1527` binds each adversarial axis to two distinct cited public moves or positions and rejects one record presented as both sides; it adds no event writer, judge, process, dependency, or persistent authority. Formatting, 27 focused newspaper tests, 484 full library tests, and 16 strategic-smoke tests pass. full_world_acceptance_checkpoint: Run 68 at exact source `69c811a2669fc38f19f9199bf57088dc250bfc3e` is mechanically complete at world revision 27 with 12/12 strategic waves, 240 events, 514 public news rows, and a final newspaper. Independent editorial and grounding reviews both fail. Earlier per-wave papers prove the immutable record already contains dramatic completed facts, including an organ breaching the rail capital, a dead pump, and families crossing borders; the final paper failed to make that committed change legible. Preserve Run 68 unchanged. Run 69 at source `84b31e57a462710eb939c4559e0437eb5d6a7a28` failed before any model call when immutable recomposition verification encountered an empty rendered `Source time:` line in inherited v13 per-wave issues. Run 70 at source `ec45952b843e102dc2ebddda3a3bbb4058687175` also failed before any model call because v14 serialization changed the legacy recomposition digest. Exact-source Run 73 `b827d897b9579c2091a67c363d35b8695288ba2f` is terminal complete at `/var/lib/gamecult/ghostlight-dungeon/acceptance/full-world-delvehold-b827d89-73`: world revision 27, 12/12 waves, 240 events, 514 news rows, inherited artifacts byte-identical to Run 68, manifest valid, and archive SHA-256 `090d05f304c8f3d33c9dccd728bffbf3e5dc2969a7aeeffc2f9d7bafe701ac8a`. Exact-source Run 74 `b6787d909bd79b68ed3a4d143b7f6dd438e00fa2` is mechanically complete at `/var/lib/gamecult/ghostlight-dungeon/acceptance/full-world-delvehold-b6787d9-74`: revision 27, 12/12 waves, 240 events, 514 news rows, Run 68 inherited artifacts byte-identical, manifest valid, unchanged live services, and archive SHA-256 `957b11a5d584d27d23bf4d50ed1fbc8d067faf67953b03f80705bfb9587c496d`. Independent editorial review fails: the page has a coherent throughline and sharp prose but no sustained adversarial narrative; opposing decision-makers, motives, responses, and countermoves are largely absent, scandal and public reaction are missing, lived costs remain abstract, and the result reads as polished minutes with conclusions. Its post-press correction notice has two items: `The corridor had begun closing hours earlier` overstates a source that says the prolonged oath-route closure was diverting arrivals six hours before the breach; `no relief or assignment of liability has followed` must narrow to `no tribunal remedy is recorded yet`. Run 75 launched at exact source `86fe6ed89d51da26b65ab6a031608075840b1527` under the superseded 240-cell-only acceptance shape and was stopped cleanly at the operator's scale correction. Preserve `/var/lib/gamecult/ghostlight-dungeon/acceptance/full-world-delvehold-86fe6ed-75` as stopped non-acceptance evidence: preflight, world-region and titled-elaboration checkpoints, a wave-one cell terminal checkpoint, partial pulses 01/02, and two rejected pulse receipts exist. `ghostlight-full-world-run75.service` invocation `86bfe6a3ef504a7d99f579b3d67710ab` is inactive/dead after explicit stop; live services remain unchanged. Do not claim terminal success or failure. Preserve Runs 64 through 75. Mechanical completion of the earlier world is proven, but final newspaper acceptance under the corrected scale target is not. - scale_checkpoint: The active 240-cell entitlement is an attention budget, not the world-size claim. Multiresolution Gestalts compress unresolved population; repeated admitted elaboration fissions them and promotes causally meaningful individuals and institutions, with `WorldElaborationDemand` deriving bounded mutation pressure and canonical remeasurement owning completion. Pushed source `8dd3147` keeps compaction journals structurally compact. Immutable Run 108 at that exact source reached exactly 2,400 actionable subjects at complexity round 43. Before strategic wave 1, clock binding advanced the campaign to revision 1551 and resolution epoch 1545. Wave 1 then produced five rejected pulses and terminally failed; every Nemesis attempt failed before reasoning with `connector frame exceeded its bound`, so no simulation cell committed and fictional time did not advance. Source inspection locates multiplicative projection in `propose_causal_follow_through`: instructions serialize the complete responder view catalog, then the initial 24 public anchors plus every current pressure and clock anchor each embed their full eligible responder views again. The causal tool already privately owns exact full eligibility, discovered anchors, and paged world and subject history. The next cut must expose bounded initial anchor summaries, page current anchors, and page eligible responders only for inspected anchors while retaining complete eligibility for deterministic tool admission. Resolution remains cover owner and WorldKernel remains commit owner. Exact resource/member assignment, compact individuation ownership, proposal supersession, civic fission rebinding, evolved-checkpoint proof, bounded lineage lookup, parallel session compaction, deterministic child-ID reconciliation, and materialized-member affiliation transfer remain preserved. + scale_checkpoint: The active 240-cell entitlement is an attention budget, not the world-size claim. Multiresolution Gestalts compress unresolved population; repeated admitted elaboration fissions them and promotes causally meaningful individuals and institutions, with `WorldElaborationDemand` deriving bounded mutation pressure and canonical remeasurement owning completion. Pushed source `8dd3147` keeps compaction journals structurally compact. Immutable Run 108 reached exactly 2,400 actionable subjects at complexity round 43; clock binding advanced the campaign to revision 1551 and resolution epoch 1545. Pushed source `b61e762` bounds Nemesis through anchor-summary and responder-dossier paging while retaining complete deterministic eligibility privately. Immutable Run 109 proved Nemesis and the 240-cell dispatch at that scale: wave-1 pulse one completed 214 cells, bounded retries raised the durable partial checkpoint to 226, and the fifth pulse terminated with exactly 14 frame-bound failures. No fictional wave committed and time did not advance. Six failures belonged to Projector frames that serialized hundreds of inactive constituent dossiers; eight belonged to Interpreter frames whose exact schemas serialized one active owner's unbounded activity-target catalog, including cells with only two subjects. The current cut makes model context a bounded query projection: Projector receives only exact decision-owner dossiers plus at most 24 deterministically relevant targets, while the Interpreter's existing agent workbench can inspect up to 12 exact name/ID matches before its dynamic schema expands. Full membership and permissions remain private deterministic state; effect verification receives only targets cited by the candidate action; Resolution remains cover owner and WorldKernel remains commit owner. Run 109 is immutable at `/var/lib/gamecult/ghostlight-dungeon/acceptance/full-world-delvehold-b61e762-109`; its final wave-1 partial checkpoint contains 226 completed and 14 failed cells with SHA-256 `f502ad560e0f23db226c326072e5e8a6d0ed9975f0e63bc67606d7d7cb9b2c27`. Exact resource/member assignment, compact individuation ownership, proposal supersession, civic fission rebinding, evolved-checkpoint proof, bounded lineage lookup, parallel session compaction, deterministic child-ID reconciliation, and materialized-member affiliation transfer remain preserved. presentation_checkpoint: The historical press-layout and seed-ownership split are complete. `notes/historical-newspaper-layout-grammar.md` remains the durable grammar. Manifest v2 at `output/tex/canopy-ledger-run-61-style-1847-flow-723.manifest.toml` independently binds `style_seed=1847`, `flow_seed=723`, frozen source SHA-256 `8f195140f52a40ba705d6a66987146a1500ae89668e5d82b01d1792eb3afb882`, and the final artifact `output/pdf/canopy-ledger-run-61-style-1847-flow-723.pdf`, SHA-256 `a457f4b1702a7a52480e42fce0039f0dbe677ff32958e98f1c412b346237190c`. An identical seed pair produces byte-identical TeX and PDF; changing only flow preserves the exact style section while materially changing a plausible layout, and changing only style preserves flow topology. Cut-first and cut-midstory templates pass visual inspection; the failed split-card template is deleted. Frozen copy/assets, actual PDF `/BM /Multiply`, and economic-layer advertising authority remain invariant. public_reference_checkpoint: Public architecture reference is complete. Canonical Ghostlight commit `15ea0f0` owns five pages under `docs/public-architecture` covering overview, authority, pipelines, 43 named production stages, and eight titled elaborators. Site commit `ea0b026` publishes exact projections under `F:\Projects\GhostlightDungeon-site\content\Documentation`, adds the sync/check script, explorer and table-of-contents navigation, scoped documentation layout, and homepage Architecture link. Local Quartz build and visual browser QA pass. GitHub Pages workflow `33408004480` succeeded. `https://ghostlight.gamecult.org/Documentation/` and the extensionless `Documentation/Agents-and-model-stages` route are live; the stage page exposes seven tables and 51 rows. Ghostlight remains technical authority; the site repository owns public composition and deployment. - next_action: Preserve Runs 75 through 108 and the exact 2,400-subject round-43 world. Map and cut Nemesis's multiplicative causal projection: keep a bounded initial anchor-summary viewport, add paged current-anchor inspection and paged eligible-responder inspection for one selected anchor, and retain the complete responder/anchor eligibility catalog privately inside the deterministic tool. Do not raise the connector bound, truncate canonical eligibility, grant the model cover authority, or replay elaboration and clock binding. Add focused size, paging, exact-eligibility, and no-authority regressions; review, gate, commit, and push. Then clone immutable Run 108 and resume strategic wave 1 from campaign revision 1551/resolution epoch 1545 without replaying any complexity round or advancing fiction on failed pulses. Require ten strategic waves, publications, archive hashes, and unchanged live-service identities. Preserve compact elaborator continuity, canonical-ID identity ownership, deterministic proposal supersession, Resolution cover ownership, WorldKernel commit authority, the completed public reference, the two-seed press checkpoint, and the economic-layer advertising boundary. + next_action: Preserve Runs 75 through 109 and the exact 2,400-subject round-43 world. Finish review and verification of bounded decision-owner projection and Interpreter target inspection, commit and push, then clone Run 109's exact 226-cell wave-1 checkpoint into a new immutable root. Resume only its 14 failed cells; do not replay complexity rounds, clock binding, Nemesis, or completed cell membranes. Continue through ten committed strategic waves, per-wave publications and final digest, exact provenance and archive/hash checks, and unchanged live-service identities. Do not raise the connector bound, truncate canonical membership or permissions, grant query projections authority, or start another persistence optimization without measured evidence. Preserve compact elaborator continuity, canonical-ID identity ownership, deterministic proposal supersession, Resolution cover ownership, WorldKernel commit authority, the completed public reference, the two-seed press checkpoint, and the economic-layer advertising boundary. gestalt_population: Gestalt Personas own shared population baselines; persistent member deltas own named individuality; WorldKernel materialization and approval-gated fission preserve lineage and identity. Materialisation/folding changes only representation and the exact member delta, advances resolution epoch, and retires the old cover; it has no Gestalt-wide effect authority. Selected actionable member exceptions are mandatory Projector perspective owners, so aggregation cannot silently erase their decision. Presence projection is limited to nearby active leaves and exact-location dormant members, includes reciprocal player relationship evidence, and may propose an earned cast without committing it. Simulation cells are derived covers only.