mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Add thread recencyAt for sidebar ordering (#27910)
## Summary Add a server-owned `recencyAt` timestamp and `recency_at` thread-list sort key for product recency ordering while preserving the existing meaning of `updatedAt` as the latest persisted thread mutation. This is the server-side alternative to #27697. Rather than narrowing `updatedAt`, clients can sort the sidebar by `recency_at` and continue treating `updatedAt` as mutation time. Paired Codex Apps PR: [openai/openai#1024599](https://github.com/openai/openai/pull/1024599) ## Contract - `recencyAt` initializes when a thread is created. - A turn start advances `recencyAt` monotonically. - Commentary, agent output, tool results, token/accounting updates, turn completion, archive, unarchive, resume, and generic metadata writes do not advance it. - `updatedAt` retains its existing behavior and continues to advance for persisted thread mutations. - Current servers populate `recencyAt`; the response field is optional in generated TypeScript so clients connected to older servers can fall back to `updatedAt`. - Filesystem-only fallback uses existing updated/mtime ordering when SQLite is unavailable. ## Persistence and compatibility Migration 0038 adds second- and millisecond-precision recency columns, backfills them from the existing updated timestamp, creates list indexes, and includes an insert trigger so older binaries writing to a migrated database seed recency without causing later mutations to advance it. Generic metadata upserts preserve existing recency values. Turn-start updates use a dedicated monotonic touch, and process-local allocation keeps millisecond cursor values unique. State DB list, search, read, filtered-list repair, rollout fallback propagation, and app-server conversions all carry the new field. ## API `Thread` responses include: ```ts recencyAt?: number ``` `thread/list` and `thread/search` accept: ```json { "sortKey": "recency_at" } ``` Generated TypeScript and JSON schemas are included. ## Validation - `just test -p codex-state` — 146 passed - `just test -p codex-rollout` — 69 passed - `just test -p codex-thread-store` — 81 passed - `just test -p codex-app-server-protocol` — 231 passed - Focused app-server list ordering, response mapping, archive/unarchive, and resume lifecycle tests passed - Scoped `just fix` for state, rollout, thread-store, app-server-protocol, and app-server - `just fmt` - `git diff --check` - Independent correctness, simplicity, elegance, security, and test-quality reviews; actionable ordering, lifecycle, query-projection, and timestamp-uniqueness findings were addressed
This commit is contained in:
1 parent
f0cb96bcb1
commit
fac3158c2a
67 files changed
+1153
-99
No files matched your search
@@ -612,6 +612,7 @@ async fn test_list_conversations_latest_first() {
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-01-03T12-00-00".into()),
|
||||
recency_at: updated_times.first().cloned().flatten(),
|
||||
updated_at: updated_times.first().cloned().flatten(),
|
||||
},
|
||||
ThreadItem {
|
||||
@@ -630,6 +631,7 @@ async fn test_list_conversations_latest_first() {
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-01-02T12-00-00".into()),
|
||||
recency_at: updated_times.get(1).cloned().flatten(),
|
||||
updated_at: updated_times.get(1).cloned().flatten(),
|
||||
},
|
||||
ThreadItem {
|
||||
@@ -648,6 +650,7 @@ async fn test_list_conversations_latest_first() {
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-01-01T12-00-00".into()),
|
||||
recency_at: updated_times.get(2).cloned().flatten(),
|
||||
updated_at: updated_times.get(2).cloned().flatten(),
|
||||
},
|
||||
],
|
||||
@@ -759,6 +762,7 @@ async fn test_pagination_cursor() {
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-03-05T09-00-00".into()),
|
||||
recency_at: updated_page1.first().cloned().flatten(),
|
||||
updated_at: updated_page1.first().cloned().flatten(),
|
||||
},
|
||||
ThreadItem {
|
||||
@@ -777,6 +781,7 @@ async fn test_pagination_cursor() {
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-03-04T09-00-00".into()),
|
||||
recency_at: updated_page1.get(1).cloned().flatten(),
|
||||
updated_at: updated_page1.get(1).cloned().flatten(),
|
||||
},
|
||||
],
|
||||
@@ -831,6 +836,7 @@ async fn test_pagination_cursor() {
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-03-03T09-00-00".into()),
|
||||
recency_at: updated_page2.first().cloned().flatten(),
|
||||
updated_at: updated_page2.first().cloned().flatten(),
|
||||
},
|
||||
ThreadItem {
|
||||
@@ -849,6 +855,7 @@ async fn test_pagination_cursor() {
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-03-02T09-00-00".into()),
|
||||
recency_at: updated_page2.get(1).cloned().flatten(),
|
||||
updated_at: updated_page2.get(1).cloned().flatten(),
|
||||
},
|
||||
],
|
||||
@@ -895,6 +902,7 @@ async fn test_pagination_cursor() {
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-03-01T09-00-00".into()),
|
||||
recency_at: updated_page3.first().cloned().flatten(),
|
||||
updated_at: updated_page3.first().cloned().flatten(),
|
||||
}],
|
||||
next_cursor: None,
|
||||
@@ -1066,6 +1074,7 @@ async fn test_get_thread_contents() {
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some(ts.into()),
|
||||
recency_at: page.items[0].updated_at.clone(),
|
||||
updated_at: page.items[0].updated_at.clone(),
|
||||
}],
|
||||
next_cursor: None,
|
||||
@@ -1421,6 +1430,7 @@ async fn test_timestamp_only_cursor_skips_same_second_filesystem_ties() {
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some(ts.to_string()),
|
||||
recency_at: updated_page1.first().cloned().flatten(),
|
||||
updated_at: updated_page1.first().cloned().flatten(),
|
||||
},
|
||||
ThreadItem {
|
||||
@@ -1439,6 +1449,7 @@ async fn test_timestamp_only_cursor_skips_same_second_filesystem_ties() {
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some(ts.to_string()),
|
||||
recency_at: updated_page1.get(1).cloned().flatten(),
|
||||
updated_at: updated_page1.get(1).cloned().flatten(),
|
||||
},
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user