Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions code-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions code-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions code-rs/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
61 changes: 46 additions & 15 deletions code-rs/core/src/rollout/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
}
Expand All @@ -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 {
Expand All @@ -194,9 +208,6 @@ async fn traverse_directories_for_paths(
continue;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update cursor logic to include modification time ordering

The listing now orders candidates by (modified, ts, uuid) (see the new sort above), but the pagination cursor still records only the filename timestamp and UUID and this comparison continues to skip entries solely on ts/sid. If the newest modified time belongs to an older timestamp (e.g., an old session file touched recently), get_conversations(page_size=1) will return that file first, and on the next call every session whose timestamp is later than the cursor is filtered out here and never surfaces. This breaks next_cursor pagination whenever filesystem mtimes reorder sessions beyond their filename timestamps. The cursor and skip logic need to include the modification time to maintain a stable order.

Useful? React with 👍 / 👎.

}
}
if items.len() == page_size {
break 'outer;
}
let summary = read_head_and_tail(&path, HEAD_RECORD_LIMIT, TAIL_RECORD_LIMIT)
.await
.unwrap_or_default();
Expand All @@ -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,
Expand Down
68 changes: 67 additions & 1 deletion code-rs/core/src/rollout/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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::{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down
Loading