fix(rollout): sort sessions by mtime first

This commit is contained in:
James Peter
2025-10-28 17:58:21 +10:00
parent 6120fbdad3
commit 9f6481a1b8
5 changed files with 118 additions and 16 deletions
+2
View File
@@ -1288,6 +1288,7 @@ dependencies = [
"dunce",
"env-flags",
"eventsource-stream",
"filetime",
"fs2",
"futures",
"htmd",
@@ -1298,6 +1299,7 @@ dependencies = [
"libc",
"maplit",
"mime_guess",
"once_cell",
"openssl-sys",
"os_info",
"portable-pty",
+1
View File
@@ -114,6 +114,7 @@ env-flags = "0.1.1"
env_logger = "0.11.5"
escargot = "0.5"
eventsource-stream = "0.2.3"
filetime = "0.2"
futures = "0.3"
icu_decimal = "2.0.0"
icu_locale_core = "2.0.0"
+2
View File
@@ -102,7 +102,9 @@ windows-sys = { version = "0.61.2", features = [
] }
[dev-dependencies]
filetime = { workspace = true }
maplit = { workspace = true }
once_cell = { workspace = true }
pretty_assertions = { workspace = true }
tokio-test = { workspace = true }
wiremock = { workspace = true }
+46 -15
View File
@@ -11,6 +11,7 @@ use time::OffsetDateTime;
use time::PrimitiveDateTime;
use time::format_description::FormatItem;
use time::macros::format_description;
use time::format_description::well_known::Rfc3339;
use uuid::Uuid;
use super::SESSIONS_SUBDIR;
@@ -47,6 +48,8 @@ pub struct ConversationItem {
pub created_at: Option<String>,
/// RFC3339 timestamp string for the most recent response in the tail, if available.
pub updated_at: Option<String>,
/// RFC3339 timestamp string for the file's last modification time, if available.
pub modified_at: Option<String>,
}
#[derive(Default)]
@@ -147,7 +150,7 @@ async fn traverse_directories_for_paths(
anchor: Option<Cursor>,
allowed_sources: &[SessionSource],
) -> io::Result<ConversationsPage> {
let mut items: Vec<ConversationItem> = Vec::with_capacity(page_size);
let mut candidates: Vec<(OffsetDateTime, OffsetDateTime, Uuid, ConversationItem)> = Vec::new();
let mut scanned_files = 0usize;
let mut anchor_passed = anchor.is_none();
let (anchor_ts, anchor_id) = match anchor {
@@ -171,7 +174,7 @@ async fn traverse_directories_for_paths(
if scanned_files >= MAX_SCAN_FILES {
break 'outer;
}
let mut day_files = collect_files(day_path, |name_str, path| {
let day_files = collect_files(day_path, |name_str, path| {
if !name_str.starts_with("rollout-") || !name_str.ends_with(".jsonl") {
return None;
}
@@ -180,11 +183,22 @@ async fn traverse_directories_for_paths(
.map(|(ts, id)| (ts, id, name_str.to_string(), path.to_path_buf()))
})
.await?;
// Stable ordering within the same second: (timestamp desc, uuid desc)
day_files.sort_by_key(|(ts, sid, _name_str, _path)| (Reverse(*ts), Reverse(*sid)));
let mut day_entries = Vec::with_capacity(day_files.len());
for (ts, sid, _name_str, path) in day_files.into_iter() {
let modified = tokio::fs::metadata(&path)
.await
.ok()
.and_then(|meta| meta.modified().ok())
.map(OffsetDateTime::from)
.unwrap_or(OffsetDateTime::UNIX_EPOCH);
day_entries.push((modified, ts, sid, path));
}
day_entries.sort_by_key(|(modified, ts, sid, _)| {
(Reverse(*modified), Reverse(*ts), Reverse(*sid))
});
for (modified, ts, sid, path) in day_entries.into_iter() {
scanned_files += 1;
if scanned_files >= MAX_SCAN_FILES && items.len() >= page_size {
if scanned_files >= MAX_SCAN_FILES {
break 'outer;
}
if !anchor_passed {
@@ -194,9 +208,6 @@ async fn traverse_directories_for_paths(
continue;
}
}
if items.len() == page_size {
break 'outer;
}
let summary = read_head_and_tail(&path, HEAD_RECORD_LIMIT, TAIL_RECORD_LIMIT)
.await
.unwrap_or_default();
@@ -216,19 +227,39 @@ async fn traverse_directories_for_paths(
..
} = summary;
updated_at = updated_at.or_else(|| created_at.clone());
items.push(ConversationItem {
path,
head,
tail,
created_at,
updated_at,
});
let modified_at = if modified == OffsetDateTime::UNIX_EPOCH {
updated_at.clone().or_else(|| created_at.clone())
} else {
modified.format(&Rfc3339).ok()
};
candidates.push((
modified,
ts,
sid,
ConversationItem {
path,
head,
tail,
created_at,
updated_at,
modified_at,
},
));
}
}
}
}
}
candidates.sort_by_key(|(modified, ts, sid, _)| {
(Reverse(*modified), Reverse(*ts), Reverse(*sid))
});
let mut items: Vec<ConversationItem> = Vec::with_capacity(page_size.min(candidates.len()));
for (_, _, _, item) in candidates.into_iter().take(page_size) {
items.push(item);
}
let next = build_next_cursor(&items);
Ok(ConversationsPage {
items,
+67 -1
View File
@@ -6,10 +6,12 @@ use std::io::BufWriter;
use std::io::Write;
use std::path::{Path, PathBuf};
use filetime::{set_file_mtime, FileTime};
use tempfile::TempDir;
use time::OffsetDateTime;
use time::PrimitiveDateTime;
use time::format_description::FormatItem;
use time::format_description::well_known::Rfc3339;
use time::macros::format_description;
use uuid::Uuid;
@@ -19,6 +21,7 @@ use crate::rollout::list::ConversationsPage;
use crate::rollout::list::Cursor;
use crate::rollout::list::get_conversation;
use crate::rollout::list::get_conversations;
use std::time::{Duration, SystemTime};
use code_protocol::models::{ContentItem, ResponseItem};
use code_protocol::ConversationId;
use code_protocol::protocol::{
@@ -426,7 +429,7 @@ async fn test_pagination_cursor() {
];
let expected_cursor1: Cursor =
serde_json::from_str(&format!("\"2025-03-04T09-00-00|{u4}\"")).unwrap();
assert_page_summary(&page1, &expected_page1_items, Some(expected_cursor1.clone()), 3);
assert_page_summary(&page1, &expected_page1_items, Some(expected_cursor1.clone()), 5);
let page2 = get_conversations(
home,
@@ -524,6 +527,69 @@ async fn test_get_conversation_contents() {
}
}
#[tokio::test]
async fn test_list_conversations_prefers_recent_mtime() {
let temp = TempDir::new().unwrap();
let home = temp.path();
let older_uuid = Uuid::from_u128(0x11);
let newer_uuid = Uuid::from_u128(0x22);
write_session_file(
home,
"2025-08-01T10-00-00",
older_uuid,
1,
Some(SessionSource::VSCode),
)
.unwrap();
write_session_file(
home,
"2025-09-01T10-00-00",
newer_uuid,
1,
Some(SessionSource::VSCode),
)
.unwrap();
let older_path = home
.join("sessions")
.join("2025")
.join("08")
.join("01")
.join(format!("rollout-2025-08-01T10-00-00-{older_uuid}.jsonl"));
let future_time = SystemTime::now() + Duration::from_secs(300);
set_file_mtime(&older_path, FileTime::from_system_time(future_time)).unwrap();
let page = get_conversations(home, 1, None, INTERACTIVE_SESSION_SOURCES)
.await
.unwrap();
let first = page.items.first().expect("expected at least one session");
assert_eq!(first.path, older_path);
let modified_at = first
.modified_at
.as_deref()
.expect("expected modified_at");
let updated_at = first
.updated_at
.as_deref()
.expect("expected updated_at");
let modified_dt = OffsetDateTime::parse(modified_at, &Rfc3339).expect("parse modified_at");
let format: &[FormatItem] =
format_description!("[year]-[month]-[day]T[hour]-[minute]-[second]");
let updated_dt = PrimitiveDateTime::parse(updated_at, format)
.expect("parse updated_at")
.assume_utc();
assert!(
modified_dt > updated_dt,
"modified_at should reflect newer filesystem mtime"
);
}
#[tokio::test]
async fn test_stable_ordering_same_second_pagination() {
let temp = TempDir::new().unwrap();