From 2e349495075751f6e96c1a6cd490c88401de2910 Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Sun, 12 Apr 2026 10:51:19 +0000 Subject: [PATCH] Keep latest-session timestamps increasing under tight loops The next repo-local sweep target was ROADMAP #73: repeated backlog sweeps exposed that session writes could share the same wall-clock millisecond, which made semantic recency fragile and forced the resume-latest regression to sleep between saves. The fix makes session timestamps monotonic within the process and removes the timing hack from the test so latest-session selection stays stable under tight loops. Constraint: Preserve the existing session file format while changing only the timestamp source semantics Rejected: Keep the sleep-based test workaround | hides the real ordering hazard instead of fixing timestamp generation Confidence: high Scope-risk: narrow Reversibility: clean Directive: Any future session-recency logic must keep `current_time_millis`, ordering tests, and latest-session expectations aligned Tested: cargo fmt --all --check; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace; architect review APPROVE Not-tested: Cross-process monotonicity when multiple binaries write sessions concurrently --- ROADMAP.md | 1 + rust/crates/runtime/src/session.rs | 36 ++++++++++++++++--- .../tests/resume_slash_commands.rs | 3 -- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 5775a70..63925ca 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -517,3 +517,4 @@ Model name prefix now wins unconditionally over env-var presence. Regression tes 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.** +73. **Session timestamps are not monotonic enough for latest-session ordering under tight loops** — **done (verified 2026-04-12):** runtime session timestamps now use a process-local monotonic millisecond source, so back-to-back saves still produce increasing `updated_at_ms` even when the wall clock does not advance. The temporary sleep hack was removed from the resume-latest regression, and fresh workspace verification stayed green with the semantic-recency ordering path from #72. **Original filing below.** diff --git a/rust/crates/runtime/src/session.rs b/rust/crates/runtime/src/session.rs index e832495..b97378e 100644 --- a/rust/crates/runtime/src/session.rs +++ b/rust/crates/runtime/src/session.rs @@ -13,6 +13,7 @@ const SESSION_VERSION: u32 = 1; const ROTATE_AFTER_BYTES: u64 = 256 * 1024; const MAX_ROTATED_FILES: usize = 3; static SESSION_ID_COUNTER: AtomicU64 = AtomicU64::new(0); +static LAST_TIMESTAMP_MS: AtomicU64 = AtomicU64::new(0); /// Speaker role associated with a persisted conversation message. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -1030,10 +1031,27 @@ fn normalize_optional_string(value: Option) -> Option { } fn current_time_millis() -> u64 { - SystemTime::now() + let wall_clock = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)) - .unwrap_or_default() + .unwrap_or_default(); + + let mut candidate = wall_clock; + loop { + let previous = LAST_TIMESTAMP_MS.load(Ordering::Relaxed); + if candidate <= previous { + candidate = previous.saturating_add(1); + } + match LAST_TIMESTAMP_MS.compare_exchange( + previous, + candidate, + Ordering::SeqCst, + Ordering::SeqCst, + ) { + Ok(_) => return candidate, + Err(actual) => candidate = actual.saturating_add(1), + } + } } fn generate_session_id() -> String { @@ -1125,8 +1143,8 @@ fn cleanup_rotated_logs(path: &Path) -> Result<(), SessionError> { #[cfg(test)] mod tests { use super::{ - cleanup_rotated_logs, rotate_session_file_if_needed, ContentBlock, ConversationMessage, - MessageRole, Session, SessionFork, + cleanup_rotated_logs, current_time_millis, rotate_session_file_if_needed, ContentBlock, + ConversationMessage, MessageRole, Session, SessionFork, }; use crate::json::JsonValue; use crate::usage::TokenUsage; @@ -1134,6 +1152,16 @@ mod tests { use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; + #[test] + fn session_timestamps_are_monotonic_under_tight_loops() { + let first = current_time_millis(); + let second = current_time_millis(); + let third = current_time_millis(); + + assert!(first < second); + assert!(second < third); + } + #[test] fn persists_and_restores_session_jsonl() { let mut session = Session::new(); diff --git a/rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs b/rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs index 53f556c..b620449 100644 --- a/rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs +++ b/rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs @@ -3,8 +3,6 @@ use std::path::Path; use std::path::PathBuf; use std::process::{Command, Output}; use std::sync::atomic::{AtomicU64, Ordering}; -use std::thread; -use std::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; use runtime::ContentBlock; @@ -193,7 +191,6 @@ fn resume_latest_restores_the_most_recent_managed_session() { older .save_to_path(&older_path) .expect("older session should persist"); - thread::sleep(Duration::from_millis(2)); let mut newer = workspace_session(&project_dir).with_persistence_path(&newer_path); newer