Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions codex-rs/app-server/src/config/external_agent_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ use codex_core::config::Config;
use codex_core::config::ConfigBuilder;
use codex_core_plugins::PluginInstallRequest;
use codex_core_plugins::PluginsManager;
use codex_core_plugins::command_migration::count_missing_commands;
use codex_core_plugins::command_migration::import_commands;
use codex_core_plugins::command_migration::missing_command_names;
use codex_core_plugins::marketplace::MarketplacePluginInstallPolicy;
use codex_core_plugins::marketplace::find_marketplace_manifest_path;
use codex_core_plugins::marketplace_add::MarketplaceAddRequest;
use codex_core_plugins::marketplace_add::add_marketplace;
use codex_core_plugins::marketplace_add::is_local_marketplace_source;
use codex_external_agent_migration::build_mcp_config_from_external;
use codex_external_agent_migration::count_missing_commands;
use codex_external_agent_migration::count_missing_subagents;
use codex_external_agent_migration::hook_migration_event_names;
use codex_external_agent_migration::import_commands;
use codex_external_agent_migration::import_hooks;
use codex_external_agent_migration::import_subagents;
use codex_external_agent_migration::missing_command_names;
use codex_external_agent_migration::missing_subagent_names;
use codex_external_agent_sessions::ExternalAgentSessionMigration;
use codex_external_agent_sessions::detect_recent_sessions;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::Arc;

use chrono::DateTime;
use chrono::Utc;
use codex_arg0::Arg0DispatchPaths;
use codex_core::ThreadManager;
Expand All @@ -20,11 +22,13 @@ use codex_protocol::protocol::ThreadMemoryMode;
use codex_rollout::is_persisted_rollout_item;
use codex_thread_store::AppendThreadItemsParams;
use codex_thread_store::CreateThreadParams;
use codex_thread_store::HistoricalThreadTimestamps;
use codex_thread_store::ThreadMetadataPatch;
use codex_thread_store::ThreadPersistenceMetadata;
use codex_thread_store::ThreadStore;
use codex_thread_store::UpdateThreadMetadataParams;
use futures::StreamExt;
use tokio::sync::Mutex;
use tokio::sync::Semaphore;

use crate::config::external_agent_config::ExternalAgentConfigImportItemResult;
Expand All @@ -37,6 +41,7 @@ const SESSION_IMPORT_CONCURRENCY: usize = 5;
pub(super) struct ExternalAgentSessionImporter {
codex_home: PathBuf,
permits: Arc<Semaphore>,
reserved_source_identities: Arc<Mutex<HashSet<String>>>,
thread_manager: Arc<ThreadManager>,
thread_store: Arc<dyn ThreadStore>,
config_manager: ConfigManager,
Expand All @@ -54,6 +59,7 @@ impl ExternalAgentSessionImporter {
Self {
codex_home,
permits: Arc::new(Semaphore::new(1)),
reserved_source_identities: Arc::new(Mutex::new(HashSet::new())),
thread_manager,
thread_store,
config_manager,
Expand Down Expand Up @@ -134,16 +140,38 @@ impl ExternalAgentSessionImporter {
else {
return Ok(None);
};
let imported_thread_id =
self.persist_session(pending_import.session)
.await
.map_err(|message| SessionImportFailure {
source_path: pending_import.source_path.clone(),
let source_identity = pending_import
.session
.source_session_id
.as_ref()
.map(|session_id| format!("session:{session_id}"))
.unwrap_or_else(|| format!("path:{}", pending_import.source_path.display()));
if !self
.reserved_source_identities
.lock()
.await
.insert(source_identity.clone())
{
return Ok(None);
}
let source_session_id = pending_import.session.source_session_id.clone();
let imported_thread_id = match self.persist_session(pending_import.session).await {
Ok(imported_thread_id) => imported_thread_id,
Err(message) => {
self.reserved_source_identities
.lock()
.await
.remove(&source_identity);
return Err(SessionImportFailure {
source_path: pending_import.source_path,
message,
stage: "session_persist",
})?;
});
}
};
Ok(Some(CompletedExternalAgentSessionImport {
source_path: pending_import.source_path,
source_session_id,
source_content_sha256: pending_import.source_content_sha256,
imported_thread_id,
}))
Expand All @@ -168,6 +196,9 @@ impl ExternalAgentSessionImporter {
cwd,
title,
first_user_message,
source_session_id: _,
created_at,
updated_at,
mut rollout_items,
} = session;
let config = self
Expand Down Expand Up @@ -204,6 +235,12 @@ impl ExternalAgentSessionImporter {
ThreadMemoryMode::Disabled
};
let now = Utc::now();
let created_at = created_at
.and_then(|timestamp| DateTime::<Utc>::from_timestamp(timestamp, 0))
.unwrap_or(now);
let updated_at = updated_at
.and_then(|timestamp| DateTime::<Utc>::from_timestamp(timestamp, 0))
.unwrap_or(created_at);
let create_params = CreateThreadParams {
session_id: thread_id.into(),
thread_id,
Expand All @@ -228,6 +265,10 @@ impl ExternalAgentSessionImporter {
cwd: Some(cwd.clone()),
model_provider: model_provider.clone(),
memory_mode,
historical_timestamps: Some(HistoricalThreadTimestamps {
created_at,
updated_at,
}),
},
};
rollout_items.retain(is_persisted_rollout_item);
Expand All @@ -238,8 +279,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),
recency_at: Some(updated_at),
source: Some(source.clone()),
thread_source: Some(None),
agent_nickname: Some(source.get_nickname()),
Expand Down Expand Up @@ -269,14 +311,6 @@ impl ExternalAgentSessionImporter {
return Err(format!("failed to import session: {err}"));
}

self.thread_store
.update_thread_metadata(UpdateThreadMetadataParams {
thread_id,
patch: metadata,
include_archived: false,
})
.await
.map_err(|err| format!("failed to update imported session: {err}"))?;
self.thread_store
.persist_thread(thread_id)
.await
Expand All @@ -285,6 +319,14 @@ impl ExternalAgentSessionImporter {
.shutdown_thread(thread_id)
.await
.map_err(|err| format!("failed to shutdown imported session: {err}"))?;
self.thread_store
.update_thread_metadata(UpdateThreadMetadataParams {
thread_id,
patch: metadata,
include_archived: false,
})
.await
.map_err(|err| format!("failed to update imported session: {err}"))?;
Ok(thread_id)
}
}
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/tests/suite/conversation_summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() ->
cwd: None,
model_provider: "test-provider".to_string(),
memory_mode: ThreadMemoryMode::Disabled,
historical_timestamps: None,
},
})
.await?;
Expand Down
35 changes: 32 additions & 3 deletions codex-rs/app-server/tests/suite/v2/external_agent_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,8 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
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 = "2026-06-03T12:00:00Z";
let source_updated_at = "2026-06-03T12:00:05Z";
let session_dir = external_agent_home(codex_home.path()).join("projects/repo");
let session_path = session_dir.join("session.jsonl");
std::fs::create_dir_all(&project_root)?;
Expand All @@ -612,14 +613,14 @@ 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,
"message": { "content": "first request" },
})
.to_string(),
serde_json::json!({
"type": "assistant",
"cwd": &project_root,
"timestamp": &recent_timestamp,
"timestamp": source_updated_at,
"message": { "content": "first answer" },
})
.to_string(),
Expand Down Expand Up @@ -731,6 +732,22 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
assert_eq!(imported_thread_id, thread.id.to_string());
assert_eq!(thread.preview, "first request");
assert_eq!(thread.name.as_deref(), Some("source session title"));
let source_created_at = chrono::DateTime::parse_from_rfc3339(source_created_at)?.timestamp();
let source_updated_at = chrono::DateTime::parse_from_rfc3339(source_updated_at)?.timestamp();
assert_eq!(
(
thread.created_at,
thread.updated_at,
thread.recency_at,
thread.cwd.as_path(),
),
(
source_created_at,
source_updated_at,
Some(source_updated_at),
project_root.as_path(),
)
);

let request_id = mcp
.send_thread_read_request(ThreadReadParams {
Expand All @@ -745,6 +762,18 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
.await??;
let response: ThreadReadResponse = to_response(response)?;
assert_eq!(response.thread.turns.len(), 1);
assert_eq!(
(
response.thread.turns[0].started_at,
response.thread.turns[0].completed_at,
response.thread.turns[0].duration_ms,
),
(
Some(source_created_at),
Some(source_updated_at),
Some(5_000),
)
);
let items = &response.thread.turns[0].items;
assert_eq!(items.len(), 3);
assert_eq!(
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/tests/suite/v2/remote_thread_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ async fn thread_delete_with_non_local_thread_store_does_not_create_local_persist
cwd: Some(codex_home.path().to_path_buf()),
model_provider: "mock_provider".to_string(),
memory_mode: ThreadMemoryMode::Enabled,
historical_timestamps: None,
},
})
.await?;
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/tests/suite/v2/thread_read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1493,6 +1493,7 @@ async fn seed_pathless_store_thread(
cwd: None,
model_provider: "test-provider".to_string(),
memory_mode: ThreadMemoryMode::Disabled,
historical_timestamps: None,
},
})
.await?;
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/tests/suite/v2/thread_unarchive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> {
cwd: None,
model_provider: "test-provider".to_string(),
memory_mode: ThreadMemoryMode::Disabled,
historical_timestamps: None,
},
})
.await?;
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core-plugins/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ regex = { workspace = true }
semver = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
tar = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
Expand Down
Loading
Loading