mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
fix(rollout): sort sessions by mtime first
This commit is contained in:
Generated
+2
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user