From dbc2824a3e6c80aad2c6bd92c972c934ec822634 Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Sun, 12 Apr 2026 07:49:32 +0000 Subject: [PATCH] Keep latest session selection tied to real session recency The next repo-local sweep target was ROADMAP #72: the `latest` managed-session alias could depend on filesystem mtime before the session's own persisted recency markers, which made the selection path vulnerable to coarse or misleading file timestamps. The fix promotes `updated_at_ms` into the summary/order path, keeps CLI wrappers in sync, and locks the mtime-vs-session-recency case with regression coverage. Constraint: Preserve existing managed-session storage layout while changing only the ordering signal Rejected: Keep sorting by filesystem mtime and just sleep longer in tests | hides the semantic ordering bug instead of fixing it Confidence: high Scope-risk: narrow Reversibility: clean Directive: Any future managed-session ordering change must keep runtime and CLI summary structs aligned on the same recency fields Tested: cargo fmt --all --check; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace; architect review APPROVE Not-tested: Cross-filesystem behavior where persisted session JSON cannot be read and fallback ordering uses mtime only --- ROADMAP.md | 2 + rust/crates/runtime/src/session_control.rs | 49 +++++++++++++++++++--- rust/crates/rusty-claude-cli/src/main.rs | 3 ++ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index fcf5681..c0f1812 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -515,3 +515,5 @@ Model name prefix now wins unconditionally over env-var presence. Regression tes 70. **Install-source ambiguity misleads real users** — **done (verified 2026-04-12):** repo-local Rust guidance now makes the source of truth explicit in `claw doctor` and `claw --help`, naming `ultraworkers/claw-code` as the canonical repo and warning that `cargo install claw-code` installs a deprecated stub rather than the `claw` binary. Regression coverage locks both the new doctor JSON check and the help-text warning. **Original filing below.** 71. **Wrong-task prompt receipt is not detected before execution** — **done (verified 2026-04-12):** worker boot prompt dispatch now accepts an optional structured `task_receipt` (`repo`, `task_kind`, `source_surface`, `expected_artifacts`, `objective_preview`) and treats mismatched visible prompt context as a `WrongTask` prompt-delivery failure before execution continues. The prompt-delivery payload now records `observed_prompt_preview` plus the expected receipt, and regression coverage locks both the existing shell/wrong-target paths and the new KakaoTalk-style wrong-task mismatch case. **Original filing below.** + +72. **`latest` managed-session selection depends on filesystem mtime before semantic session recency** — **done (verified 2026-04-12):** managed-session summaries now carry `updated_at_ms`, `SessionStore::list_sessions()` sorts by semantic recency before filesystem mtime, and regression coverage locks the case where `latest` must prefer the newer session payload even when file mtimes point the other way. The CLI session-summary wrapper now stays in sync with the runtime field so `latest` resolution uses the same ordering signal everywhere. **Original filing below.** diff --git a/rust/crates/runtime/src/session_control.rs b/rust/crates/runtime/src/session_control.rs index eaf4de4..1c3654c 100644 --- a/rust/crates/runtime/src/session_control.rs +++ b/rust/crates/runtime/src/session_control.rs @@ -144,12 +144,7 @@ impl SessionStore { if let Some(legacy_root) = self.legacy_sessions_root() { self.collect_sessions_from_dir(&legacy_root, &mut sessions)?; } - sessions.sort_by(|left, right| { - right - .modified_epoch_millis - .cmp(&left.modified_epoch_millis) - .then_with(|| right.id.cmp(&left.id)) - }); + sort_managed_sessions(&mut sessions); Ok(sessions) } @@ -260,6 +255,7 @@ impl SessionStore { ManagedSessionSummary { id: session.session_id, path, + updated_at_ms: session.updated_at_ms, modified_epoch_millis, message_count: session.messages.len(), parent_session_id: session @@ -279,6 +275,7 @@ impl SessionStore { .unwrap_or("unknown") .to_string(), path, + updated_at_ms: 0, modified_epoch_millis, message_count: 0, parent_session_id: None, @@ -322,12 +319,23 @@ pub struct SessionHandle { pub struct ManagedSessionSummary { pub id: String, pub path: PathBuf, + pub updated_at_ms: u64, pub modified_epoch_millis: u128, pub message_count: usize, pub parent_session_id: Option, pub branch_name: Option, } +fn sort_managed_sessions(sessions: &mut [ManagedSessionSummary]) { + sessions.sort_by(|left, right| { + right + .updated_at_ms + .cmp(&left.updated_at_ms) + .then_with(|| right.modified_epoch_millis.cmp(&left.modified_epoch_millis)) + .then_with(|| right.id.cmp(&left.id)) + }); +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct LoadedManagedSession { pub handle: SessionHandle, @@ -598,6 +606,35 @@ mod tests { .expect("session summary should exist") } + #[test] + fn latest_session_prefers_semantic_updated_at_over_file_mtime() { + let mut sessions = vec![ + ManagedSessionSummary { + id: "older-file-newer-session".to_string(), + path: PathBuf::from("/tmp/older"), + updated_at_ms: 200, + modified_epoch_millis: 100, + message_count: 2, + parent_session_id: None, + branch_name: None, + }, + ManagedSessionSummary { + id: "newer-file-older-session".to_string(), + path: PathBuf::from("/tmp/newer"), + updated_at_ms: 100, + modified_epoch_millis: 200, + message_count: 1, + parent_session_id: None, + branch_name: None, + }, + ]; + + crate::session_control::sort_managed_sessions(&mut sessions); + + assert_eq!(sessions[0].id, "older-file-newer-session"); + assert_eq!(sessions[1].id, "newer-file-older-session"); + } + #[test] fn creates_and_lists_managed_sessions() { // given diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 4bdf873..32d8177 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -3122,6 +3122,7 @@ struct SessionHandle { struct ManagedSessionSummary { id: String, path: PathBuf, + updated_at_ms: u64, modified_epoch_millis: u128, message_count: usize, parent_session_id: Option, @@ -4711,6 +4712,7 @@ fn list_managed_sessions() -> Result, Box Result