mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 10:01:30 -10:00
fix(session): persist and restore model provider across session save/load
Add model_provider to SessionMetadata (default 'deepseek' for legacy sessions) and SessionSnapshot so saved sessions remember which provider was active. On session load, restore the provider, its subagent limits, fallback chain, context window, and reasoning effort normalization. This prevents sessions created under one provider (e.g. DeepSeek) from silently loading under a different active provider.
This commit is contained in:
1 parent
2c404ac5e7
commit
e3033bfa47
6 files changed
+55
-5
No files matched your search
@@ -38,6 +38,7 @@ pub fn save(app: &mut App, path: Option<&str>) -> CommandResult {
|
||||
app.system_prompt.as_ref(),
|
||||
Some(app.mode.label()),
|
||||
);
|
||||
session.metadata.model_provider = app.api_provider.as_str().to_string();
|
||||
app.sync_cost_to_metadata(&mut session.metadata);
|
||||
session.artifacts = app.session_artifacts.clone();
|
||||
|
||||
@@ -94,6 +95,7 @@ pub fn fork(app: &mut App) -> CommandResult {
|
||||
app.system_prompt.as_ref(),
|
||||
Some(app.mode.label()),
|
||||
);
|
||||
parent.metadata.model_provider = app.api_provider.as_str().to_string();
|
||||
app.sync_cost_to_metadata(&mut parent.metadata);
|
||||
parent.artifacts = app.session_artifacts.clone();
|
||||
|
||||
@@ -109,6 +111,7 @@ pub fn fork(app: &mut App) -> CommandResult {
|
||||
app.system_prompt.as_ref(),
|
||||
Some(app.mode.label()),
|
||||
);
|
||||
forked.metadata.model_provider = app.api_provider.as_str().to_string();
|
||||
forked.metadata.copy_cost_from(&parent.metadata);
|
||||
forked.metadata.mark_forked_from(&parent.metadata);
|
||||
|
||||
@@ -238,6 +241,10 @@ pub fn load(app: &mut App, path: Option<&str>) -> CommandResult {
|
||||
app.extend_history(cells_to_add);
|
||||
app.mark_history_updated();
|
||||
app.viewport.transcript_selection.clear();
|
||||
if let Some(provider) = crate::config::ApiProvider::parse(&session.metadata.model_provider) {
|
||||
app.api_provider = provider;
|
||||
app.reasoning_effort = app.reasoning_effort.normalize_for_provider(provider);
|
||||
}
|
||||
app.set_model_selection(session.metadata.model.clone());
|
||||
app.update_model_compaction_budget();
|
||||
app.workspace.clone_from(&session.metadata.workspace);
|
||||
|
||||
@@ -1742,6 +1742,7 @@ impl Engine {
|
||||
messages: self.session.messages.to_vec(),
|
||||
total_tokens,
|
||||
model: self.session.model.clone(),
|
||||
model_provider: self.api_provider.as_str().to_string(),
|
||||
workspace: self.session.workspace.clone(),
|
||||
system_prompt: self.session.system_prompt.clone(),
|
||||
mode: self.current_mode.as_setting().to_string(),
|
||||
|
||||
@@ -22,6 +22,7 @@ pub struct SessionSnapshot {
|
||||
pub messages: Vec<Message>,
|
||||
pub total_tokens: u64,
|
||||
pub model: String,
|
||||
pub model_provider: String,
|
||||
pub workspace: PathBuf,
|
||||
pub system_prompt: Option<SystemPrompt>,
|
||||
pub mode: String,
|
||||
|
||||
@@ -472,12 +472,13 @@ pub(super) async fn save_current_session(
|
||||
snapshot.system_prompt.as_ref(),
|
||||
);
|
||||
updated.metadata.model = snapshot.model.clone();
|
||||
updated.metadata.model_provider = snapshot.model_provider.clone();
|
||||
updated.metadata.mode = Some(snapshot.mode.clone());
|
||||
updated
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
crate::session_manager::create_saved_session_with_id_and_mode(
|
||||
let mut session = crate::session_manager::create_saved_session_with_id_and_mode(
|
||||
existing_id.clone(),
|
||||
&snapshot.messages,
|
||||
&snapshot.model,
|
||||
@@ -485,7 +486,9 @@ pub(super) async fn save_current_session(
|
||||
snapshot.total_tokens,
|
||||
snapshot.system_prompt.as_ref(),
|
||||
Some(snapshot.mode.as_str()),
|
||||
)
|
||||
);
|
||||
session.metadata.model_provider = snapshot.model_provider.clone();
|
||||
session
|
||||
} else {
|
||||
return Err(ApiError::internal(format!(
|
||||
"Failed to load session {existing_id}: {e}"
|
||||
@@ -494,14 +497,16 @@ pub(super) async fn save_current_session(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
crate::session_manager::create_saved_session_with_mode(
|
||||
let mut session = crate::session_manager::create_saved_session_with_mode(
|
||||
&snapshot.messages,
|
||||
&snapshot.model,
|
||||
&snapshot.workspace,
|
||||
snapshot.total_tokens,
|
||||
snapshot.system_prompt.as_ref(),
|
||||
Some(snapshot.mode.as_str()),
|
||||
)
|
||||
);
|
||||
session.metadata.model_provider = snapshot.model_provider.clone();
|
||||
session
|
||||
};
|
||||
|
||||
// Save the session.
|
||||
|
||||
@@ -113,6 +113,9 @@ pub struct SessionMetadata {
|
||||
pub total_tokens: u64,
|
||||
/// Model used for the session
|
||||
pub model: String,
|
||||
/// Provider used for the session model. Defaults for legacy saved sessions.
|
||||
#[serde(default = "default_model_provider")]
|
||||
pub model_provider: String,
|
||||
/// Workspace directory
|
||||
pub workspace: PathBuf,
|
||||
/// Optional mode label (agent/plan/etc.)
|
||||
@@ -135,6 +138,10 @@ pub struct SessionMetadata {
|
||||
pub cumulative_turn_secs: u64,
|
||||
}
|
||||
|
||||
fn default_model_provider() -> String {
|
||||
"deepseek".to_string()
|
||||
}
|
||||
|
||||
/// Cost and high-water-mark fields persisted with each session.
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
pub struct SessionCostSnapshot {
|
||||
@@ -828,6 +835,7 @@ pub fn create_saved_session_with_id_and_mode(
|
||||
message_count: messages.len(),
|
||||
total_tokens,
|
||||
model: model.to_string(),
|
||||
model_provider: default_model_provider(),
|
||||
workspace: workspace.to_path_buf(),
|
||||
mode: mode.map(str::to_string),
|
||||
cost: SessionCostSnapshot::default(),
|
||||
|
||||
@@ -5717,6 +5717,7 @@ fn build_session_snapshot(app: &App, manager: &SessionManager) -> SavedSession {
|
||||
app.system_prompt.as_ref(),
|
||||
);
|
||||
updated.metadata.model = model;
|
||||
updated.metadata.model_provider = app.api_provider.as_str().to_string();
|
||||
updated.metadata.mode = Some(app.mode.as_setting().to_string());
|
||||
app.sync_cost_to_metadata(&mut updated.metadata);
|
||||
updated.context_references = app.session_context_references.clone();
|
||||
@@ -5743,6 +5744,7 @@ fn build_session_snapshot(app: &App, manager: &SessionManager) -> SavedSession {
|
||||
Some(app.mode.as_setting()),
|
||||
)
|
||||
};
|
||||
session.metadata.model_provider = app.api_provider.as_str().to_string();
|
||||
app.sync_cost_to_metadata(&mut session.metadata);
|
||||
session.context_references = app.session_context_references.clone();
|
||||
session.artifacts = app.session_artifacts.clone();
|
||||
@@ -11028,7 +11030,7 @@ fn set_provider_auth_mode_in_memory(config: &mut Config, provider: ApiProvider,
|
||||
entry.auth_mode = Some(auth_mode);
|
||||
}
|
||||
|
||||
fn apply_loaded_session(app: &mut App, config: &Config, session: &SavedSession) -> bool {
|
||||
fn apply_loaded_session(app: &mut App, config: &mut Config, session: &SavedSession) -> bool {
|
||||
let (messages, recovered_draft) = recover_interrupted_user_tail(&session.messages);
|
||||
app.api_messages = messages;
|
||||
app.clear_history();
|
||||
@@ -11073,7 +11075,12 @@ fn apply_loaded_session(app: &mut App, config: &Config, session: &SavedSession)
|
||||
app.sync_context_references_from_session(&session.context_references, &message_to_cell);
|
||||
app.mark_history_updated();
|
||||
app.viewport.transcript_selection.clear();
|
||||
restore_loaded_session_provider(app, config, &session.metadata.model_provider);
|
||||
app.set_model_selection(session.metadata.model.clone());
|
||||
app.provider_models.insert(
|
||||
app.api_provider.as_str().to_string(),
|
||||
app.model_selection_for_persistence(),
|
||||
);
|
||||
app.update_model_compaction_budget();
|
||||
apply_workspace_runtime_state(app, config, session.metadata.workspace.clone());
|
||||
if let Some(mode) = session.metadata.mode.as_deref().and_then(AppMode::parse) {
|
||||
@@ -11136,6 +11143,27 @@ fn apply_loaded_session(app: &mut App, config: &Config, session: &SavedSession)
|
||||
recovered
|
||||
}
|
||||
|
||||
fn restore_loaded_session_provider(app: &mut App, config: &mut Config, model_provider: &str) {
|
||||
let Some(provider) = ApiProvider::parse(model_provider) else {
|
||||
return;
|
||||
};
|
||||
|
||||
app.api_provider = provider;
|
||||
config.provider = Some(provider.as_str().to_string());
|
||||
app.max_subagents = config
|
||||
.max_subagents_for_provider(provider)
|
||||
.clamp(1, crate::config::MAX_SUBAGENTS);
|
||||
app.provider_chain = provider
|
||||
.kind()
|
||||
.map(|kind| codewhale_config::ProviderChain::new(kind, &config.fallback_providers))
|
||||
.filter(|chain| chain.providers().len() > 1);
|
||||
app.last_fallback_reason = None;
|
||||
app.model_ids_passthrough = config.model_ids_pass_through();
|
||||
app.reasoning_effort = app.reasoning_effort.normalize_for_provider(provider);
|
||||
app.set_active_context_window_override(config.context_window_for_provider_config(provider));
|
||||
app.active_route_limits = app.context_window_override_limits();
|
||||
}
|
||||
|
||||
/// Derive a short display title from the API message list.
|
||||
/// Skips the `<turn_meta>` block prepended by the engine and takes the first
|
||||
/// real user-text block, truncated to 32 characters.
|
||||
|
||||
Reference in new issue
Block a user