diff --git a/crates/tui/src/client/chat.rs b/crates/tui/src/client/chat.rs index c9bbb01675..70a76c3ad0 100644 --- a/crates/tui/src/client/chat.rs +++ b/crates/tui/src/client/chat.rs @@ -6595,7 +6595,7 @@ mod image_block_wire_tests { assert_eq!(later_wire[6]["content"], "What happened next?"); let restored = crate::compaction::restore_compaction_checkpoint( - crate::runtime_handoff::project_messages_for_restore(&messages), + crate::runtime_handoff::project_owned_messages_for_restore(messages.clone()), Some(&summary), ); let restored_wire = build_chat_messages(None, &restored, "gpt-4o"); @@ -8201,8 +8201,9 @@ mod google_thought_signature_tests { let recovery = manager .recover_session_for_resume(&id) .expect("recover session for resume"); - let restored = - crate::runtime_handoff::project_messages_for_restore(&recovery.session.messages); + let restored = crate::runtime_handoff::project_owned_messages_for_restore( + recovery.session.messages.clone(), + ); (recovery, restored, on_disk) } diff --git a/crates/tui/src/runtime_handoff.rs b/crates/tui/src/runtime_handoff.rs index 9377526f1c..c0199d6c8f 100644 --- a/crates/tui/src/runtime_handoff.rs +++ b/crates/tui/src/runtime_handoff.rs @@ -561,15 +561,7 @@ fn runtime_handoff_message_with_meta(text: String, turn_meta: &str) -> Message { /// Replace persisted runtime handoffs with concise, non-authoritative resume /// checkpoints. Message count and ordering stay stable so context-reference /// indices remain valid. Calling this repeatedly returns the same messages. -pub(crate) fn project_messages_for_restore(messages: &[Message]) -> Vec { - messages - .iter() - .map(|message| rewrite_message_for_restore(message).unwrap_or_else(|| message.clone())) - .collect() -} - -/// [`project_messages_for_restore`] for a caller that owns the history: -/// messages the projection leaves alone are moved, not cloned, so a restore +/// Messages the projection leaves alone are moved, not cloned, so a restore /// holds one copy of the conversation instead of two while it runs. pub(crate) fn project_owned_messages_for_restore(messages: Vec) -> Vec { messages @@ -1275,7 +1267,7 @@ mod tests { assert!(first_checkpoint.contains("\"nonterminal\":1")); assert!(first_checkpoint.contains("\"status\":\"running\"")); - let running_projection = project_messages_for_restore(&messages); + let running_projection = project_owned_messages_for_restore(messages.clone()); let running_display = restored_subagent_checkpoint_display( running_projection .last() @@ -1325,7 +1317,7 @@ mod tests { "repeated compaction must retain exactly one typed checkpoint" ); - let projected = project_messages_for_restore(&messages); + let projected = project_owned_messages_for_restore(messages.clone()); let display = restored_subagent_checkpoint_display( projected.last().expect("restored topology checkpoint"), ) @@ -1335,7 +1327,10 @@ mod tests { assert!(display.contains("terminal fact retained")); assert!(!display.contains("prior worker processes are not assumed active")); assert!(!display.contains("\"status\":\"completed\"")); - assert_eq!(project_messages_for_restore(&projected), projected); + assert_eq!( + project_owned_messages_for_restore(projected.clone()), + projected + ); } #[test] @@ -1377,7 +1372,7 @@ mod tests { "Implemented the shared restore projection.\nCheckpoint: focused tests pass.", )); - let projected = project_messages_for_restore(&[user_task.clone(), raw.clone()]); + let projected = project_owned_messages_for_restore(vec![user_task.clone(), raw.clone()]); assert_eq!( project_owned_messages_for_restore(vec![user_task.clone(), raw]), projected, @@ -1394,7 +1389,10 @@ mod tests { assert!(!display.contains("")); assert!(!display.contains("Do not tell the user")); - assert_eq!(project_messages_for_restore(&projected), projected); + assert_eq!( + project_owned_messages_for_restore(projected.clone()), + projected + ); } #[test] @@ -1410,7 +1408,7 @@ mod tests { persisted, "Terminal checkpoint", )); - let projected = project_messages_for_restore(&[raw]); + let projected = project_owned_messages_for_restore(vec![raw]); let display = restored_subagent_checkpoint_display(&projected[0]) .expect("restored checkpoint display"); assert!( @@ -1458,7 +1456,7 @@ mod tests { UserTurnPromptKind::NotPrompt ); - let projected = project_messages_for_restore(&[raw]); + let projected = project_owned_messages_for_restore(vec![raw]); assert_eq!( classify_user_turn_prompt(&projected[0]), UserTurnPromptKind::NotPrompt @@ -1609,7 +1607,7 @@ mod tests { "", )); - let projected = project_messages_for_restore(&[raw]); + let projected = project_owned_messages_for_restore(vec![raw]); let display = restored_subagent_checkpoint_display(&projected[0]) .expect("restored failed checkpoint display"); assert!(display.contains("Agent: agent_failed")); @@ -1639,7 +1637,7 @@ mod tests { assert!(text.contains("priority=\"high\"")); assert!(text.contains("agent:agent_failed/full_transcript")); - let projected = project_messages_for_restore(&[raw]); + let projected = project_owned_messages_for_restore(vec![raw]); let display = restored_subagent_checkpoint_display(&projected[0]) .expect("restored failed checkpoint display"); assert!(display.contains("Agent: Tide (agent_failed)")); @@ -1662,7 +1660,7 @@ mod tests { )); let raw = runtime_handoff_message(format!("{first}\n\n{second}")); - let projected = project_messages_for_restore(&[raw]); + let projected = project_owned_messages_for_restore(vec![raw]); let display = restored_subagent_checkpoint_display(&projected[0]) .expect("restored checkpoint display"); assert!(display.starts_with(RESTORED_COMPLETIONS_HEADER)); @@ -1696,7 +1694,7 @@ mod tests { #[test] fn restore_projection_replaces_stale_waiting_directions_with_historical_state() { let raw = waiting_for_subagents_runtime_message(2); - let projected = project_messages_for_restore(&[raw]); + let projected = project_owned_messages_for_restore(vec![raw]); let display = restored_subagent_checkpoint_display(&projected[0]) .expect("restored runtime checkpoint display"); assert!(display.contains("Status at save: running (2 child jobs)")); @@ -1738,7 +1736,8 @@ mod tests { ], }; - let projected = project_messages_for_restore(&[lookalike.clone(), wrong_authority.clone()]); + let projected = + project_owned_messages_for_restore(vec![lookalike.clone(), wrong_authority.clone()]); assert_eq!(projected, vec![lookalike.clone(), wrong_authority.clone()]); assert_eq!( classify_user_turn_prompt(&lookalike), @@ -1781,7 +1780,7 @@ mod tests { ], }; - let projected = project_messages_for_restore(&[raw]); + let projected = project_owned_messages_for_restore(vec![raw]); let display = restored_subagent_checkpoint_display(&projected[0]) .expect("restored checkpoint display"); assert!(display.contains("agent_idle")); @@ -1794,7 +1793,7 @@ mod tests { "Partial child result\n{not-json}", )); - let projected = project_messages_for_restore(&[raw]); + let projected = project_owned_messages_for_restore(vec![raw]); let display = restored_subagent_checkpoint_display(&projected[0]) .expect("restored fallback checkpoint display"); assert!(display.contains("Status: unavailable")); @@ -1818,7 +1817,7 @@ mod tests { }) ); let raw = subagent_completion_runtime_message(&payload); - let projected = project_messages_for_restore(&[raw]); + let projected = project_owned_messages_for_restore(vec![raw]); let display = restored_subagent_checkpoint_display(&projected[0]) .expect("workflow uses the same persisted receipt reader"); assert!(display.contains("workflow_release")); @@ -1826,7 +1825,10 @@ mod tests { assert!(display.contains("inspect recorded evidence")); assert!(!display.contains("runtime_event")); assert!(!display.contains("subagent.done")); - assert_eq!(project_messages_for_restore(&projected), projected); + assert_eq!( + project_owned_messages_for_restore(projected.clone()), + projected + ); } } @@ -1854,7 +1856,7 @@ mod tests { nested, )); - let projected = project_messages_for_restore(&[raw]); + let projected = project_owned_messages_for_restore(vec![raw]); let display = restored_subagent_checkpoint_display(&projected[0]) .expect("restored nested checkpoint display"); assert!(display.contains("Parent checkpoint before nested result.")); diff --git a/crates/tui/src/session_manager.rs b/crates/tui/src/session_manager.rs index 5b8fdf2f59..79cf086c12 100644 --- a/crates/tui/src/session_manager.rs +++ b/crates/tui/src/session_manager.rs @@ -6226,7 +6226,7 @@ mod tests { let tmp = tempdir().expect("tempdir"); let waiting = crate::runtime_handoff::waiting_for_subagents_runtime_message(2); let restored = - crate::runtime_handoff::project_messages_for_restore(std::slice::from_ref(&waiting)) + crate::runtime_handoff::project_owned_messages_for_restore(vec![waiting.clone()]) .into_iter() .next() .expect("restore projection yields one message"); diff --git a/crates/tui/src/session_peek.rs b/crates/tui/src/session_peek.rs index 0a2439c2fd..f767ee486e 100644 --- a/crates/tui/src/session_peek.rs +++ b/crates/tui/src/session_peek.rs @@ -459,7 +459,7 @@ mod tests { fn runtime_handoffs() -> Vec<(&'static str, Message)> { let waiting = crate::runtime_handoff::waiting_for_subagents_runtime_message(2); let restored = - crate::runtime_handoff::project_messages_for_restore(std::slice::from_ref(&waiting)); + crate::runtime_handoff::project_owned_messages_for_restore(vec![waiting.clone()]); vec![ ("waiting_for_subagents", waiting), ( diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index f1ec23f4a3..3ea2006099 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -4903,13 +4903,48 @@ impl App { messages: Vec, session: &crate::session_manager::SavedSession, ) { - self.session_journal = session.journal.clone().unwrap_or_else(|| { + let journal = session.journal.clone().unwrap_or_else(|| { crate::session_tree::SessionJournal::from_messages( session.messages.clone(), session.metadata.spawn_depth, ) }); - self.api_message_stamps = session.journal_message_stamps(); + self.install_restored_api_messages(messages, journal, session.journal_message_stamps()); + } + + /// [`Self::restore_api_messages`] for a caller that owns the loaded + /// session: the journal and the message history are *moved* out of it, + /// not cloned, and the history goes through the owned restore projection, + /// so a resume holds one copy of the transcript instead of three (memory + /// note M3). `session.journal` and `session.messages` are left empty. + pub fn restore_api_messages_from_owned( + &mut self, + session: &mut crate::session_manager::SavedSession, + ) { + let stamps = session.journal_message_stamps(); + let journal = match session.journal.take() { + Some(journal) => journal, + // Legacy session without a journal: rebuild it from the saved + // history, as the borrowing path does. + None => crate::session_tree::SessionJournal::from_messages( + session.messages.clone(), + session.metadata.spawn_depth, + ), + }; + let messages = crate::runtime_handoff::project_owned_messages_for_restore(std::mem::take( + &mut session.messages, + )); + self.install_restored_api_messages(messages, journal, stamps); + } + + fn install_restored_api_messages( + &mut self, + messages: Vec, + journal: crate::session_tree::SessionJournal, + stamps: Vec>, + ) { + self.session_journal = journal; + self.api_message_stamps = stamps; self.api_message_stamps .resize_with(messages.len(), Utc::now); self.api_messages = Arc::new(messages); diff --git a/crates/tui/src/tui/app/tests.rs b/crates/tui/src/tui/app/tests.rs index ff01b2a61e..d78f9cbe89 100644 --- a/crates/tui/src/tui/app/tests.rs +++ b/crates/tui/src/tui/app/tests.rs @@ -7509,3 +7509,80 @@ fn launch_onboarding_scenario() { assert_eq!(ready, OnboardingState::None); } } + +/// Memory note M3: a resume owns the loaded session, so its journal and +/// history move into the App instead of being cloned beside a copy that is +/// dropped right after. The journal's entry buffer is the *same allocation* +/// afterwards, and the result matches the borrowing path exactly. +#[test] +fn owned_restore_moves_the_journal_and_matches_the_borrowing_restore() { + let message = |text: &str| Message { + role: codewhale_models::Role::User, + content: vec![codewhale_models::ContentBlock::Text { + text: text.to_string(), + cache_control: None, + }], + }; + let messages = vec![message("first"), message("second")]; + let t0 = DateTime::::from_timestamp(1_700_000_000, 0).unwrap(); + let t1 = t0 + chrono::Duration::seconds(12); + let saved = crate::session_manager::create_saved_session_with_id_mode_and_stamps( + "owned-restore".to_string(), + &messages, + &[t0, t1], + "test-model", + Path::new("."), + 0, + None, + None, + ); + + let mut borrowed = App::new(test_options(false), &Config::default()); + borrowed.restore_api_messages( + crate::runtime_handoff::project_owned_messages_for_restore(saved.messages.clone()), + &saved, + ); + + let mut owned_session = saved.clone(); + let entries_buffer = owned_session + .journal + .as_ref() + .expect("journal") + .entries + .as_ptr(); + let mut owned = App::new(test_options(false), &Config::default()); + owned.restore_api_messages_from_owned(&mut owned_session); + + assert_eq!( + owned.session_journal.entries.as_ptr(), + entries_buffer, + "the journal must be moved into the App, not cloned" + ); + assert!(owned_session.journal.is_none()); + assert!(owned_session.messages.is_empty()); + assert_eq!( + owned.session_journal.entries, + borrowed.session_journal.entries + ); + assert_eq!(owned.api_messages, borrowed.api_messages); + assert_eq!(owned.api_message_stamps, vec![t0, t1]); + assert_eq!(owned.api_message_stamps, borrowed.api_message_stamps); + + // A legacy session without a journal rebuilds it from the history, on + // both paths alike. + let mut legacy = saved.clone(); + legacy.journal = None; + let mut legacy_borrowed = App::new(test_options(false), &Config::default()); + legacy_borrowed.restore_api_messages( + crate::runtime_handoff::project_owned_messages_for_restore(legacy.messages.clone()), + &legacy, + ); + let mut legacy_owned = App::new(test_options(false), &Config::default()); + legacy_owned.restore_api_messages_from_owned(&mut legacy); + assert_eq!(legacy_owned.api_messages, legacy_borrowed.api_messages); + assert_eq!( + legacy_owned.session_journal.entries.len(), + legacy_borrowed.session_journal.entries.len() + ); + assert_eq!(legacy_owned.api_message_stamps.len(), 2); +} diff --git a/crates/tui/src/tui/ui/apply.rs b/crates/tui/src/tui/ui/apply.rs index aa8d261b0f..eb91c0dd29 100644 --- a/crates/tui/src/tui/ui/apply.rs +++ b/crates/tui/src/tui/ui/apply.rs @@ -1364,10 +1364,12 @@ pub(crate) async fn apply_command_result( return Ok(false); } }; + let resumed_id = session.metadata.id.clone(); + let title = crate::session_manager::sanitize_session_title(&session.metadata.title); let respawn = match apply_loaded_session_config_snapshot( app, config, - &session, + session, fresh_config, true, ) { @@ -1409,7 +1411,6 @@ pub(crate) async fn apply_command_result( config: app.compaction_config(), }) .await; - let title = crate::session_manager::sanitize_session_title(&session.metadata.title); // Restore may have queued a legacy configuration notice. // Admit it first so the confirmed resume remains the latest // toast instead of being immediately covered on the next draw. @@ -1421,7 +1422,7 @@ pub(crate) async fn apply_command_result( StatusToastLevel::Success, Some(4_000), ) - .for_event(format!("session-resumed:{}", session.metadata.id)), + .for_event(format!("session-resumed:{resumed_id}")), ); // A loaded session is the working screen. The launch card's // recent rows reach here through `/resume`-shaped dispatch; @@ -3571,13 +3572,17 @@ pub(crate) fn apply_loaded_session( config: &mut Config, session: &SavedSession, ) -> Result<(), String> { - apply_loaded_session_with_goal(app, config, session, None) + apply_loaded_session_with_goal(app, config, session.clone(), None) } +/// Install a loaded session as the live conversation. The session is taken +/// by value because it is consumed: its journal, history, artifacts and +/// metadata move into `app` instead of being cloned beside a copy the caller +/// would drop right after (memory note M3). On `Err` nothing was installed. pub(crate) fn apply_loaded_session_with_goal( app: &mut App, config: &mut Config, - session: &SavedSession, + mut session: SavedSession, goal: Option<&crate::session_manager::SessionGoalState>, ) -> Result<(), String> { let mut recovered_binding = None; @@ -3672,10 +3677,7 @@ pub(crate) fn apply_loaded_session_with_goal( let _settled_old_cost_scope = crate::cost_status::close_current_scope(); *config = *restored_route.config; app.refresh_notification_settings(config); - app.restore_api_messages( - crate::runtime_handoff::project_messages_for_restore(&session.messages), - session, - ); + app.restore_api_messages_from_owned(&mut session); app.clear_history(); app.tool_cells.clear(); app.tool_details_by_cell.clear(); @@ -3847,7 +3849,8 @@ pub(crate) fn apply_loaded_session_with_goal( app.cumulative_turn_duration = std::time::Duration::from_secs(session.metadata.cumulative_turn_secs); app.current_session_id = Some(session.metadata.id.clone()); - app.current_session_metadata = Some(session.metadata.clone()); + app.session_title = Some(session.metadata.title.clone()); + app.current_session_metadata = Some(session.metadata); reset_approval_scope_for_new_conversation(app); if let Some(binding) = recovered_binding { if let Some(metadata) = app.current_session_metadata.as_mut() { @@ -3859,17 +3862,12 @@ pub(crate) fn apply_loaded_session_with_goal( None, ); } - app.session_artifacts = session.artifacts.clone(); - app.session_title = Some(session.metadata.title.clone()); - app.window_title = session.window_title.clone(); + app.session_artifacts = session.artifacts; + app.window_title = session.window_title; app.workspace_context = None; app.workspace_is_linked_worktree = false; app.workspace_context_refreshed_at = None; - if let Some(sp) = session.system_prompt.as_ref() { - app.system_prompt = Some(SystemPrompt::Text(sp.clone())); - } else { - app.system_prompt = None; - } + app.system_prompt = session.system_prompt.map(SystemPrompt::Text); app.scroll_to_bottom(); Ok(()) } @@ -3877,7 +3875,7 @@ pub(crate) fn apply_loaded_session_with_goal( pub(crate) fn apply_loaded_session_config_snapshot( app: &mut App, config: &mut Config, - session: &SavedSession, + session: SavedSession, mut next_config: Config, force_engine_respawn: bool, ) -> Result { diff --git a/crates/tui/src/tui/ui/event_loop.rs b/crates/tui/src/tui/ui/event_loop.rs index 7f32f37cf3..0b989b797e 100644 --- a/crates/tui/src/tui/ui/event_loop.rs +++ b/crates/tui/src/tui/ui/event_loop.rs @@ -813,11 +813,12 @@ pub async fn run_tui( match load_result { Ok(Some(saved)) => match manager.load_session_goal(&saved.metadata.id) { Ok(goal) => { - match apply_loaded_session_with_goal(&mut app, config, &saved, goal.as_ref()) { + let saved_id = saved.metadata.id.clone(); + match apply_loaded_session_with_goal(&mut app, config, saved, goal.as_ref()) { Ok(()) => { app.status_message = Some(format!( "Resumed session: {}", - crate::session_manager::truncate_id(&saved.metadata.id) + crate::session_manager::truncate_id(&saved_id) )); } Err(err) => { diff --git a/crates/tui/src/tui/ui/handlers.rs b/crates/tui/src/tui/ui/handlers.rs index bd6a7f3821..01122cb88f 100644 --- a/crates/tui/src/tui/ui/handlers.rs +++ b/crates/tui/src/tui/ui/handlers.rs @@ -1513,10 +1513,11 @@ pub(crate) async fn handle_view_events( Ok(recovery) => { let session = recovery.session; let next_config = config.clone(); + let message_count = session.metadata.message_count; let respawn = match apply_loaded_session_config_snapshot( app, config, - &session, + session, next_config, false, ) { @@ -1570,7 +1571,7 @@ pub(crate) async fn handle_view_events( let loaded_message = format!( "Session loaded (ID: {}, {} messages)", crate::session_manager::truncate_id(&session_id), - session.metadata.message_count + message_count ); app.add_message(HistoryCell::System { content: loaded_message.clone(), diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index 66758be2ae..fd409295a3 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -21378,7 +21378,7 @@ fn missing_named_custom_provider_resume_leaves_current_session_wholly_unchanged( let err = apply_loaded_session_config_snapshot( &mut app, &mut config, - &session, + session.clone(), Config::default(), true, ) @@ -21547,7 +21547,7 @@ fn file_load_uses_one_fresh_config_snapshot_for_custom_route_and_app_state() { let respawn = apply_loaded_session_config_snapshot( &mut app, &mut stale_config, - &session, + session.clone(), fresh_config, true, ) @@ -21699,7 +21699,7 @@ fn file_load_respawns_engine_when_same_custom_identity_changes_endpoint() { let respawn = apply_loaded_session_config_snapshot( &mut app, &mut stale_config, - &session, + session.clone(), fresh_config, true, ) @@ -21766,7 +21766,7 @@ fn file_load_route_refresh_preserves_effective_permission_and_feature_overlays() let respawn = apply_loaded_session_config_snapshot( &mut app, &mut effective_config, - &session, + session.clone(), raw_disk_config, true, ) diff --git a/crates/tui/src/tui/ui/tests/runtime_store_binding.rs b/crates/tui/src/tui/ui/tests/runtime_store_binding.rs index 6abb1c1540..147278170d 100644 --- a/crates/tui/src/tui/ui/tests/runtime_store_binding.rs +++ b/crates/tui/src/tui/ui/tests/runtime_store_binding.rs @@ -42,7 +42,7 @@ async fn runtime_store_binding_persists_on_exit_without_a_model_turn() -> anyhow sessions.save_session(&original)?; sessions.save_checkpoint(&original)?; let mut app = Box::new(create_test_app()); - apply_loaded_session_with_goal(&mut app, &mut config, &original, None) + apply_loaded_session_with_goal(&mut app, &mut config, original.clone(), None) .map_err(anyhow::Error::msg)?; let task_config = TaskManagerConfig::from_runtime(&config, root.path().into(), None, Some(1)); let tasks = TaskManager::start( @@ -226,7 +226,7 @@ fn runtime_store_binding_survives_launch_snapshot_and_resume() -> anyhow::Result let resumed_config = &mut resumed_config; boxed_phase(move || async move { let mut resumed = Box::new(create_test_app()); - apply_loaded_session_with_goal(&mut resumed, resumed_config, loaded, None) + apply_loaded_session_with_goal(&mut resumed, resumed_config, loaded.clone(), None) .map_err(anyhow::Error::msg)?; let tasks = TaskManager::start( task_config.clone(), @@ -325,9 +325,13 @@ fn runtime_store_binding_survives_launch_snapshot_and_resume() -> anyhow::Result other_app.runtime_services.task_manager = Some(foreign.clone()); other_app.input = "preserve pending input".into(); let old_id = other_app.current_session_id.clone(); - let error = - apply_loaded_session_with_goal(&mut other_app, resumed_config, loaded, None) - .unwrap_err(); + let error = apply_loaded_session_with_goal( + &mut other_app, + resumed_config, + loaded.clone(), + None, + ) + .unwrap_err(); // The refusal must name the route that actually works. "Resume // it in a new Codewhale process" was true but unactionable: // starting a new process and then picking the session from @@ -579,7 +583,7 @@ async fn picker_recovers_missing_store_into_the_idle_host_and_persists_before_re let held = plan_state .try_lock() .expect("hold Work state during recovery"); - assert!(apply_loaded_session_with_goal(&mut app, &mut config, &saved, None).is_err()); + assert!(apply_loaded_session_with_goal(&mut app, &mut config, saved.clone(), None).is_err()); assert_eq!(app.current_session_id.as_deref(), Some("picker-current")); assert_eq!(app.api_messages, current_messages); assert_eq!( @@ -592,7 +596,7 @@ async fn picker_recovers_missing_store_into_the_idle_host_and_persists_before_re "binding repair survives a contended UI restore" ); drop(held); - apply_loaded_session_with_goal(&mut app, &mut config, &saved, None) + apply_loaded_session_with_goal(&mut app, &mut config, saved.clone(), None) .map_err(anyhow::Error::msg)?; assert_eq!( app.current_session_id.as_deref(), @@ -835,7 +839,7 @@ async fn picker_adopts_existing_empty_unheld_store() -> anyhow::Result<()> { app.runtime_services.task_manager = Some(tasks.clone()); app.current_session_id = Some("picker-current".into()); - apply_loaded_session_with_goal(&mut app, &mut config, &saved, None) + apply_loaded_session_with_goal(&mut app, &mut config, saved.clone(), None) .map_err(anyhow::Error::msg)?; assert_eq!(app.current_session_id.as_deref(), Some("picker-adoptable")); let durable = sessions.load_session("picker-adoptable")?;