From e9f6c45afca31441d459e8da04e04f7510dd39e8 Mon Sep 17 00:00:00 2001 From: Scott McMaster Date: Sat, 16 May 2026 11:00:48 +0800 Subject: [PATCH 1/3] feat(evolve): enhance EvolveState to track last evolution state and preserve chat memory for conversational Begin's --- apps/native/src-tauri/src/evolve/lifecycle.rs | 12 ++++- .../src/managed_edits/managed_edit.rs | 1 + .../src-tauri/src/shared_types/evolve.rs | 7 +++ .../src-tauri/src/state/evolve_state.rs | 50 ++++++++++++++++--- 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/apps/native/src-tauri/src/evolve/lifecycle.rs b/apps/native/src-tauri/src/evolve/lifecycle.rs index c5422678e..698f7e1bf 100644 --- a/apps/native/src-tauri/src/evolve/lifecycle.rs +++ b/apps/native/src-tauri/src/evolve/lifecycle.rs @@ -231,7 +231,15 @@ pub async fn backup_evolve_and_record_changeset( // Short-circuit: conversational responses made no environment changes. if evolution.state == EvolutionState::Conversational { info!("[evolution] Conversational response — skipping git/db workflow"); - let evolve_state = evolve_state::get(app).unwrap_or_default(); + let evolve_state = evolve_state::set( + app, + EvolveState { + last_evolution_state: Some(EvolutionState::Conversational), + ..evolve_state::get(app).unwrap_or_default() + }, + &initial_status.changes, + ) + .unwrap_or_default(); return Ok(EvolutionResult { change_map: SemanticChangeMap::default(), git_status: initial_status, @@ -271,6 +279,7 @@ pub async fn backup_evolve_and_record_changeset( evolution_id: db_evolution_id, current_changeset_id: new_changeset_id, backup_branch, + last_evolution_state: Some(evolution.state.clone()), ..current_state }, &final_status.changes, @@ -321,6 +330,7 @@ fn restore_after_failure(app: &AppHandle, config_dir: &str, backup_branch: &Opti app, EvolveState { backup_branch: None, + last_evolution_state: Some(EvolutionState::Failed), ..evolve_state::get(app).unwrap_or_default() }, &[], diff --git a/apps/native/src-tauri/src/managed_edits/managed_edit.rs b/apps/native/src-tauri/src/managed_edits/managed_edit.rs index 40fcacf73..059b00109 100644 --- a/apps/native/src-tauri/src/managed_edits/managed_edit.rs +++ b/apps/native/src-tauri/src/managed_edits/managed_edit.rs @@ -63,6 +63,7 @@ pub fn prepare_managed_edit(app: &AppHandle) -> Result { rollback_store_path, rollback_changeset_id, step: shared_types::EvolveStep::Evolve, + last_evolution_state: None, }, &pre_edit_status.changes, ) diff --git a/apps/native/src-tauri/src/shared_types/evolve.rs b/apps/native/src-tauri/src/shared_types/evolve.rs index d5fc28585..f025af3c2 100644 --- a/apps/native/src-tauri/src/shared_types/evolve.rs +++ b/apps/native/src-tauri/src/shared_types/evolve.rs @@ -101,6 +101,12 @@ pub struct EvolveState { pub rollback_changeset_id: Option, /// UI step derived from the routing state. pub step: EvolveStep, + /// Last terminal state observed for this routing session. + /// + /// This supports transition-sensitive behavior when returning to Begin + /// and maybe some other useful things in the future. + #[serde(default)] + pub last_evolution_state: Option, } impl Default for EvolveState { @@ -115,6 +121,7 @@ impl Default for EvolveState { rollback_store_path: None, rollback_changeset_id: None, step: EvolveStep::Begin, + last_evolution_state: None, } } } diff --git a/apps/native/src-tauri/src/state/evolve_state.rs b/apps/native/src-tauri/src/state/evolve_state.rs index b1d632b2b..2e34d7a48 100644 --- a/apps/native/src-tauri/src/state/evolve_state.rs +++ b/apps/native/src-tauri/src/state/evolve_state.rs @@ -5,7 +5,7 @@ use tauri::{AppHandle, Runtime}; use tauri_plugin_store::StoreExt; use crate::evolve::session_chat_memory_store; -use crate::shared_types::{EvolveState, EvolveStep}; +use crate::shared_types::{EvolutionState, EvolveState, EvolveStep}; use crate::sqlite_types::Change; impl EvolveState { @@ -24,8 +24,12 @@ impl EvolveState { const EVOLVE_STATE_PATH: &str = "evolve-state.json"; const EVOLVE_STATE_KEY: &str = "evolveState"; -fn clear_chat_memory_if_begin(step: &EvolveStep, clear_fn: impl FnOnce()) { - if *step == EvolveStep::Begin { +fn clear_chat_memory_if_begin( + step: &EvolveStep, + preserve_for_conversational_begin: bool, + clear_fn: impl FnOnce(), +) { + if *step == EvolveStep::Begin && !preserve_for_conversational_begin { clear_fn(); } } @@ -52,6 +56,16 @@ pub fn set( let is_built = crate::state::build_state::current_state_built(app, current_changes); let has_changes = !current_changes.is_empty(); state.recompute_step(is_built, has_changes); + + // Preserve chat memory whenever we are at Begin and the last evolution + // outcome was conversational (no edits). This intentionally persists + // across repeated conversational cycles. + let preserve_for_conversational_begin = state.step == EvolveStep::Begin + && matches!( + state.last_evolution_state, + Some(EvolutionState::Conversational) + ); + let store = app.store(EVOLVE_STATE_PATH)?; store.set(EVOLVE_STATE_KEY, serde_json::to_value(&state)?); store.save()?; @@ -62,7 +76,9 @@ pub fn set( // Note that `clear` is NOT a suitable place to do this since it is not called // on all possible transitions back to Begin (e.g. Evolve -> Begin when evolution_id // is cleared but committable is still true). - clear_chat_memory_if_begin(&state.step, || session_chat_memory_store().clear()); + clear_chat_memory_if_begin(&state.step, preserve_for_conversational_begin, || { + session_chat_memory_store().clear() + }); Ok(state) } @@ -79,7 +95,7 @@ mod tests { #[test] fn clear_chat_memory_if_begin_calls_clear() { let mut cleared = false; - clear_chat_memory_if_begin(&EvolveStep::Begin, || { + clear_chat_memory_if_begin(&EvolveStep::Begin, false, || { cleared = true; }); assert!(cleared); @@ -88,17 +104,35 @@ mod tests { #[test] fn clear_chat_memory_if_begin_skips_non_begin_steps() { let mut cleared = false; - clear_chat_memory_if_begin(&EvolveStep::Evolve, || { + clear_chat_memory_if_begin(&EvolveStep::Evolve, false, || { + cleared = true; + }); + assert!(!cleared); + + clear_chat_memory_if_begin(&EvolveStep::Commit, false, || { cleared = true; }); assert!(!cleared); + } - clear_chat_memory_if_begin(&EvolveStep::Commit, || { + #[test] + fn clear_chat_memory_if_begin_skips_when_last_state_is_conversational() { + let mut cleared = false; + clear_chat_memory_if_begin(&EvolveStep::Begin, true, || { cleared = true; }); assert!(!cleared); } + #[test] + fn clear_chat_memory_if_begin_clears_for_non_conversational_last_state() { + let mut cleared = false; + clear_chat_memory_if_begin(&EvolveStep::Begin, false, || { + cleared = true; + }); + assert!(cleared); + } + #[test] fn recomputed_begin_triggers_clear_logic() { let mut state = EvolveState { @@ -109,7 +143,7 @@ mod tests { state.recompute_step(true, false); let mut cleared = false; - clear_chat_memory_if_begin(&state.step, || { + clear_chat_memory_if_begin(&state.step, false, || { cleared = true; }); From 47e4ff3cf5515c312afb02c01db40793f643ad12 Mon Sep 17 00:00:00 2001 From: Scott McMaster Date: Sat, 16 May 2026 11:10:59 +0800 Subject: [PATCH 2/3] Borrow last evolution state (Copilot reviewer comment) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- apps/native/src-tauri/src/state/evolve_state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/native/src-tauri/src/state/evolve_state.rs b/apps/native/src-tauri/src/state/evolve_state.rs index 2e34d7a48..5192ef10e 100644 --- a/apps/native/src-tauri/src/state/evolve_state.rs +++ b/apps/native/src-tauri/src/state/evolve_state.rs @@ -62,7 +62,7 @@ pub fn set( // across repeated conversational cycles. let preserve_for_conversational_begin = state.step == EvolveStep::Begin && matches!( - state.last_evolution_state, + state.last_evolution_state.as_ref(), Some(EvolutionState::Conversational) ); From 786f262a2c36d60c03f07f34f6de2599f6956cd6 Mon Sep 17 00:00:00 2001 From: Scott McMaster Date: Sat, 16 May 2026 11:11:37 +0800 Subject: [PATCH 3/3] Make sure to use most recent evolve state when store write fails (Copilot reviewer comment) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- apps/native/src-tauri/src/evolve/lifecycle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/native/src-tauri/src/evolve/lifecycle.rs b/apps/native/src-tauri/src/evolve/lifecycle.rs index 698f7e1bf..f0d02cdfc 100644 --- a/apps/native/src-tauri/src/evolve/lifecycle.rs +++ b/apps/native/src-tauri/src/evolve/lifecycle.rs @@ -239,7 +239,7 @@ pub async fn backup_evolve_and_record_changeset( }, &initial_status.changes, ) - .unwrap_or_default(); + .unwrap_or_else(|_| evolve_state::get(app).unwrap_or_default()); return Ok(EvolutionResult { change_map: SemanticChangeMap::default(), git_status: initial_status,