[codex] Restore thread recency with compatible migration history (#28671)

## Summary

- Revert #28655, restoring the thread `recencyAt` behavior introduced by
#27910.
- Move `threads_recency_at` to migration 0039 so it no longer collides
with `external_agent_config_imports` at version 0038.
- Repair databases that already applied the recency migration as version
38 by moving the matching migration-history row to version 39 before
SQLx validation. The current version-38 migration can then apply
normally.

## Validation

- `just test -p codex-state
migrations::tests::repairs_recency_migration_that_was_applied_as_version_38`
- `just test -p codex-state -p codex-rollout -p codex-thread-store -p
codex-app-server-protocol -p codex-tui`: 3,439 passed; six TUI tests
could not open the machine's existing read-only incident database at
`~/.codex/sqlite/state_5.sqlite`.
- `just fix -p codex-state`
- `just fmt`
- Verified that state migration versions are unique.
This commit is contained in:
Jeremy Rose
2026-06-17 18:52:18 +00:00
committed by GitHub
parent d9e0551564
commit 7dc7096ae1
67 changed files with 1274 additions and 100 deletions
+25 -1
View File
@@ -18,6 +18,8 @@ pub enum SortKey {
CreatedAt,
/// Sort by the thread's last update timestamp.
UpdatedAt,
/// Sort by the thread's product recency timestamp.
RecencyAt,
}
/// Sort direction to use when listing threads.
@@ -32,6 +34,8 @@ pub enum SortDirection {
pub struct Anchor {
/// The timestamp component of the anchor.
pub ts: DateTime<Utc>,
/// The thread ID component used to disambiguate equal recency timestamps.
pub id: Option<ThreadId>,
}
/// A single page of thread metadata results.
@@ -67,6 +71,8 @@ pub struct ThreadMetadata {
pub created_at: DateTime<Utc>,
/// The last update timestamp.
pub updated_at: DateTime<Utc>,
/// The product recency timestamp.
pub recency_at: DateTime<Utc>,
/// The session source (stringified enum).
pub source: String,
/// Optional analytics source classification for this thread.
@@ -120,6 +126,8 @@ pub struct ThreadMetadataBuilder {
pub created_at: DateTime<Utc>,
/// The last update timestamp, if known.
pub updated_at: Option<DateTime<Utc>>,
/// The product recency timestamp, if known.
pub recency_at: Option<DateTime<Utc>>,
/// The session source.
pub source: SessionSource,
/// Optional analytics source classification for this thread.
@@ -163,6 +171,7 @@ impl ThreadMetadataBuilder {
rollout_path,
created_at,
updated_at: None,
recency_at: None,
source,
thread_source: None,
agent_nickname: None,
@@ -190,11 +199,16 @@ impl ThreadMetadataBuilder {
.updated_at
.map(canonicalize_datetime)
.unwrap_or(created_at);
let recency_at = self
.recency_at
.map(canonicalize_datetime)
.unwrap_or(updated_at);
ThreadMetadata {
id: self.id,
rollout_path: self.rollout_path.clone(),
created_at,
updated_at,
recency_at,
source,
thread_source: self.thread_source.clone(),
agent_nickname: self.agent_nickname.clone(),
@@ -340,6 +354,7 @@ pub(crate) struct ThreadRow {
rollout_path: String,
created_at: i64,
updated_at: i64,
recency_at: i64,
source: String,
thread_source: Option<String>,
agent_nickname: Option<String>,
@@ -369,6 +384,7 @@ impl ThreadRow {
rollout_path: row.try_get("rollout_path")?,
created_at: row.try_get("created_at")?,
updated_at: row.try_get("updated_at")?,
recency_at: row.try_get("recency_at")?,
source: row.try_get("source")?,
thread_source: row.try_get("thread_source")?,
agent_nickname: row.try_get("agent_nickname")?,
@@ -402,6 +418,7 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
rollout_path,
created_at,
updated_at,
recency_at,
source,
thread_source,
agent_nickname,
@@ -432,6 +449,7 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
rollout_path: PathBuf::from(rollout_path),
created_at: epoch_millis_to_datetime(created_at)?,
updated_at: epoch_millis_to_datetime(updated_at)?,
recency_at: epoch_millis_to_datetime(recency_at)?,
source,
thread_source,
agent_nickname,
@@ -461,8 +479,12 @@ pub(crate) fn anchor_from_item(item: &ThreadMetadata, sort_key: SortKey) -> Opti
let ts = match sort_key {
SortKey::CreatedAt => item.created_at,
SortKey::UpdatedAt => item.updated_at,
SortKey::RecencyAt => item.recency_at,
};
Some(Anchor { ts })
Some(Anchor {
ts,
id: (sort_key == SortKey::RecencyAt).then_some(item.id),
})
}
pub(crate) fn datetime_to_epoch_millis(dt: DateTime<Utc>) -> i64 {
@@ -519,6 +541,7 @@ mod tests {
rollout_path: "/tmp/rollout-123.jsonl".to_string(),
created_at: 1_700_000_000,
updated_at: 1_700_000_100,
recency_at: 1_700_000_100,
source: "cli".to_string(),
thread_source: None,
agent_nickname: None,
@@ -549,6 +572,7 @@ mod tests {
rollout_path: PathBuf::from("/tmp/rollout-123.jsonl"),
created_at: DateTime::<Utc>::from_timestamp(1_700_000_000, 0).expect("timestamp"),
updated_at: DateTime::<Utc>::from_timestamp(1_700_000_100, 0).expect("timestamp"),
recency_at: DateTime::<Utc>::from_timestamp(1_700_000_100, 0).expect("timestamp"),
source: "cli".to_string(),
thread_source: None,
agent_nickname: None,