feat(session): persist model in session metadata — ROADMAP #59

Add 'model: Option<String>' to Session struct. The model used is now
saved in the session_meta JSONL record and surfaced in resumed /status:
- JSON mode: {model: 'claude-sonnet-4-6'} instead of null
- Text mode: shows actual model instead of 'restored-session'

Model is set in build_runtime_with_plugin_state() before the runtime
is constructed, and only when not already set (preserves model through
fork/resume cycles).

Backward compatible: old sessions without a model field load cleanly
with model: None (shown as null in JSON, 'restored-session' in text).

All workspace tests pass.
This commit is contained in:
YeonGyu-Kim
2026-04-10 10:05:42 +09:00
parent 6af0189906
commit 0f34c66acd
2 changed files with 26 additions and 3 deletions

View File

@@ -96,6 +96,9 @@ pub struct Session {
pub fork: Option<SessionFork>, pub fork: Option<SessionFork>,
pub workspace_root: Option<PathBuf>, pub workspace_root: Option<PathBuf>,
pub prompt_history: Vec<SessionPromptEntry>, pub prompt_history: Vec<SessionPromptEntry>,
/// The model used in this session, persisted so resumed sessions can
/// report which model was originally used.
pub model: Option<String>,
persistence: Option<SessionPersistence>, persistence: Option<SessionPersistence>,
} }
@@ -161,6 +164,7 @@ impl Session {
fork: None, fork: None,
workspace_root: None, workspace_root: None,
prompt_history: Vec::new(), prompt_history: Vec::new(),
model: None,
persistence: None, persistence: None,
} }
} }
@@ -263,6 +267,7 @@ impl Session {
}), }),
workspace_root: self.workspace_root.clone(), workspace_root: self.workspace_root.clone(),
prompt_history: self.prompt_history.clone(), prompt_history: self.prompt_history.clone(),
model: self.model.clone(),
persistence: None, persistence: None,
} }
} }
@@ -371,6 +376,10 @@ impl Session {
.collect() .collect()
}) })
.unwrap_or_default(); .unwrap_or_default();
let model = object
.get("model")
.and_then(JsonValue::as_str)
.map(String::from);
Ok(Self { Ok(Self {
version, version,
session_id, session_id,
@@ -381,6 +390,7 @@ impl Session {
fork, fork,
workspace_root, workspace_root,
prompt_history, prompt_history,
model,
persistence: None, persistence: None,
}) })
} }
@@ -394,6 +404,7 @@ impl Session {
let mut compaction = None; let mut compaction = None;
let mut fork = None; let mut fork = None;
let mut workspace_root = None; let mut workspace_root = None;
let mut model = None;
let mut prompt_history = Vec::new(); let mut prompt_history = Vec::new();
for (line_number, raw_line) in contents.lines().enumerate() { for (line_number, raw_line) in contents.lines().enumerate() {
@@ -433,6 +444,10 @@ impl Session {
.get("workspace_root") .get("workspace_root")
.and_then(JsonValue::as_str) .and_then(JsonValue::as_str)
.map(PathBuf::from); .map(PathBuf::from);
model = object
.get("model")
.and_then(JsonValue::as_str)
.map(String::from);
} }
"message" => { "message" => {
let message_value = object.get("message").ok_or_else(|| { let message_value = object.get("message").ok_or_else(|| {
@@ -475,6 +490,7 @@ impl Session {
fork, fork,
workspace_root, workspace_root,
prompt_history, prompt_history,
model,
persistence: None, persistence: None,
}) })
} }
@@ -580,6 +596,9 @@ impl Session {
JsonValue::String(workspace_root_to_string(workspace_root)?), JsonValue::String(workspace_root_to_string(workspace_root)?),
); );
} }
if let Some(model) = &self.model {
object.insert("model".to_string(), JsonValue::String(model.clone()));
}
Ok(JsonValue::Object(object)) Ok(JsonValue::Object(object))
} }

View File

@@ -2789,7 +2789,7 @@ fn run_resume_command(
Ok(ResumeCommandOutcome { Ok(ResumeCommandOutcome {
session: session.clone(), session: session.clone(),
message: Some(format_status_report( message: Some(format_status_report(
"restored-session", session.model.as_deref().unwrap_or("restored-session"),
StatusUsage { StatusUsage {
message_count: session.messages.len(), message_count: session.messages.len(),
turns: tracker.turns(), turns: tracker.turns(),
@@ -2801,7 +2801,7 @@ fn run_resume_command(
&context, &context,
)), )),
json: Some(status_json_value( json: Some(status_json_value(
None, session.model.as_deref(),
StatusUsage { StatusUsage {
message_count: session.messages.len(), message_count: session.messages.len(),
turns: tracker.turns(), turns: tracker.turns(),
@@ -6752,7 +6752,7 @@ fn build_runtime(
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn build_runtime_with_plugin_state( fn build_runtime_with_plugin_state(
session: Session, mut session: Session,
session_id: &str, session_id: &str,
model: String, model: String,
system_prompt: Vec<String>, system_prompt: Vec<String>,
@@ -6763,6 +6763,10 @@ fn build_runtime_with_plugin_state(
progress_reporter: Option<InternalPromptProgressReporter>, progress_reporter: Option<InternalPromptProgressReporter>,
runtime_plugin_state: RuntimePluginState, runtime_plugin_state: RuntimePluginState,
) -> Result<BuiltRuntime, Box<dyn std::error::Error>> { ) -> Result<BuiltRuntime, Box<dyn std::error::Error>> {
// Persist the model in session metadata so resumed sessions can report it.
if session.model.is_none() {
session.model = Some(model.clone());
}
let RuntimePluginState { let RuntimePluginState {
feature_config, feature_config,
tool_registry, tool_registry,