Preserve thread model settings across resume and revert (#55923)

## Why

Resuming a thread can replace saved model settings with global defaults. A selected service tier can also be lost when the current model does not support it.

## What changed

- Restore saved service tier, reasoning summary, and personality on resume unless the caller explicitly overrides them, respecting thread ownership of settings snapshots.
- Retain the selected service tier for persistence, settings notifications, and resume comparisons while filtering the effective tier against the current model's capabilities.
- Carry the runtime model, service tier, and personality into thread revert reconstruction.
- Persist initial thread settings before user input during MCP startup so an interrupted first turn retains its settings checkpoint.

## Testing

Extend regression coverage for cold resume and explicit overrides, unsupported service tiers, settings preservation during revert, and settings checkpoints before user input when MCP startup is interrupted.

GitOrigin-RevId: e5ec012a5678048c5eedd443e524f38c142bf9fd
This commit is contained in:
Eric Traut
2026-10-21 15:34:43 +00:00
committed by copyberry
parent a6839a4812
commit 430a16799d
16 changed files with 299 additions and 63 deletions
@@ -5,6 +5,7 @@ mod daemon_continuation;
mod daemon_snapshot;
use super::persisted_resume_settings::PersistedResumeSettings;
use super::persisted_resume_settings::latest_persisted_model_settings;
use super::persisted_resume_settings::latest_persisted_resume_settings;
use super::thread_enrichment::enrich_loaded_threads;
use super::thread_fork_goal::inherit_thread_goal_snapshot;
@@ -120,6 +121,7 @@ struct ThreadRevertRuntimeSnapshot {
fn collect_resume_override_mismatches(
request: &ThreadResumeParams,
config_snapshot: &ThreadConfigSnapshot,
selected_service_tier: Option<&str>,
) -> Vec<String> {
let mut mismatch_details = Vec::new();
@@ -140,7 +142,7 @@ fn collect_resume_override_mismatches(
));
}
if let Some(requested_service_tier) = request.service_tier.as_ref()
&& requested_service_tier != &config_snapshot.service_tier
&& Some(requested_service_tier.as_deref().unwrap_or("default")) != selected_service_tier
{
mismatch_details.push(format!(
"service_tier requested={requested_service_tier:?} active={:?}",
@@ -2405,10 +2407,17 @@ impl ThreadRequestProcessor {
app_server_client_version: Option<String>,
) -> Result<ThreadRevertResponse, JSONRPCErrorError> {
let ThreadRevertRuntimeSnapshot {
config,
mut config,
settings,
client_mcp_extensions,
} = runtime_snapshot;
if let Some(collaboration_mode) = settings.collaboration_mode.as_ref() {
config.model = Some(collaboration_mode.model().to_string());
}
if let Some(service_tier) = settings.service_tier.as_ref() {
config.service_tier.clone_from(service_tier);
}
config.personality = settings.personality;
let thread_id_string = thread_id.to_string();
let stored_thread = self
.read_stored_thread_for_resume(
@@ -4186,6 +4195,17 @@ impl ThreadRequestProcessor {
}
let has_explicit_model_resume_override =
has_model_resume_override(request_overrides.as_ref(), &typesafe_overrides);
let persisted_model_settings = latest_persisted_model_settings(&thread_history);
let has_override = |key| {
request_overrides
.as_ref()
.is_some_and(|overrides| overrides.contains_key(key))
};
let restore_service_tier =
typesafe_overrides.service_tier.is_none() && !has_override("service_tier");
let restore_summary = !has_override("model_reasoning_summary");
let restore_personality =
typesafe_overrides.personality.is_none() && !has_override("personality");
typesafe_overrides.persisted_permission_profile = history_settings
.filter(|settings| {
settings.active_permission_profile.is_none()
@@ -4249,7 +4269,17 @@ impl ThreadRequestProcessor {
if clear_reasoning_effort {
config.model_reasoning_effort = None;
}
if let Some(settings) = persisted_model_settings {
if restore_service_tier {
config.service_tier.clone_from(&settings.service_tier);
}
if restore_summary {
config.model_reasoning_summary = settings.reasoning_summary;
}
if restore_personality {
config.personality = settings.personality;
}
}
let response_history = thread_history.clone();
match self
@@ -4616,8 +4646,13 @@ impl ThreadRequestProcessor {
active_path.display()
)));
}
let config_snapshot = existing_thread.config_snapshot().await;
let mismatch_details = collect_resume_override_mismatches(params, &config_snapshot);
let (config_snapshot, selected_service_tier) =
existing_thread.resume_config_snapshot().await;
let mismatch_details = collect_resume_override_mismatches(
params,
&config_snapshot,
selected_service_tier.as_deref(),
);
if !mismatch_details.is_empty() {
let has_subscribers = !self
.thread_state_manager