diff --git a/src-tauri/src/storage_manager/companion_shared_memory.rs b/src-tauri/src/storage_manager/companion_shared_memory.rs index 4d3526d6a..bbe641775 100644 --- a/src-tauri/src/storage_manager/companion_shared_memory.rs +++ b/src-tauri/src/storage_manager/companion_shared_memory.rs @@ -548,6 +548,7 @@ pub fn merge_continuity_into_state( character_id: &str, persona_id: Option<&str>, state: Option, + pull_relationship: bool, ) -> Result, String> { let Some(mut state) = state else { return Ok(None); @@ -585,14 +586,21 @@ pub fn merge_continuity_into_state( state_object.insert("soulGrowth".to_string(), JsonValue::Array(merged)); } - let relationship_states = - serde_json::from_str::(&continuity.relationship_states_json) - .unwrap_or_else(|_| JsonValue::Object(Default::default())); - if let Some(relationship) = relationship_states - .get(relationship_key(persona_id)) - .filter(|value| value.is_object()) - { - state_object.insert("relationshipState".to_string(), relationship.clone()); + // Only pull the shared relationship state in when the caller wants it (i.e. + // on load, or when a fresh session should inherit the shared bond). On save + // of a session that already carries its own — possibly freshly evolved — + // relationship state, pulling here would overwrite it with the stale stored + // value before it can be persisted, permanently freezing the bond. + if pull_relationship { + let relationship_states = + serde_json::from_str::(&continuity.relationship_states_json) + .unwrap_or_else(|_| JsonValue::Object(Default::default())); + if let Some(relationship) = relationship_states + .get(relationship_key(persona_id)) + .filter(|value| value.is_object()) + { + state_object.insert("relationshipState".to_string(), relationship.clone()); + } } if let Some(episode) = session_id .map(str::trim) @@ -731,6 +739,7 @@ mod tests { "character-1", Some("persona-1"), Some(fresh_session), + true, ) .unwrap() .unwrap(); @@ -741,6 +750,58 @@ mod tests { assert_eq!(hydrated["emotionalState"]["felt"]["calm"], 0.9); } + #[test] + fn saving_keeps_the_sessions_evolved_relationship_over_stored_continuity() { + let conn = connection(); + // Stored continuity holds an older/default bond. + persist_continuity_from_state( + &conn, + None, + "character-1", + Some("persona-1"), + &json!({"relationshipState": {"trust": 0.25, "interactionCount": 0}}), + ) + .unwrap(); + + // A session that evolved its own bond this turn. + let evolved = json!({ + "relationshipState": {"trust": 0.6, "interactionCount": 42}, + "emotionalState": {"felt": {"calm": 0.5}} + }); + + // The save-side merge (pull_relationship = false) must NOT overwrite the + // evolved bond with the stale stored value — that was the freeze bug. + let merged = merge_continuity_into_state( + &conn, + None, + "character-1", + Some("persona-1"), + Some(evolved), + false, + ) + .unwrap() + .unwrap(); + assert_eq!(merged["relationshipState"]["trust"], 0.6); + assert_eq!(merged["relationshipState"]["interactionCount"], 42); + + // Persisting it promotes the evolved bond to the shared store, so a fresh + // load then sees it. + persist_continuity_from_state(&conn, None, "character-1", Some("persona-1"), &merged) + .unwrap(); + let reloaded = merge_continuity_into_state( + &conn, + None, + "character-1", + Some("persona-1"), + Some(json!({"relationshipState": {"trust": 0.0, "interactionCount": 0}})), + true, + ) + .unwrap() + .unwrap(); + assert_eq!(reloaded["relationshipState"]["trust"], 0.6); + assert_eq!(reloaded["relationshipState"]["interactionCount"], 42); + } + #[test] fn relationships_remain_isolated_per_persona() { let conn = connection(); @@ -767,6 +828,7 @@ mod tests { "character-1", Some("persona-1"), Some(json!({"relationshipState": {"trust": 0.0}})), + true, ) .unwrap() .unwrap(); @@ -776,6 +838,7 @@ mod tests { "character-1", Some("persona-2"), Some(json!({"relationshipState": {"trust": 0.0}})), + true, ) .unwrap() .unwrap(); @@ -829,6 +892,7 @@ mod tests { "character-1", None, Some(json!({"soulGrowth": []})), + true, ) .unwrap() .unwrap(); @@ -872,6 +936,7 @@ mod tests { "character-1", Some("persona-1"), Some(json!({"relationshipState": {"trust": 0.0}})), + true, ) .unwrap() .unwrap(); diff --git a/src-tauri/src/storage_manager/sessions.rs b/src-tauri/src/storage_manager/sessions.rs index 10938b04a..e2f72fc70 100644 --- a/src-tauri/src/storage_manager/sessions.rs +++ b/src-tauri/src/storage_manager/sessions.rs @@ -55,6 +55,12 @@ fn resolve_companion_state_json( character_id, mode, )?; + // A session that already carries its own companion state owns the + // authoritative (and possibly freshly evolved) relationship bond, so we must + // not let the merge overwrite it with the stored value. A fresh session with + // no state yet should still inherit the shared relationship. + let session_carries_state = + matches!(session_value.get("companionState"), Some(v) if !v.is_null()); let state = match session_value.get("companionState") { Some(v) if !v.is_null() => Some(v.clone()), _ if !is_companion => None, @@ -79,6 +85,7 @@ fn resolve_companion_state_json( character_id, persona_id, state, + !session_carries_state, ) .map(|state| state.and_then(|value| serde_json::to_string(&value).ok())) } @@ -106,6 +113,7 @@ fn hydrate_stored_companion_state( character_id, if persona_disabled { None } else { persona_id }, state, + true, ) }