diff --git a/rust/crates/runtime/src/session.rs b/rust/crates/runtime/src/session.rs index 2a8d1e60..0cc32c03 100644 --- a/rust/crates/runtime/src/session.rs +++ b/rust/crates/runtime/src/session.rs @@ -413,6 +413,7 @@ impl Session { .get("created_at_ms") .map(|value| required_u64_from_value(value, "created_at_ms")) .transpose()? + .or_else(|| parse_created_at_ms_from_session_id(&session_id)) .unwrap_or(now); let updated_at_ms = object .get("updated_at_ms") @@ -500,7 +501,10 @@ impl Session { "session_meta" => { version = required_u32(object, "version")?; session_id = Some(required_string(object, "session_id")?); - created_at_ms = Some(required_u64(object, "created_at_ms")?); + created_at_ms = object + .get("created_at_ms") + .map(|value| required_u64_from_value(value, "created_at_ms")) + .transpose()?; updated_at_ms = Some(required_u64(object, "updated_at_ms")?); fork = object.get("fork").map(SessionFork::from_json).transpose()?; workspace_root = object @@ -543,11 +547,15 @@ impl Session { } let now = current_time_millis(); + let session_id = session_id.unwrap_or_else(generate_session_id); + let created_at_ms = created_at_ms + .or_else(|| parse_created_at_ms_from_session_id(&session_id)) + .unwrap_or(now); Ok(Self { version, - session_id: session_id.unwrap_or_else(generate_session_id), - created_at_ms: created_at_ms.unwrap_or(now), - updated_at_ms: updated_at_ms.unwrap_or(created_at_ms.unwrap_or(now)), + session_id, + created_at_ms, + updated_at_ms: updated_at_ms.unwrap_or(created_at_ms), messages, compaction, fork, @@ -1291,6 +1299,15 @@ fn current_time_millis() -> u64 { } } +pub(crate) fn parse_created_at_ms_from_session_id(session_id: &str) -> Option { + let timestamp_and_suffix = session_id.strip_prefix("session-")?; + let (timestamp, suffix) = timestamp_and_suffix.split_once('-')?; + if suffix.is_empty() { + return None; + } + timestamp.parse::().ok() +} + fn generate_session_id() -> String { let millis = current_time_millis(); let counter = SESSION_ID_COUNTER.fetch_add(1, Ordering::Relaxed); @@ -1380,8 +1397,9 @@ fn cleanup_rotated_logs(path: &Path) -> Result<(), SessionError> { #[cfg(test)] mod tests { use super::{ - cleanup_rotated_logs, current_time_millis, rotate_session_file_if_needed, ContentBlock, - ConversationMessage, MessageRole, Session, SessionFork, + cleanup_rotated_logs, current_time_millis, parse_created_at_ms_from_session_id, + rotate_session_file_if_needed, ContentBlock, ConversationMessage, MessageRole, Session, + SessionFork, }; use crate::json::JsonValue; use crate::usage::TokenUsage; @@ -1502,6 +1520,44 @@ mod tests { assert!(!restored.session_id.is_empty()); } + #[test] + fn created_at_parser_requires_full_session_id_shape() { + assert_eq!( + parse_created_at_ms_from_session_id("session-1743724800123-0"), + Some(1_743_724_800_123) + ); + assert_eq!( + parse_created_at_ms_from_session_id("session-1743724800123"), + None + ); + assert_eq!( + parse_created_at_ms_from_session_id("session-1743724800123-"), + None + ); + assert_eq!( + parse_created_at_ms_from_session_id("other-1743724800123-0"), + None + ); + } + + #[test] + fn loads_legacy_jsonl_created_at_from_session_id_when_meta_omits_it() { + let path = temp_session_path("legacy-jsonl-created-at"); + fs::write( + &path, + r#"{"type":"session_meta","version":3,"session_id":"session-1743724800123-0","updated_at_ms":1743724800456} +"#, + ) + .expect("legacy jsonl should write"); + + let restored = Session::load_from_path(&path).expect("legacy jsonl should load"); + fs::remove_file(&path).expect("temp file should be removable"); + + assert_eq!(restored.session_id, "session-1743724800123-0"); + assert_eq!(restored.created_at_ms, 1_743_724_800_123); + assert_eq!(restored.updated_at_ms, 1_743_724_800_456); + } + #[test] fn appends_messages_to_persisted_jsonl_session() { let path = temp_session_path("append"); diff --git a/rust/crates/runtime/src/session_control.rs b/rust/crates/runtime/src/session_control.rs index 362a0b54..e6c3f6c0 100644 --- a/rust/crates/runtime/src/session_control.rs +++ b/rust/crates/runtime/src/session_control.rs @@ -5,7 +5,7 @@ use std::fs; use std::path::{Path, PathBuf}; use std::time::UNIX_EPOCH; -use crate::session::{Session, SessionError}; +use crate::session::{parse_created_at_ms_from_session_id, Session, SessionError}; /// Per-worktree session store that namespaces on-disk session files by /// workspace fingerprint so that parallel `opencode serve` instances never @@ -345,6 +345,9 @@ impl SessionStore { .and_then(|time| time.duration_since(UNIX_EPOCH).ok()) .map(|duration| duration.as_millis()) .unwrap_or_default(); + let fallback_id = session_id_from_path(&path).unwrap_or_else(|| "unknown".to_string()); + let fallback_created_at_ms = + parse_created_at_ms_from_session_id(&fallback_id).unwrap_or(0); let summary = match Session::load_from_path(&path) { Ok(session) => { if self.validate_loaded_session(&path, &session).is_err() { @@ -353,6 +356,7 @@ impl SessionStore { ManagedSessionSummary { id: session.session_id, path, + created_at_ms: session.created_at_ms, updated_at_ms: session.updated_at_ms, modified_epoch_millis, message_count: session.messages.len(), @@ -367,12 +371,9 @@ impl SessionStore { } } Err(_) => ManagedSessionSummary { - id: path - .file_stem() - .and_then(|value| value.to_str()) - .unwrap_or("unknown") - .to_string(), + id: fallback_id, path, + created_at_ms: fallback_created_at_ms, updated_at_ms: 0, modified_epoch_millis, message_count: 0, @@ -409,10 +410,14 @@ impl SessionStore { .and_then(|time| time.duration_since(UNIX_EPOCH).ok()) .map(|duration| duration.as_millis()) .unwrap_or_default(); + let fallback_id = session_id_from_path(&path).unwrap_or_else(|| "unknown".to_string()); + let fallback_created_at_ms = + parse_created_at_ms_from_session_id(&fallback_id).unwrap_or(0); let summary = match Session::load_from_path(&path) { Ok(session) => ManagedSessionSummary { id: session.session_id, path, + created_at_ms: session.created_at_ms, updated_at_ms: session.updated_at_ms, modified_epoch_millis, message_count: session.messages.len(), @@ -426,12 +431,9 @@ impl SessionStore { .and_then(|fork| fork.branch_name.clone()), }, Err(_) => ManagedSessionSummary { - id: path - .file_stem() - .and_then(|value| value.to_str()) - .unwrap_or("unknown") - .to_string(), + id: fallback_id, path, + created_at_ms: fallback_created_at_ms, updated_at_ms: 0, modified_epoch_millis, message_count: 0, @@ -483,6 +485,7 @@ pub struct SessionHandle { pub struct ManagedSessionSummary { pub id: String, pub path: PathBuf, + pub created_at_ms: u64, pub updated_at_ms: u64, pub modified_epoch_millis: u128, pub message_count: usize, @@ -810,6 +813,7 @@ mod tests { ManagedSessionSummary { id: "older-file-newer-session".to_string(), path: PathBuf::from("/tmp/older"), + created_at_ms: 100, updated_at_ms: 200, modified_epoch_millis: 100, message_count: 2, @@ -819,6 +823,7 @@ mod tests { ManagedSessionSummary { id: "newer-file-older-session".to_string(), path: PathBuf::from("/tmp/newer"), + created_at_ms: 50, updated_at_ms: 100, modified_epoch_millis: 200, message_count: 1, diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 97cece06..7e70ea14 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -3960,18 +3960,7 @@ fn run_resume_command( let session_list_outcome = || -> Result> { let sessions = list_managed_sessions().unwrap_or_default(); let session_ids: Vec = sessions.iter().map(|s| s.id.clone()).collect(); - let session_details: Vec = sessions - .iter() - .map(|session| { - serde_json::json!({ - "id": session.id, - "path": session.path.display().to_string(), - "message_count": session.message_count, - "updated_at_ms": session.updated_at_ms, - "lifecycle": session.lifecycle.json_value(), - }) - }) - .collect(); + let session_details = session_details_json(&sessions); let active_id = session.session_id.clone(); let text = render_session_list(&active_id).unwrap_or_else(|e| format!("error: {e}")); Ok(ResumeCommandOutcome { @@ -4550,6 +4539,7 @@ struct SessionHandle { struct ManagedSessionSummary { id: String, path: PathBuf, + created_at_ms: u64, updated_at_ms: u64, modified_epoch_millis: u128, message_count: usize, @@ -6367,6 +6357,7 @@ fn list_managed_sessions() -> Result, Box Result Vec