Attribute inaction inside gestalt arenas
This commit is contained in:
parent
6253f9f42e
commit
97e05c571e
10 changed files with 317 additions and 49 deletions
|
|
@ -110,9 +110,9 @@ async fn main() -> anyhow::Result<()> {
|
|||
.filter(|appraisal| {
|
||||
appraisal.actions.is_empty()
|
||||
&& appraisal
|
||||
.inaction_reason
|
||||
.as_deref()
|
||||
.is_some_and(|reason| !reason.trim().is_empty())
|
||||
.inactions
|
||||
.iter()
|
||||
.any(|inaction| !inaction.reason.trim().is_empty())
|
||||
})
|
||||
.count();
|
||||
for stage in &output.stages {
|
||||
|
|
|
|||
|
|
@ -489,6 +489,14 @@ pub struct CellActionProposal {
|
|||
pub effect: StrategicCellEffect,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CellInaction {
|
||||
pub subject_id: String,
|
||||
#[schemars(length(min = 1, max = 240))]
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct CellAppraisal {
|
||||
pub schema: String,
|
||||
|
|
@ -497,7 +505,7 @@ pub struct CellAppraisal {
|
|||
pub resolution_epoch: u64,
|
||||
pub considered_subject_ids: BTreeSet<String>,
|
||||
pub actions: Vec<CellActionProposal>,
|
||||
pub inaction_reason: Option<String>,
|
||||
pub inactions: Vec<CellInaction>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
|
|
|
|||
|
|
@ -4165,7 +4165,16 @@ mod tests {
|
|||
resolution_epoch: campaign.resolution_policy.resolution_epoch,
|
||||
considered_subject_ids: cell.subject_ids.clone(),
|
||||
actions: vec![],
|
||||
inaction_reason: Some("No justified move.".into()),
|
||||
inactions: cell
|
||||
.subject_ids
|
||||
.iter()
|
||||
.next()
|
||||
.map(|subject_id| crate::domain::CellInaction {
|
||||
subject_id: subject_id.clone(),
|
||||
reason: "No justified move.".into(),
|
||||
})
|
||||
.into_iter()
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
cover,
|
||||
|
|
@ -4370,7 +4379,7 @@ mod tests {
|
|||
location_ids: vec![],
|
||||
},
|
||||
}];
|
||||
wave.appraisals[0].inaction_reason = None;
|
||||
wave.appraisals[0].inactions.clear();
|
||||
let mut rejected_verifier = resolution_stage(
|
||||
&wave.cover.cells[0].id,
|
||||
&persisted,
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ pub struct CellTerminalBundle {
|
|||
#[serde(deny_unknown_fields)]
|
||||
struct CellAppraisalProposal {
|
||||
actions: Vec<CellActionCandidate>,
|
||||
inaction_reason: Option<String>,
|
||||
inactions: Vec<crate::domain::CellInaction>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
|
|
@ -236,7 +236,7 @@ const CELL_PROJECTION_OUTPUT_CONTRACT: &str = r#"{
|
|||
|
||||
const CELL_APPRAISAL_OUTPUT_CONTRACT: &str = r#"{
|
||||
"type":"object",
|
||||
"required":["actions","inaction_reason"],
|
||||
"required":["actions","inactions"],
|
||||
"properties":{
|
||||
"actions":{"type":"array","items":{"type":"object","required":["subject_id","intent","intended_effect","priority","state_references","public_channels","effect"],"properties":{
|
||||
"subject_id":{"type":"string"},"intent":{"type":"string"},"intended_effect":{"type":"string"},"priority":{"type":"integer"},
|
||||
|
|
@ -251,7 +251,7 @@ const CELL_APPRAISAL_OUTPUT_CONTRACT: &str = r#"{
|
|||
{"type":"object","required":["type","destination_gestalt_id"],"properties":{"type":{"const":"member_migration"},"destination_gestalt_id":{"type":"string"}}}
|
||||
]}
|
||||
}}},
|
||||
"inaction_reason":{"type":["string","null"]}
|
||||
"inactions":{"type":"array","items":{"type":"object","required":["subject_id","reason"],"properties":{"subject_id":{"type":"string"},"reason":{"type":"string","minLength":1,"maxLength":240}}}}
|
||||
}
|
||||
}"#;
|
||||
|
||||
|
|
@ -823,7 +823,7 @@ impl CellProjectionEngine {
|
|||
"target_subject_ids and location_ids must come from that exact subject's permissions. 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. Never substitute a containing population, related institution, or merely permitted ID for an unnamed role. ",
|
||||
"Write intended_effect as the attempted act, never its hoped-for outcome or target response. Institution posture must be a specific materially new commitment or withholding of at most 240 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 and public channels. ",
|
||||
"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. Use an empty actions array plus a concrete inaction_reason when nobody acts."
|
||||
"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. Record every voiced subject that explicitly holds, waits, or merely continues already_committed_posture in inactions with its exact subject_id and a concrete reason. 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."
|
||||
),
|
||||
slice.max_actions
|
||||
);
|
||||
|
|
@ -1013,7 +1013,7 @@ fn append_cell_correction(
|
|||
rejected_appraisal: &str,
|
||||
) {
|
||||
request.lived_stream.push_str(&format!(
|
||||
"\n\nCORRECTION TASK—THE PREVIOUS APPRAISAL WAS REJECTED.\nREJECTION: {error}\nPREVIOUS_REJECTED_APPRAISAL:\n{rejected_appraisal}\nReturn one corrected complete appraisal against the same snapshot, lived stream, Persona turn, and exact permission context. A rejected action is forbidden unchanged: remove it or replace it with a different, faithful, permitted typed consequence. Do not repeat its subject, intended_effect, and typed effect together. Every retained action must still carry a valid non-empty typed transition under the original contract. If an institution merely continues or restates already_committed_posture, omit that no-op action; it is holding steady. If the Persona chose travel but that subject has no exact permitted destination in reachable_destination_ids or migration_destinations, no movement transition is available: remove that action and use explicit inaction. If an attempted preparation, inspection, request, or deliberation has no permitted typed consequence, remove it and use explicit inaction; never emit an empty transition or upgrade consideration into a completed consequence."
|
||||
"\n\nCORRECTION TASK—THE PREVIOUS APPRAISAL WAS REJECTED.\nREJECTION: {error}\nPREVIOUS_REJECTED_APPRAISAL:\n{rejected_appraisal}\nReturn one corrected complete appraisal against the same snapshot, lived stream, Persona turn, and exact permission context. A rejected action is forbidden unchanged: remove it or replace it with a different, faithful, permitted typed consequence. Do not repeat its subject, intended_effect, and typed effect together. Every retained action must still carry a valid non-empty typed transition under the original contract. If an institution merely continues or restates already_committed_posture, move that exact subject to inactions; it is holding steady. If the Persona chose travel but that subject has no exact permitted destination in reachable_destination_ids or migration_destinations, no movement transition is available: remove that action and record attributed inaction. If an attempted preparation, inspection, request, or deliberation has no permitted typed consequence, remove it and record attributed inaction; never emit an empty transition or upgrade consideration into a completed consequence."
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -1179,7 +1179,7 @@ fn bind_cell_appraisal(
|
|||
.map(|subject| subject.subject_id.clone())
|
||||
.collect(),
|
||||
actions,
|
||||
inaction_reason: proposal.inaction_reason,
|
||||
inactions: proposal.inactions,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1209,16 +1209,41 @@ fn validate_cell_appraisal(
|
|||
slice.max_actions
|
||||
));
|
||||
}
|
||||
if appraisal.actions.is_empty()
|
||||
&& appraisal
|
||||
.inaction_reason
|
||||
.as_deref()
|
||||
.is_none_or(|reason| reason.trim().is_empty())
|
||||
{
|
||||
if appraisal.actions.is_empty() && appraisal.inactions.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"an appraisal with no actions requires one concrete non-empty inaction_reason"
|
||||
"an appraisal with no actions requires one exact attributed inaction"
|
||||
));
|
||||
}
|
||||
let permitted_subject_ids = slice
|
||||
.constituents
|
||||
.iter()
|
||||
.map(|subject| subject.subject_id.as_str())
|
||||
.chain(
|
||||
slice
|
||||
.member_exceptions
|
||||
.iter()
|
||||
.map(|member| member.subject_id.as_str()),
|
||||
)
|
||||
.collect::<BTreeSet<_>>();
|
||||
let action_subject_ids = appraisal
|
||||
.actions
|
||||
.iter()
|
||||
.map(|action| action.subject_id.as_str())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let mut inaction_subject_ids = BTreeSet::new();
|
||||
for inaction in &appraisal.inactions {
|
||||
if inaction.reason.trim().is_empty()
|
||||
|| inaction.reason.len() > 240
|
||||
|| !permitted_subject_ids.contains(inaction.subject_id.as_str())
|
||||
|| action_subject_ids.contains(inaction.subject_id.as_str())
|
||||
|| !inaction_subject_ids.insert(inaction.subject_id.as_str())
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"inaction for subject {} is empty, duplicate, outside the cell, or conflicts with an action",
|
||||
inaction.subject_id
|
||||
));
|
||||
}
|
||||
}
|
||||
for action in &appraisal.actions {
|
||||
if action.intent.trim().is_empty() || action.intended_effect.trim().is_empty() {
|
||||
return Err(anyhow!(
|
||||
|
|
@ -1512,6 +1537,11 @@ fn constrain_cell_proposal_schema(
|
|||
.and_then(serde_json::Value::as_object_mut)
|
||||
.ok_or_else(|| anyhow!("cell appraisal schema has no action array"))?;
|
||||
actions.insert("maxItems".into(), slice.max_actions.into());
|
||||
let inactions = properties
|
||||
.get_mut("inactions")
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
.ok_or_else(|| anyhow!("cell appraisal schema has no inaction array"))?;
|
||||
inactions.insert("maxItems".into(), slice.max_actions.into());
|
||||
let proposal = schema
|
||||
.pointer_mut("/$defs/CellActionCandidate/properties")
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
|
|
@ -1529,12 +1559,20 @@ fn constrain_cell_proposal_schema(
|
|||
.collect::<Vec<_>>();
|
||||
proposal.insert(
|
||||
"subject_id".into(),
|
||||
serde_json::json!({"type":"string","enum":subject_ids}),
|
||||
serde_json::json!({"type":"string","enum":subject_ids.clone()}),
|
||||
);
|
||||
proposal.insert(
|
||||
"priority".into(),
|
||||
serde_json::json!({"type":"integer","minimum":0,"maximum":100}),
|
||||
);
|
||||
let inaction = schema
|
||||
.pointer_mut("/$defs/CellInaction/properties")
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
.ok_or_else(|| anyhow!("cell appraisal schema has no inaction properties"))?;
|
||||
inaction.insert(
|
||||
"subject_id".into(),
|
||||
serde_json::json!({"type":"string","enum":subject_ids}),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -1643,7 +1681,7 @@ mod tests {
|
|||
"public_channels":["public bulletin"],
|
||||
"effect":{"type":"actor_move","destination_id":"forum"}
|
||||
}],
|
||||
"inaction_reason":null
|
||||
"inactions":[]
|
||||
})
|
||||
.to_string());
|
||||
}
|
||||
|
|
@ -1663,7 +1701,7 @@ mod tests {
|
|||
"public_channels":["public bulletin"],
|
||||
"effect":{"type":"institution","posture":"published a bounded position","location_ids":["forum"]}
|
||||
}],
|
||||
"inaction_reason":null
|
||||
"inactions":[]
|
||||
}).to_string())
|
||||
}
|
||||
"cell_effect_verifier" => {
|
||||
|
|
@ -1811,7 +1849,7 @@ mod tests {
|
|||
"location_ids":["forum"]
|
||||
}
|
||||
}],
|
||||
"inaction_reason":null
|
||||
"inactions":[]
|
||||
}).to_string())
|
||||
}
|
||||
"cell_effect_verifier" => {
|
||||
|
|
@ -1879,21 +1917,47 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn empty_cell_appraisal_names_the_missing_inaction_reason() {
|
||||
fn empty_cell_appraisal_requires_exact_attributed_inaction() {
|
||||
let slice = fixture_cell_slice();
|
||||
let appraisal = bind_cell_appraisal(
|
||||
let mut appraisal = bind_cell_appraisal(
|
||||
&slice,
|
||||
CellAppraisalProposal {
|
||||
actions: vec![],
|
||||
inaction_reason: Some(" ".into()),
|
||||
inactions: vec![crate::domain::CellInaction {
|
||||
subject_id: "faction-06".into(),
|
||||
reason: "The institution deliberately holds its current position.".into(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
validate_cell_appraisal(&slice, &appraisal).unwrap();
|
||||
appraisal.inactions[0].reason = " ".into();
|
||||
let error = validate_cell_appraisal(&slice, &appraisal).unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("concrete non-empty inaction_reason")
|
||||
.contains("inaction for subject faction-06")
|
||||
);
|
||||
|
||||
appraisal.inactions[0].reason = "The institution holds.".into();
|
||||
appraisal.actions.push(crate::domain::CellActionProposal {
|
||||
subject_id: "faction-06".into(),
|
||||
intent: "publish a new position".into(),
|
||||
intended_effect: "adopt a bounded commitment".into(),
|
||||
priority: 50,
|
||||
state_references: vec!["institution:faction-06".into()],
|
||||
public_channels: vec!["public bulletin".into()],
|
||||
effect: StrategicCellEffect::Institution {
|
||||
institution_id: "faction-06".into(),
|
||||
posture: "publishing a bounded new commitment".into(),
|
||||
location_ids: vec!["forum".into()],
|
||||
},
|
||||
});
|
||||
assert!(
|
||||
validate_cell_appraisal(&slice, &appraisal)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("conflicts with an action")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -2009,7 +2073,7 @@ mod tests {
|
|||
pressure_resolutions: vec![],
|
||||
},
|
||||
}],
|
||||
inaction_reason: None,
|
||||
inactions: vec![],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
|
@ -2150,7 +2214,7 @@ mod tests {
|
|||
location_ids: vec!["forum".into()],
|
||||
},
|
||||
}],
|
||||
inaction_reason: None,
|
||||
inactions: vec![],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
|
@ -2258,7 +2322,7 @@ mod tests {
|
|||
.lived_stream
|
||||
.contains("preparation, inspection, request, or deliberation")
|
||||
);
|
||||
assert!(request.lived_stream.contains("use explicit inaction"));
|
||||
assert!(request.lived_stream.contains("record attributed inaction"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1614,18 +1614,32 @@ pub fn validate_and_resolve_wave(
|
|||
|| appraisal.world_revision != campaign.revision
|
||||
|| appraisal.resolution_epoch != campaign.resolution_policy.resolution_epoch
|
||||
|| appraisal.considered_subject_ids != cell.subject_ids
|
||||
|| (appraisal.actions.is_empty()
|
||||
&& appraisal
|
||||
.inaction_reason
|
||||
.as_deref()
|
||||
.is_none_or(|reason| reason.trim().is_empty()))
|
||||
|| (appraisal.actions.is_empty() && appraisal.inactions.is_empty())
|
||||
{
|
||||
return Err(anyhow!("cell appraisal is incomplete or stale"));
|
||||
}
|
||||
let quota = cell_action_limit(cell);
|
||||
if appraisal.actions.len() > quota {
|
||||
if appraisal.actions.len() > quota || appraisal.inactions.len() > quota {
|
||||
return Err(anyhow!("cell appraisal exceeds its action quota"));
|
||||
}
|
||||
let action_subject_ids = appraisal
|
||||
.actions
|
||||
.iter()
|
||||
.map(|proposal| proposal.subject_id.as_str())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let mut inaction_subject_ids = BTreeSet::new();
|
||||
for inaction in &appraisal.inactions {
|
||||
if inaction.reason.trim().is_empty()
|
||||
|| inaction.reason.len() > 240
|
||||
|| action_subject_ids.contains(inaction.subject_id.as_str())
|
||||
|| !inaction_subject_ids.insert(inaction.subject_id.as_str())
|
||||
|| !cell_contains_attributed_subject(campaign, cell, &inaction.subject_id)
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"cell appraisal contains an invalid attributed inaction"
|
||||
));
|
||||
}
|
||||
}
|
||||
for proposal in &appraisal.actions {
|
||||
validate_cell_proposal(campaign, cell, proposal)?;
|
||||
proposals.push(proposal.clone());
|
||||
|
|
@ -1907,6 +1921,23 @@ fn validate_cell_proposal(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn cell_contains_attributed_subject(
|
||||
campaign: &Campaign,
|
||||
cell: &SimulationCell,
|
||||
subject_id: &str,
|
||||
) -> bool {
|
||||
if subject_id == campaign.player_actor_id {
|
||||
return false;
|
||||
}
|
||||
if let Some(member_id) = subject_id.strip_prefix("member:") {
|
||||
return campaign
|
||||
.gestalt_members
|
||||
.get(member_id)
|
||||
.is_some_and(|member| cell.subject_ids.contains(&member.gestalt_id));
|
||||
}
|
||||
cell.subject_ids.contains(subject_id)
|
||||
}
|
||||
|
||||
pub fn substantive_text_change(current: &str, candidate: &str) -> bool {
|
||||
let current = current.trim();
|
||||
let candidate = candidate.trim();
|
||||
|
|
@ -3079,7 +3110,7 @@ pub(crate) mod tests {
|
|||
resolution_epoch: value.resolution_policy.resolution_epoch,
|
||||
considered_subject_ids: cell.subject_ids.clone(),
|
||||
actions: vec![proposal],
|
||||
inaction_reason: None,
|
||||
inactions: vec![],
|
||||
}],
|
||||
cover: cover.clone(),
|
||||
model_receipt_hashes: vec![],
|
||||
|
|
@ -3112,6 +3143,33 @@ pub(crate) mod tests {
|
|||
},
|
||||
};
|
||||
assert!(validate_and_resolve_wave(&value, &make_wave(borrowed_secret)).is_err());
|
||||
|
||||
let valid_action = CellActionProposal {
|
||||
subject_id: "faction-0000".into(),
|
||||
intent: "publish a bounded position".into(),
|
||||
intended_effect: "adopt a materially different posture".into(),
|
||||
priority: 50,
|
||||
state_references: vec![],
|
||||
public_channels: vec![],
|
||||
effect: StrategicCellEffect::Institution {
|
||||
institution_id: "faction-0000".into(),
|
||||
posture: "publishing a bounded position under the current pressure".into(),
|
||||
location_ids: vec![],
|
||||
},
|
||||
};
|
||||
let mut mixed = make_wave(valid_action.clone());
|
||||
mixed.appraisals[0].inactions = vec![CellInaction {
|
||||
subject_id: "faction-0001".into(),
|
||||
reason: "The rival deliberately holds its separate position.".into(),
|
||||
}];
|
||||
validate_and_resolve_wave(&value, &mixed).unwrap();
|
||||
|
||||
let mut contradictory = make_wave(valid_action);
|
||||
contradictory.appraisals[0].inactions = vec![CellInaction {
|
||||
subject_id: "faction-0000".into(),
|
||||
reason: "The same institution cannot also hold.".into(),
|
||||
}];
|
||||
assert!(validate_and_resolve_wave(&value, &contradictory).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -3225,7 +3283,7 @@ pub(crate) mod tests {
|
|||
resolution_epoch: value.resolution_policy.resolution_epoch,
|
||||
considered_subject_ids: cover.cells[0].subject_ids.clone(),
|
||||
actions: vec![proposal],
|
||||
inaction_reason: None,
|
||||
inactions: vec![],
|
||||
}],
|
||||
cover: cover.clone(),
|
||||
model_receipt_hashes: vec![],
|
||||
|
|
@ -3274,7 +3332,7 @@ pub(crate) mod tests {
|
|||
resolution_epoch: value.resolution_policy.resolution_epoch,
|
||||
considered_subject_ids: cover.cells[0].subject_ids.clone(),
|
||||
actions: vec![member_activity.clone(), proposal.clone()],
|
||||
inaction_reason: None,
|
||||
inactions: vec![],
|
||||
}],
|
||||
cover: cover.clone(),
|
||||
model_receipt_hashes: vec![],
|
||||
|
|
@ -3345,7 +3403,7 @@ pub(crate) mod tests {
|
|||
resolution_epoch: value.resolution_policy.resolution_epoch,
|
||||
considered_subject_ids: cover.cells[0].subject_ids.clone(),
|
||||
actions: vec![exact_rival_activity.clone(), lower_priority_pressure],
|
||||
inaction_reason: None,
|
||||
inactions: vec![],
|
||||
}],
|
||||
cover: cover.clone(),
|
||||
model_receipt_hashes: vec![],
|
||||
|
|
@ -3427,7 +3485,7 @@ pub(crate) mod tests {
|
|||
action("faction-0001", 100),
|
||||
action("faction-0002", 50),
|
||||
],
|
||||
inaction_reason: None,
|
||||
inactions: vec![],
|
||||
}],
|
||||
cover,
|
||||
model_receipt_hashes: vec![],
|
||||
|
|
|
|||
|
|
@ -833,11 +833,24 @@ mod tests {
|
|||
}
|
||||
"cell_persona" => Ok("Each constituent watches and deliberately holds.".into()),
|
||||
"cell_interpreter" if self.malformed_cell => Ok("not-json".into()),
|
||||
"cell_interpreter" => Ok(serde_json::json!({
|
||||
"actions": [],
|
||||
"inaction_reason": "No constituent has a justified move this horizon."
|
||||
})
|
||||
.to_string()),
|
||||
"cell_interpreter" => {
|
||||
let subject_id = request
|
||||
.output_schema
|
||||
.as_ref()
|
||||
.and_then(|schema| {
|
||||
schema.pointer("/$defs/CellInaction/properties/subject_id/enum/0")
|
||||
})
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.ok_or_else(|| anyhow!("fixture Interpreter lacks a bound subject"))?;
|
||||
Ok(serde_json::json!({
|
||||
"actions": [],
|
||||
"inactions": [{
|
||||
"subject_id":subject_id,
|
||||
"reason":"No justified move this horizon."
|
||||
}]
|
||||
})
|
||||
.to_string())
|
||||
}
|
||||
stage => Err(anyhow!("unexpected fixture stage {stage}")),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,14 @@ outside the player's active location. A direct internal command may preserve a
|
|||
named offscreen actor, but model-driven presence reconciliation cannot teleport
|
||||
one into view. Existing member deltas are preferred when their relationship,
|
||||
memory, obligation, or goal makes an ordinary encounter a meaningful callback.
|
||||
The player actor's reciprocal relationship to each nearby dormant member is
|
||||
projected beside that member's exact delta. Presence planning is scene casting,
|
||||
not roster cleanup: an earned local callback may surface without the player
|
||||
asking for the person, while an incompatible event or absence of durable
|
||||
callback evidence may still produce no promotion. The model only proposes the
|
||||
cast; `WorldKernel` validates exact member, gestalt, location, and versions
|
||||
before materialization. Live harnesses persist the private presence preflight
|
||||
even when their acceptance assertion fails.
|
||||
|
||||
## Canonical population migration
|
||||
|
||||
|
|
@ -273,6 +281,17 @@ Projector → Persona → Interpreter membrane:
|
|||
- Interpreters emit a typed appraisal and exact constituent effects;
|
||||
- malformed or semantically invalid output gets one same-snapshot correction.
|
||||
|
||||
An appraisal has two attributed lanes: `actions[]` for proposed transitions and
|
||||
`inactions[]` for exact subjects that deliberately hold, wait, or continue an
|
||||
already committed course. The old cell-wide inaction string is gone. This
|
||||
matters in a mixed arena: one institution can act while two rivals hold without
|
||||
the Interpreter manufacturing repeated posture transitions merely to preserve
|
||||
their separately voiced decisions. Inaction IDs use the same constituent or
|
||||
selected-member permission set, carry a bounded reason, cannot duplicate, and
|
||||
cannot also appear in the action lane. They are audit/fairness evidence and
|
||||
never mutate the world. A cell with no actions must still contain at least one
|
||||
attributed inaction.
|
||||
|
||||
An arena may contain simultaneous views from distant locations. The Projector
|
||||
names each subject's actual location and keeps those views as separate scenes;
|
||||
a graph relation or activity target grants permission to attempt contact, not
|
||||
|
|
@ -574,6 +593,44 @@ explicit aggregate `detail_focus_subject_id` and every singleton cell both
|
|||
receive direct resolution attention. A named focus is not required for a
|
||||
subject already receiving its own complete Persona pipeline.
|
||||
|
||||
The lease-preserving budget-4 profile committed all 23 requested sustained
|
||||
waves and recovered from 18 rejected provider pulses without partial mutation.
|
||||
It then failed its separate callback assertion because the presence planner
|
||||
returned an empty cast. This isolated a player-experience fault from the agency
|
||||
invariants: Mira remained the same dormant person in the correct destination,
|
||||
but continuity was not surfaced. The focused `gestalt-callback-6253f9f-1` run
|
||||
then passed: after one fresh 24-subject arena wave, the planner promoted
|
||||
`mira-venn` from `harbor-neighbors` without the player naming her, and the
|
||||
kernel rematerialized the same private and effective state. The full run took
|
||||
18.465 seconds. Its strategic membrane used 18,662 prompt and 1,096 completion
|
||||
tokens; the additional presence decision used 2,504 prompt and 78 completion
|
||||
tokens.
|
||||
|
||||
Institution posture is projected as `already_committed_posture`. It is durable
|
||||
state already in force, not a new pressure or decision. Projector, Persona, and
|
||||
Interpreter contracts all treat continuing or restating it as explicit
|
||||
inaction; only a materially different commitment may emit an institution
|
||||
posture transition. Local validation remains strict rather than silently
|
||||
dropping a no-op.
|
||||
|
||||
The 31-wave budget-8 / provider-parallelism-4 profile
|
||||
`gestalt-lease-b8p4-6253f9f-1` passed after 1,211.592 seconds. All 24 subjects
|
||||
received direct attention. Its sustained appraisals made 199 accepted proposals
|
||||
from 22 distinct subjects; consequence resolution committed 51 institution
|
||||
postures, 110 Gestalt activities, one population migration, and 31 named-member
|
||||
activities, plus the initial arena consequence. Fifteen rejected pulses left
|
||||
the world unchanged and later recovered. The same Mira callback passed after
|
||||
the full run.
|
||||
|
||||
The profile used 1,468,039 strategic prompt tokens and 165,725 completion
|
||||
tokens, reached a 60.69% cache-hit ratio, and spent 7,567 prompt tokens per
|
||||
committed consequence. Interpreter remained the largest stage at 737,148
|
||||
prompt tokens but reached 67.18% cache hits; Projector reached 70.03%. The
|
||||
presence decision added 2,606 prompt and 78 completion tokens. Live traces
|
||||
showed that the remaining repeated-posture failures occurred when several
|
||||
constituents in one arena held while others acted, motivating the attributed
|
||||
inaction lane rather than another prompt-only prohibition.
|
||||
|
||||
The run also exposes the next authority gap. Institution postures, population
|
||||
migration, clocks, and history changed canonically, but most Gestalt activities
|
||||
remain attributed attempts recorded as events. Over many ticks this can repeat
|
||||
|
|
|
|||
|
|
@ -277,6 +277,50 @@ cover, so `advance_detail_debt` correctly treated it as directly resolved while
|
|||
the harness counted only explicit aggregate focus labels. Acceptance now uses
|
||||
the canonical union of explicit focus plus singleton subjects.
|
||||
|
||||
The corrected lease-preserving budget-4 run committed all 23 sustained waves
|
||||
and recovered from 18 rejected pulses without partial world mutation. It failed
|
||||
only after those waves when automatic presence planning returned an empty cast
|
||||
for Mira's South Harbor callback. Mira's dormant identity, destination,
|
||||
relationships, memories, and effective state were intact. `dab6aae` made the
|
||||
planner's scene-casting objective explicit, projected the player's reciprocal
|
||||
relationship beside the exact member delta, and began persisting
|
||||
`presence-preflight.json` before assertions.
|
||||
|
||||
The first focused callback probe was blocked before casting by the dominant
|
||||
long-run failure: an institution repeatedly emitted its already committed
|
||||
posture as if it were a new action. `6253f9f` now projects that field as
|
||||
`already_committed_posture` and defines continuation as inaction throughout the
|
||||
Projector, Persona, Interpreter, and correction contexts. Validation still
|
||||
rejects the no-op rather than silently repairing it. The same commit adds
|
||||
`GHOSTLIGHT_LIVE_FIRE_PRESENCE_ONLY=1` for a baseline-backed focused acceptance
|
||||
run.
|
||||
|
||||
`gestalt-callback-6253f9f-1` passed in 18.465 seconds. A fresh 24-subject arena
|
||||
wave ran first; then the presence planner selected `mira-venn` from
|
||||
`harbor-neighbors` without the player naming her. `WorldKernel` rematerialized
|
||||
the same private and effective state at South Harbor. The strategic membrane
|
||||
used 18,662 prompt / 1,096 completion tokens; presence used another 2,504 / 78.
|
||||
|
||||
`gestalt-lease-b8p4-6253f9f-1` then passed 31 sustained budget-8 waves at
|
||||
provider parallelism 4 in 1,211.592 seconds. It covered all 24 subjects, made
|
||||
199 accepted proposals from 22 subjects, committed 193 sustained consequences
|
||||
plus the initial arena consequence, recovered 15 rejected pulses, preserved the
|
||||
player and Mira, and passed the same-person callback. Strategic prompt use was
|
||||
1,468,039 with 60.69% cache hits and 7,567 prompt tokens per committed
|
||||
consequence. Interpreter consumed 737,148 prompt tokens at 67.18% cache hits;
|
||||
Projector reached 70.03%.
|
||||
|
||||
That run falsified the prompt-only repeated-posture fix. Its trace showed mixed
|
||||
arena turns where one constituent acted while other institutions explicitly
|
||||
held their existing course. The old `CellAppraisal` could represent actions or
|
||||
one global inaction reason, not attributed inaction beside another subject's
|
||||
action. The Interpreter sometimes restated the holding institution's posture to
|
||||
avoid dropping its choice. The appraisal contract now has exact `actions[]` and
|
||||
exact `inactions[]`; the old global field is deleted. Inactions are bounded,
|
||||
non-mutating, permission-checked, unique, and mutually exclusive with an action
|
||||
from the same subject. The next live profile must compare retry rate and token
|
||||
efficiency under this new contract before claiming the waste is cut.
|
||||
|
||||
Do not call setting-wide material agency finished yet. Most population output
|
||||
is still an attributed `gestalt_activity` attempt stored as history. The long
|
||||
run became repetitive around ferry loading and shelter requests because no
|
||||
|
|
|
|||
|
|
@ -176,6 +176,21 @@ location, dormant members whose exact location matches, and materialized
|
|||
members eligible for folding. The kernel rejects inactive hierarchy nodes and
|
||||
location mismatches. Nested fission and migration therefore change which
|
||||
baseline a durable delta composes with, never who the person is.
|
||||
The projection also names the player actor's reciprocal relationship to each
|
||||
nearby dormant member. The planner owns reversible scene casting and may surface
|
||||
an earned callback without a player prompt; it cannot move a remote person or
|
||||
commit the cast. Failed live casts retain a private presence-preflight artifact.
|
||||
|
||||
Institution `already_committed_posture` is projected as durable state, distinct
|
||||
from unresolved pressure and fresh choice. Maintaining it is inaction. A model
|
||||
may propose only a materially different posture; repeated posture remains a
|
||||
locally rejected no-op and cannot enter an atomic wave.
|
||||
|
||||
Cell appraisals carry exact `actions[]` and exact `inactions[]`. Inactions name
|
||||
the constituent or selected dormant member who holds and a bounded reason. They
|
||||
cannot duplicate, cross cell authority, or coexist with an action from the same
|
||||
subject. They are non-mutating audit/fairness evidence. The former global
|
||||
inaction string is no longer an owner or runtime path.
|
||||
|
||||
The active cell budget and provider concurrency limit are separate controls.
|
||||
Budget and pins increment `resolution_epoch` without advancing world revision or
|
||||
|
|
|
|||
|
|
@ -267,6 +267,6 @@ data_flow:
|
|||
- If state grows faster than understanding, cut it back.
|
||||
|
||||
current_status:
|
||||
summary: GhostlightDungeon production remains on the verified Starfire release. Multiresolution pressure proves the callback, nested diaspora, fairness, and scale matrix. Lease-preserving pressure exposed two hidden faults now cut: redundant controls manufactured epochs, and migrated topology could leave a disconnected leased cell alive. Production state remains untouched.
|
||||
next_action: Rerun the lease-preserving budget-4 profile with canonical singleton-plus-focus fairness accounting, then test budget 8 with independent provider parallelism. Require covers to survive valid leases and replan immediately around migration-broken adjacency.
|
||||
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.
|
||||
summary: GhostlightDungeon production remains on the verified Starfire release. The 31-wave budget-8/provider-parallelism-4 live profile covered every subject, committed 194 consequences, recovered 15 rejected pulses without partial mutation, preserved player and rival/member agency, and returned the same Mira. Its trace exposed the missing attributed-inaction representation; the global inaction string has now been replaced by exact non-mutating per-subject inactions. Production state remains untouched.
|
||||
next_action: Run the same budget-8 pressure with the attributed-inaction contract and provider parallelism 8, then compare retry rate, cache efficiency, latency, and exact cover behavior against the 6253f9f parallelism-4 baseline. Keep a future strategic outcome resolver separate from Interpreter authority.
|
||||
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, includes reciprocal player relationship evidence, and may propose an earned cast without committing it. Simulation cells are derived covers only.
|
||||
|
|
|
|||
Loading…
Reference in a new issue