Moving updated-at timestamps to unique millisecond times (#17489)

To allow the ability to have guaranteed-unique cursors, we make two
important updates:
* Add new updated_at_ms and created_at_ms columns that are in
millisecond precision
* Guarantee uniqueness -- if multiple items are inserted at the same
millisecond, bump the new one by one millisecond until it becomes unique

This lets us use single-number cursors for forwards and backwards paging
through resultsets and guarantee that the cursor is a fixed point to do
(timestamp > cursor) and get new items only.

This updated implementation is backwards-compatible since multiple
appservers can be running and won't handle the previous method well.
This commit is contained in:
David de Regt
2026-04-14 11:55:34 -04:00
committed by GitHub
parent 61fe23159e
commit 4f2fc3e3fa
13 changed files with 470 additions and 167 deletions
+8 -7
View File
@@ -71,7 +71,6 @@ pub struct ThreadItem {
/// created_at comes from the filename timestamp with second precision.
pub created_at: Option<String>,
/// RFC3339 timestamp string for the most recent update (from file mtime).
/// updated_at is truncated to second precision to match created_at.
pub updated_at: Option<String>,
}
@@ -292,7 +291,10 @@ impl<'de> serde::Deserialize<'de> for Cursor {
impl From<codex_state::Anchor> for Cursor {
fn from(anchor: codex_state::Anchor) -> Self {
let ts = OffsetDateTime::from_unix_timestamp(anchor.ts.timestamp())
let ts = anchor
.ts
.timestamp_nanos_opt()
.and_then(|nanos| OffsetDateTime::from_unix_timestamp_nanos(nanos as i128).ok())
.unwrap_or(OffsetDateTime::UNIX_EPOCH);
Self::new(ts, anchor.id)
}
@@ -1156,17 +1158,16 @@ async fn file_modified_time(path: &Path) -> io::Result<Option<OffsetDateTime>> {
return Ok(None);
};
let dt = OffsetDateTime::from(modified);
// Truncate to seconds so ordering and cursor comparisons align with the
// cursor timestamp format (which exposes seconds), keeping pagination stable.
Ok(truncate_to_seconds(dt))
Ok(truncate_to_millis(dt))
}
fn format_rfc3339(dt: OffsetDateTime) -> Option<String> {
dt.format(&Rfc3339).ok()
}
fn truncate_to_seconds(dt: OffsetDateTime) -> Option<OffsetDateTime> {
dt.replace_nanosecond(0).ok()
fn truncate_to_millis(dt: OffsetDateTime) -> Option<OffsetDateTime> {
let millis_nanos = (dt.nanosecond() / 1_000_000) * 1_000_000;
dt.replace_nanosecond(millis_nanos).ok()
}
async fn find_thread_path_by_id_str_in_subdir(
+2 -2
View File
@@ -371,7 +371,7 @@ fn backfill_watermark_for_path(codex_home: &Path, path: &Path) -> String {
async fn file_modified_time_utc(path: &Path) -> Option<DateTime<Utc>> {
let modified = tokio::fs::metadata(path).await.ok()?.modified().ok()?;
let updated_at: DateTime<Utc> = modified.into();
updated_at.with_nanosecond(0)
Some(updated_at)
}
fn parse_timestamp_to_utc(ts: &str) -> Option<DateTime<Utc>> {
@@ -381,7 +381,7 @@ fn parse_timestamp_to_utc(ts: &str) -> Option<DateTime<Utc>> {
return dt.with_nanosecond(0);
}
if let Ok(dt) = DateTime::parse_from_rfc3339(ts) {
return dt.with_timezone(&Utc).with_nanosecond(0);
return Some(dt.with_timezone(&Utc));
}
None
}
+1 -1
View File
@@ -1252,7 +1252,7 @@ impl From<codex_state::ThreadsPage> for ThreadsPage {
model_provider: Some(item.model_provider),
cli_version: Some(item.cli_version),
created_at: Some(item.created_at.to_rfc3339_opts(SecondsFormat::Secs, true)),
updated_at: Some(item.updated_at.to_rfc3339_opts(SecondsFormat::Secs, true)),
updated_at: Some(item.updated_at.to_rfc3339_opts(SecondsFormat::Millis, true)),
})
.collect();
Self {
+1 -3
View File
@@ -5,7 +5,6 @@ use crate::list::ThreadSortKey;
use crate::metadata;
use chrono::DateTime;
use chrono::NaiveDateTime;
use chrono::Timelike;
use chrono::Utc;
use codex_protocol::ThreadId;
use codex_protocol::dynamic_tools::DynamicToolSpec;
@@ -131,8 +130,7 @@ fn cursor_to_anchor(cursor: Option<&Cursor>) -> Option<codex_state::Anchor> {
dt.with_timezone(&Utc)
} else {
return None;
}
.with_nanosecond(0)?;
};
Some(codex_state::Anchor { ts, id })
}