From 0d4910331db5e5c6ec8e42f94d82119570801f95 Mon Sep 17 00:00:00 2001 From: stefanstokic-oai Date: Thu, 23 Jul 2026 18:30:22 +0000 Subject: [PATCH] Preserve timestamps when importing external agent sessions (#34989) ## Why Imported sessions previously used the import time for their thread metadata, discarding the chronology recorded by the source session. ## What changed - Set `created_at` and `updated_at` from the earliest turn start and latest turn completion timestamps in the imported rollout. - Use the latest source timestamp for thread recency, while retaining the current time as a fallback when the rollout has no valid turn timestamps. ## Testing - Update the external agent import integration test to verify the persisted creation, update, and recency timestamps. GitOrigin-RevId: 396d5b7e7463a305a5a828ba3a432b4a97e5d2b9 --- .../session_importer.rs | 30 +++++++++++++++++-- .../tests/suite/v2/external_agent_config.rs | 18 +++++++---- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/codex-rs/app-server/src/external_agent_migration/session_importer.rs b/codex-rs/app-server/src/external_agent_migration/session_importer.rs index 885db8fff2fd..df8b17fc1655 100644 --- a/codex-rs/app-server/src/external_agent_migration/session_importer.rs +++ b/codex-rs/app-server/src/external_agent_migration/session_importer.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use std::sync::Arc; +use chrono::DateTime; use chrono::Utc; use codex_arg0::Arg0DispatchPaths; use codex_core::ThreadManager; @@ -19,7 +20,9 @@ use codex_external_agent_migration::sessions::record_completed_session_imports; use codex_models_manager::manager::RefreshStrategy; use codex_protocol::ThreadId; use codex_protocol::models::BaseInstructions; +use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::MultiAgentVersion; +use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::ThreadHistoryMode; use codex_protocol::protocol::ThreadMemoryMode; use codex_rollout::is_persisted_rollout_item; @@ -330,6 +333,28 @@ impl ExternalAgentSessionImporter { }, }; rollout_items.retain(|item| is_persisted_rollout_item(item, ThreadHistoryMode::Legacy)); + let (created_at, updated_at) = rollout_items + .iter() + .filter_map(|item| match item { + RolloutItem::EventMsg(EventMsg::TurnStarted(event)) => event.started_at, + RolloutItem::EventMsg(EventMsg::TurnComplete(event)) => event.completed_at, + _ => None, + }) + .fold(None, |chronology: Option<(i64, i64)>, timestamp| { + Some(match chronology { + Some((created_at, updated_at)) => { + (created_at.min(timestamp), updated_at.max(timestamp)) + } + None => (timestamp, timestamp), + }) + }) + .and_then(|(created_at, updated_at)| { + Some(( + DateTime::from_timestamp(created_at, /*nsecs*/ 0)?, + DateTime::from_timestamp(updated_at, /*nsecs*/ 0)?, + )) + }) + .unwrap_or((now, now)); let title = title .as_deref() .and_then(codex_core::util::normalize_thread_name); @@ -337,8 +362,9 @@ impl ExternalAgentSessionImporter { title, preview: first_user_message.clone(), model_provider: Some(model_provider), - created_at: Some(now), - updated_at: Some(now), + created_at: Some(created_at), + updated_at: Some(updated_at), + advance_recency_at: Some(updated_at), source: Some(source.clone()), thread_source: Some(None), agent_nickname: Some(source.get_nickname()), diff --git a/codex-rs/app-server/tests/suite/v2/external_agent_config.rs b/codex-rs/app-server/tests/suite/v2/external_agent_config.rs index 08fa545bee71..ce1c493397d2 100644 --- a/codex-rs/app-server/tests/suite/v2/external_agent_config.rs +++ b/codex-rs/app-server/tests/suite/v2/external_agent_config.rs @@ -1513,7 +1513,12 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> { let codex_home = TempDir::new()?; MockResponsesConfig::new(&server.uri()).write(codex_home.path())?; let project_root = codex_home.path().join("repo"); - let recent_timestamp = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true); + let source_created_at_text = "2024-01-02T03:04:05Z"; + let source_updated_at_text = "2024-03-01T04:05:06Z"; + let source_created_at = + chrono::DateTime::parse_from_rfc3339(source_created_at_text)?.timestamp(); + let source_updated_at = + chrono::DateTime::parse_from_rfc3339(source_updated_at_text)?.timestamp(); let session_dir = external_agent_home(codex_home.path()).join("projects/repo"); let session_path = session_dir.join("session.jsonl"); let manifest_dir = connector_metadata_root(codex_home.path()) @@ -1540,21 +1545,21 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> { serde_json::json!({ "type": "user", "cwd": &project_root, - "timestamp": &recent_timestamp, + "timestamp": source_created_at_text, "message": { "content": control_request }, }) .to_string(), serde_json::json!({ "type": "user", "cwd": &project_root, - "timestamp": &recent_timestamp, + "timestamp": "2024-01-03T00:00:00Z", "message": { "content": first_request }, }) .to_string(), serde_json::json!({ "type": "assistant", "cwd": &project_root, - "timestamp": &recent_timestamp, + "timestamp": source_updated_at_text, "attributionMcpServer": "gmail-server", "message": { "content": "first answer" }, }) @@ -1659,7 +1664,7 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> { archived: None, is_pinned: None, cwd: None, - use_state_db_only: false, + use_state_db_only: true, search_term: None, parent_thread_id: None, ancestor_thread_id: None, @@ -1675,6 +1680,9 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> { assert_eq!(imported_thread_id, thread.id.to_string()); assert_eq!(thread.preview, control_request); assert_eq!(thread.name.as_deref(), Some("Fix auth flow")); + assert_eq!(thread.created_at, source_created_at); + assert_eq!(thread.updated_at, source_updated_at); + assert_eq!(thread.recency_at, Some(source_updated_at)); let request_id = mcp .send_thread_read_request(ThreadReadParams {