mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Preserve thread metadata when rollout files are missing (#35644)
## What changed - Skip threads whose rollout files cannot be found while continuing through database pages until the requested number of valid threads is collected. - Retain the skipped threads and their rollout paths in the state database instead of deleting their metadata during listing. ## Testing - Update the state-backed listing test to verify that a missing rollout path does not consume a page slot or remove the stored thread metadata. GitOrigin-RevId: 51c03a78434b0aeeb5a2dc685b97243b6ad958bf
This commit is contained in:
1 parent
0affaf2acb
commit
e8f0f64f20
2 files changed
+118
-58
No files matched your search
@@ -1007,7 +1007,8 @@ async fn list_threads_db_disabled_does_not_skip_paginated_items() -> std::io::Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn list_threads_db_enabled_drops_missing_rollout_paths() -> std::io::Result<()> {
|
async fn list_threads_db_enabled_preserves_metadata_for_missing_rollout_paths()
|
||||||
|
-> std::io::Result<()> {
|
||||||
let home = TempDir::new().expect("temp dir");
|
let home = TempDir::new().expect("temp dir");
|
||||||
let config = test_config(home.path());
|
let config = test_config(home.path());
|
||||||
|
|
||||||
@@ -1047,11 +1048,34 @@ async fn list_threads_db_enabled_drops_missing_rollout_paths() -> std::io::Resul
|
|||||||
.await
|
.await
|
||||||
.expect("state db upsert should succeed");
|
.expect("state db upsert should succeed");
|
||||||
|
|
||||||
|
let valid_uuid = Uuid::from_u128(9011);
|
||||||
|
let valid_thread_id = ThreadId::from_string(&valid_uuid.to_string()).expect("valid thread id");
|
||||||
|
let valid_path = write_session_file(home.path(), "2025-01-02T13-00-00", valid_uuid)?;
|
||||||
|
let valid_created_at = chrono::Utc
|
||||||
|
.with_ymd_and_hms(2025, 1, 2, 13, 0, 0)
|
||||||
|
.single()
|
||||||
|
.expect("valid datetime");
|
||||||
|
let mut valid_builder = codex_state::ThreadMetadataBuilder::new(
|
||||||
|
valid_thread_id,
|
||||||
|
valid_path.clone(),
|
||||||
|
valid_created_at,
|
||||||
|
SessionSource::Cli,
|
||||||
|
);
|
||||||
|
valid_builder.model_provider = Some(config.model_provider_id.clone());
|
||||||
|
valid_builder.cwd = home.path().to_path_buf();
|
||||||
|
let mut valid_metadata = valid_builder.build(config.model_provider_id.as_str());
|
||||||
|
valid_metadata.first_user_message = Some("Older valid thread".to_string());
|
||||||
|
valid_metadata.preview = valid_metadata.first_user_message.clone();
|
||||||
|
runtime
|
||||||
|
.upsert_thread(&valid_metadata)
|
||||||
|
.await
|
||||||
|
.expect("state db upsert should succeed");
|
||||||
|
|
||||||
let default_provider = config.model_provider_id.clone();
|
let default_provider = config.model_provider_id.clone();
|
||||||
let page = RolloutRecorder::list_threads(
|
let page = RolloutRecorder::list_threads(
|
||||||
Some(runtime.clone()),
|
Some(runtime.clone()),
|
||||||
&config,
|
&config,
|
||||||
/*page_size*/ 10,
|
/*page_size*/ 1,
|
||||||
/*cursor*/ None,
|
/*cursor*/ None,
|
||||||
ThreadSortKey::CreatedAt,
|
ThreadSortKey::CreatedAt,
|
||||||
SortDirection::Desc,
|
SortDirection::Desc,
|
||||||
@@ -1062,12 +1086,18 @@ async fn list_threads_db_enabled_drops_missing_rollout_paths() -> std::io::Resul
|
|||||||
/*search_term*/ None,
|
/*search_term*/ None,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(page.items.len(), 0);
|
assert_eq!(page.items.len(), 1);
|
||||||
|
assert_eq!(page.items[0].path, valid_path);
|
||||||
let stored_path = runtime
|
let stored_path = runtime
|
||||||
.find_rollout_path_by_id(thread_id, Some(false))
|
.find_rollout_path_by_id(thread_id, Some(false))
|
||||||
.await
|
.await
|
||||||
.expect("state db lookup should succeed");
|
.expect("state db lookup should succeed");
|
||||||
assert_eq!(stored_path, None);
|
assert_eq!(stored_path, Some(metadata.rollout_path.clone()));
|
||||||
|
let stored_metadata = runtime
|
||||||
|
.get_thread(thread_id)
|
||||||
|
.await
|
||||||
|
.expect("state db lookup should succeed");
|
||||||
|
assert_eq!(stored_metadata, Some(metadata));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ pub async fn list_threads_db(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let anchor = cursor_to_anchor(cursor);
|
let mut anchor = cursor_to_anchor(cursor);
|
||||||
let allowed_sources: Vec<String> = allowed_sources
|
let allowed_sources: Vec<String> = allowed_sources
|
||||||
.iter()
|
.iter()
|
||||||
.map(|value| match serde_json::to_value(value) {
|
.map(|value| match serde_json::to_value(value) {
|
||||||
@@ -390,63 +390,93 @@ pub async fn list_threads_db(
|
|||||||
.map(|cwd| normalize_cwd_for_state_db(cwd))
|
.map(|cwd| normalize_cwd_for_state_db(cwd))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
});
|
});
|
||||||
let filters = codex_state::ThreadFilterOptions {
|
let state_sort_key = match sort_key {
|
||||||
archived_only: archived,
|
ThreadSortKey::CreatedAt => codex_state::SortKey::CreatedAt,
|
||||||
is_pinned,
|
ThreadSortKey::UpdatedAt => codex_state::SortKey::UpdatedAt,
|
||||||
allowed_sources: allowed_sources.as_slice(),
|
ThreadSortKey::RecencyAt => codex_state::SortKey::RecencyAt,
|
||||||
model_providers: model_providers.as_deref(),
|
|
||||||
cwd_filters: normalized_cwd_filters.as_deref(),
|
|
||||||
anchor: anchor.as_ref(),
|
|
||||||
sort_key: match sort_key {
|
|
||||||
ThreadSortKey::CreatedAt => codex_state::SortKey::CreatedAt,
|
|
||||||
ThreadSortKey::UpdatedAt => codex_state::SortKey::UpdatedAt,
|
|
||||||
ThreadSortKey::RecencyAt => codex_state::SortKey::RecencyAt,
|
|
||||||
},
|
|
||||||
sort_direction: match sort_direction {
|
|
||||||
SortDirection::Asc => codex_state::SortDirection::Asc,
|
|
||||||
SortDirection::Desc => codex_state::SortDirection::Desc,
|
|
||||||
},
|
|
||||||
search_term,
|
|
||||||
};
|
};
|
||||||
let page = match relation_filter {
|
let state_sort_direction = match sort_direction {
|
||||||
Some(relation_filter) => {
|
SortDirection::Asc => codex_state::SortDirection::Asc,
|
||||||
ctx.list_threads_by_relation(page_size, relation_filter, filters)
|
SortDirection::Desc => codex_state::SortDirection::Desc,
|
||||||
.await
|
|
||||||
}
|
|
||||||
None => ctx.list_threads(page_size, filters).await,
|
|
||||||
};
|
};
|
||||||
match page {
|
|
||||||
Ok(mut page) => {
|
if let Some(relation_filter) = relation_filter {
|
||||||
// Relationship-filtered listings intentionally treat persisted state as authoritative.
|
let filters = codex_state::ThreadFilterOptions {
|
||||||
if relation_filter.is_some() {
|
archived_only: archived,
|
||||||
return Some(page);
|
is_pinned,
|
||||||
|
allowed_sources: allowed_sources.as_slice(),
|
||||||
|
model_providers: model_providers.as_deref(),
|
||||||
|
cwd_filters: normalized_cwd_filters.as_deref(),
|
||||||
|
anchor: anchor.as_ref(),
|
||||||
|
sort_key: state_sort_key,
|
||||||
|
sort_direction: state_sort_direction,
|
||||||
|
search_term,
|
||||||
|
};
|
||||||
|
return match ctx
|
||||||
|
.list_threads_by_relation(page_size, relation_filter, filters)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(page) => Some(page),
|
||||||
|
Err(err) => {
|
||||||
|
warn!("state db list_threads failed: {err}");
|
||||||
|
None
|
||||||
}
|
}
|
||||||
let mut valid_items = Vec::with_capacity(page.items.len());
|
};
|
||||||
for item in page.items {
|
|
||||||
if let Some(existing_path) =
|
|
||||||
crate::compression::existing_rollout_path(item.rollout_path.as_path()).await
|
|
||||||
{
|
|
||||||
let mut item = item;
|
|
||||||
item.rollout_path = existing_path;
|
|
||||||
valid_items.push(item);
|
|
||||||
} else {
|
|
||||||
warn!(
|
|
||||||
"state db list_threads returned stale rollout path for thread {}: {}",
|
|
||||||
item.id,
|
|
||||||
item.rollout_path.display()
|
|
||||||
);
|
|
||||||
warn!("state db discrepancy during list_threads_db: stale_db_path_dropped");
|
|
||||||
let _ = ctx.delete_thread(item.id).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
page.items = valid_items;
|
|
||||||
Some(page)
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!("state db list_threads failed: {err}");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut items = Vec::with_capacity(page_size);
|
||||||
|
let mut next_anchor = None;
|
||||||
|
let mut num_scanned_rows = 0usize;
|
||||||
|
while items.len() < page_size {
|
||||||
|
let filters = codex_state::ThreadFilterOptions {
|
||||||
|
archived_only: archived,
|
||||||
|
is_pinned,
|
||||||
|
allowed_sources: allowed_sources.as_slice(),
|
||||||
|
model_providers: model_providers.as_deref(),
|
||||||
|
cwd_filters: normalized_cwd_filters.as_deref(),
|
||||||
|
anchor: anchor.as_ref(),
|
||||||
|
sort_key: state_sort_key,
|
||||||
|
sort_direction: state_sort_direction,
|
||||||
|
search_term,
|
||||||
|
};
|
||||||
|
let page = match ctx.list_threads(page_size - items.len(), filters).await {
|
||||||
|
Ok(page) => page,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("state db list_threads failed: {err}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
num_scanned_rows = num_scanned_rows.saturating_add(page.num_scanned_rows);
|
||||||
|
next_anchor = page.next_anchor;
|
||||||
|
|
||||||
|
for item in page.items {
|
||||||
|
if let Some(existing_path) =
|
||||||
|
crate::compression::existing_rollout_path(item.rollout_path.as_path()).await
|
||||||
|
{
|
||||||
|
let mut item = item;
|
||||||
|
item.rollout_path = existing_path;
|
||||||
|
items.push(item);
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"state db list_threads returned stale rollout path for thread {}: {}",
|
||||||
|
item.id,
|
||||||
|
item.rollout_path.display()
|
||||||
|
);
|
||||||
|
warn!("state db discrepancy during list_threads_db: stale_db_path_retained");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if items.len() == page_size || next_anchor.is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
anchor = next_anchor.clone();
|
||||||
|
}
|
||||||
|
Some(codex_state::ThreadsPage {
|
||||||
|
items,
|
||||||
|
parent_thread_ids: Default::default(),
|
||||||
|
next_anchor,
|
||||||
|
num_scanned_rows,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Look up the rollout path for a thread id using SQLite.
|
/// Look up the rollout path for a thread id using SQLite.
|
||||||
|
|||||||
Reference in new issue
Block a user