mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Record external agent import results (#28396)
## Summary - restore `externalAgentConfig/import/progress` notifications while keeping `externalAgentConfig/import/completed` as the must-deliver event - persist completed external-agent config imports in state DB by `importId`, including concrete success/failure details for config, AGENTS.md, skills, plugins, MCP servers, subagents, hooks, commands, and sessions - add `externalAgentConfig/import/readHistories` so clients can recover persisted import results after missing the live completion notification - include `errorType` on import failures in protocol responses/notifications and persisted DB JSON so future code can classify failures without another wire/storage shape change ## Validation - `git diff --check` - `just test -p codex-state external_agent_config_imports` - `just test -p codex-app-server-protocol` - `CODEX_SQLITE_HOME=/private/tmp/codex-app-server-sqlite-read-details just test -p codex-app-server external_agent_config_import_sends_completion_notification_for_sync_only_import` Also ran earlier broader checks before publishing: - `just test -p codex-state` - `CODEX_SQLITE_HOME=/private/tmp/codex-app-server-external-agent-test-sqlite just test -p codex-app-server external_agent_config` - `just test -p codex-external-agent-migration`
This commit is contained in:
@@ -19,9 +19,12 @@ use codex_app_server_protocol::CommandMigration;
|
||||
use codex_app_server_protocol::ExternalAgentConfigDetectParams;
|
||||
use codex_app_server_protocol::ExternalAgentConfigDetectResponse;
|
||||
use codex_app_server_protocol::ExternalAgentConfigImportCompletedNotification;
|
||||
use codex_app_server_protocol::ExternalAgentConfigImportHistoriesReadResponse;
|
||||
use codex_app_server_protocol::ExternalAgentConfigImportHistory;
|
||||
use codex_app_server_protocol::ExternalAgentConfigImportItemTypeFailure as ProtocolImportFailure;
|
||||
use codex_app_server_protocol::ExternalAgentConfigImportItemTypeSuccess as ProtocolImportSuccess;
|
||||
use codex_app_server_protocol::ExternalAgentConfigImportParams;
|
||||
use codex_app_server_protocol::ExternalAgentConfigImportProgressNotification;
|
||||
use codex_app_server_protocol::ExternalAgentConfigImportResponse;
|
||||
use codex_app_server_protocol::ExternalAgentConfigImportTypeResult as ProtocolImportTypeResult;
|
||||
use codex_app_server_protocol::ExternalAgentConfigMigrationItem;
|
||||
@@ -35,6 +38,9 @@ use codex_app_server_protocol::ServerNotification;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_external_agent_sessions::ExternalAgentSessionMigration as CoreSessionMigration;
|
||||
use codex_rollout::StateDbHandle;
|
||||
use codex_state::ExternalAgentConfigImportFailureRecord;
|
||||
use codex_state::ExternalAgentConfigImportSuccessRecord;
|
||||
use codex_thread_store::ThreadStore;
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
@@ -50,18 +56,32 @@ pub(crate) struct ExternalAgentConfigRequestProcessor {
|
||||
session_importer: ExternalAgentSessionImporter,
|
||||
thread_manager: Arc<ThreadManager>,
|
||||
config_processor: ConfigRequestProcessor,
|
||||
state_db: Option<StateDbHandle>,
|
||||
}
|
||||
|
||||
pub(crate) struct ExternalAgentConfigRequestProcessorArgs {
|
||||
pub(crate) outgoing: Arc<OutgoingMessageSender>,
|
||||
pub(crate) thread_manager: Arc<ThreadManager>,
|
||||
pub(crate) thread_store: Arc<dyn ThreadStore>,
|
||||
pub(crate) config_manager: ConfigManager,
|
||||
pub(crate) config_processor: ConfigRequestProcessor,
|
||||
pub(crate) state_db: Option<StateDbHandle>,
|
||||
pub(crate) arg0_paths: Arg0DispatchPaths,
|
||||
pub(crate) codex_home: PathBuf,
|
||||
}
|
||||
|
||||
impl ExternalAgentConfigRequestProcessor {
|
||||
pub(crate) fn new(
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
thread_manager: Arc<ThreadManager>,
|
||||
thread_store: Arc<dyn ThreadStore>,
|
||||
config_manager: ConfigManager,
|
||||
config_processor: ConfigRequestProcessor,
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
codex_home: PathBuf,
|
||||
) -> Self {
|
||||
pub(crate) fn new(args: ExternalAgentConfigRequestProcessorArgs) -> Self {
|
||||
let ExternalAgentConfigRequestProcessorArgs {
|
||||
outgoing,
|
||||
thread_manager,
|
||||
thread_store,
|
||||
config_manager,
|
||||
config_processor,
|
||||
state_db,
|
||||
arg0_paths,
|
||||
codex_home,
|
||||
} = args;
|
||||
let session_importer = ExternalAgentSessionImporter::new(
|
||||
codex_home.clone(),
|
||||
Arc::clone(&thread_manager),
|
||||
@@ -75,6 +95,7 @@ impl ExternalAgentConfigRequestProcessor {
|
||||
session_importer,
|
||||
thread_manager,
|
||||
config_processor,
|
||||
state_db,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,23 +228,31 @@ impl ExternalAgentConfigRequestProcessor {
|
||||
|
||||
let mut completed_item_results = Vec::new();
|
||||
if let Some(session_validation_result) = session_validation_result {
|
||||
send_import_progress(&self.outgoing, &import_id, &session_validation_result).await;
|
||||
completed_item_results.push(session_validation_result);
|
||||
}
|
||||
for item_result in import_outcome.item_results {
|
||||
send_import_progress(&self.outgoing, &import_id, &item_result).await;
|
||||
completed_item_results.push(item_result);
|
||||
}
|
||||
|
||||
let has_background_imports = !import_outcome.pending_plugin_imports.is_empty()
|
||||
|| !pending_session_imports.is_empty();
|
||||
if !has_background_imports {
|
||||
send_completed_import_notification(&self.outgoing, import_id, &completed_item_results)
|
||||
.await;
|
||||
send_completed_import_notification(
|
||||
&self.outgoing,
|
||||
self.state_db.as_ref(),
|
||||
import_id,
|
||||
&completed_item_results,
|
||||
)
|
||||
.await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let session_importer = self.session_importer.clone();
|
||||
let plugin_processor = self.clone();
|
||||
let outgoing = Arc::clone(&self.outgoing);
|
||||
let state_db = self.state_db.clone();
|
||||
let thread_manager = Arc::clone(&self.thread_manager);
|
||||
let session_import_result = (!pending_session_imports.is_empty()).then(|| {
|
||||
CoreImportItemResult::new(
|
||||
@@ -234,13 +263,19 @@ impl ExternalAgentConfigRequestProcessor {
|
||||
});
|
||||
let pending_plugin_imports = import_outcome.pending_plugin_imports;
|
||||
tokio::spawn(async move {
|
||||
let session_progress_outgoing = Arc::clone(&outgoing);
|
||||
let session_import_id = import_id.clone();
|
||||
let session_imports = async move {
|
||||
let session_import_result = session_import_result?;
|
||||
let item_result = session_importer
|
||||
.import_sessions(pending_session_imports, session_import_result)
|
||||
.await;
|
||||
send_import_progress(&session_progress_outgoing, &session_import_id, &item_result)
|
||||
.await;
|
||||
Some(item_result)
|
||||
};
|
||||
let plugin_progress_outgoing = Arc::clone(&outgoing);
|
||||
let plugin_import_id = import_id.clone();
|
||||
let plugin_imports = async move {
|
||||
let mut item_results = Vec::new();
|
||||
for pending_plugin_import in pending_plugin_imports {
|
||||
@@ -265,6 +300,12 @@ impl ExternalAgentConfigRequestProcessor {
|
||||
);
|
||||
}
|
||||
}
|
||||
send_import_progress(
|
||||
&plugin_progress_outgoing,
|
||||
&plugin_import_id,
|
||||
&item_result,
|
||||
)
|
||||
.await;
|
||||
item_results.push(item_result);
|
||||
}
|
||||
item_results
|
||||
@@ -280,12 +321,37 @@ impl ExternalAgentConfigRequestProcessor {
|
||||
thread_manager.plugins_manager().clear_cache();
|
||||
thread_manager.skills_manager().clear_cache();
|
||||
}
|
||||
send_completed_import_notification(&outgoing, import_id, &completed_item_results).await;
|
||||
send_completed_import_notification(
|
||||
&outgoing,
|
||||
state_db.as_ref(),
|
||||
import_id,
|
||||
&completed_item_results,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn read_import_histories(
|
||||
&self,
|
||||
) -> Result<ExternalAgentConfigImportHistoriesReadResponse, JSONRPCErrorError> {
|
||||
let state_db = self
|
||||
.state_db
|
||||
.as_ref()
|
||||
.ok_or_else(|| internal_error("state database is unavailable"))?;
|
||||
let histories = state_db
|
||||
.external_agent_config_import_history_records()
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to read import histories: {err}")))?;
|
||||
let data = histories
|
||||
.into_iter()
|
||||
.map(protocol_import_history)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(ExternalAgentConfigImportHistoriesReadResponse { data })
|
||||
}
|
||||
|
||||
fn validate_pending_session_imports(
|
||||
&self,
|
||||
params: &ExternalAgentConfigImportParams,
|
||||
@@ -467,12 +533,37 @@ impl ExternalAgentConfigRequestProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_import_progress(
|
||||
outgoing: &OutgoingMessageSender,
|
||||
import_id: &str,
|
||||
item_result: &CoreImportItemResult,
|
||||
) {
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ExternalAgentConfigImportProgress(
|
||||
ExternalAgentConfigImportProgressNotification {
|
||||
import_id: import_id.to_string(),
|
||||
item_type_results: vec![protocol_import_type_result(item_result)],
|
||||
},
|
||||
))
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn send_completed_import_notification(
|
||||
outgoing: &OutgoingMessageSender,
|
||||
state_db: Option<&StateDbHandle>,
|
||||
import_id: String,
|
||||
item_results: &[CoreImportItemResult],
|
||||
) {
|
||||
let notification = completed_notification(import_id, item_results);
|
||||
if let Some(state_db) = state_db
|
||||
&& let Err(err) = record_completed_import_notification(state_db, ¬ification).await
|
||||
{
|
||||
tracing::warn!(
|
||||
import_id = %notification.import_id,
|
||||
error = %err,
|
||||
"failed to record external agent config import completion"
|
||||
);
|
||||
}
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ExternalAgentConfigImportCompleted(
|
||||
notification,
|
||||
@@ -480,6 +571,103 @@ async fn send_completed_import_notification(
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn record_completed_import_notification(
|
||||
state_db: &StateDbHandle,
|
||||
notification: &ExternalAgentConfigImportCompletedNotification,
|
||||
) -> anyhow::Result<()> {
|
||||
let successes = notification
|
||||
.item_type_results
|
||||
.iter()
|
||||
.flat_map(|type_result| type_result.successes.iter())
|
||||
.map(|success| {
|
||||
Ok(ExternalAgentConfigImportSuccessRecord {
|
||||
item_type: serde_json::from_value(serde_json::to_value(success.item_type)?)?,
|
||||
cwd: success.cwd.clone(),
|
||||
source: success.source.clone(),
|
||||
target: success.target.clone(),
|
||||
})
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
let failures = notification
|
||||
.item_type_results
|
||||
.iter()
|
||||
.flat_map(|type_result| type_result.failures.iter())
|
||||
.map(|failure| {
|
||||
Ok(ExternalAgentConfigImportFailureRecord {
|
||||
item_type: serde_json::from_value(serde_json::to_value(failure.item_type)?)?,
|
||||
error_type: failure.error_type.clone(),
|
||||
failure_stage: failure.failure_stage.clone(),
|
||||
message: failure.message.clone(),
|
||||
cwd: failure.cwd.clone(),
|
||||
source: failure.source.clone(),
|
||||
})
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
state_db
|
||||
.record_external_agent_config_import_completed(
|
||||
notification.import_id.as_str(),
|
||||
&successes,
|
||||
&failures,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn protocol_import_history(
|
||||
record: codex_state::ExternalAgentConfigImportHistoryRecord,
|
||||
) -> Result<ExternalAgentConfigImportHistory, JSONRPCErrorError> {
|
||||
let successes = record
|
||||
.successes
|
||||
.into_iter()
|
||||
.map(protocol_import_success_record)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let failures = record
|
||||
.failures
|
||||
.into_iter()
|
||||
.map(protocol_import_failure_record)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(ExternalAgentConfigImportHistory {
|
||||
import_id: record.import_id,
|
||||
completed_at_ms: record.completed_at_ms,
|
||||
successes,
|
||||
failures,
|
||||
})
|
||||
}
|
||||
|
||||
fn protocol_import_success_record(
|
||||
record: ExternalAgentConfigImportSuccessRecord,
|
||||
) -> Result<ProtocolImportSuccess, JSONRPCErrorError> {
|
||||
Ok(ProtocolImportSuccess {
|
||||
item_type: protocol_import_record_item_type(record.item_type)?,
|
||||
cwd: record.cwd,
|
||||
source: record.source,
|
||||
target: record.target,
|
||||
})
|
||||
}
|
||||
|
||||
fn protocol_import_failure_record(
|
||||
record: ExternalAgentConfigImportFailureRecord,
|
||||
) -> Result<ProtocolImportFailure, JSONRPCErrorError> {
|
||||
Ok(ProtocolImportFailure {
|
||||
item_type: protocol_import_record_item_type(record.item_type)?,
|
||||
error_type: record.error_type,
|
||||
failure_stage: record.failure_stage,
|
||||
message: record.message,
|
||||
cwd: record.cwd,
|
||||
source: record.source,
|
||||
})
|
||||
}
|
||||
|
||||
fn protocol_import_record_item_type(
|
||||
item_type: String,
|
||||
) -> Result<ExternalAgentConfigMigrationItemType, JSONRPCErrorError> {
|
||||
serde_json::from_value(serde_json::Value::String(item_type.clone())).map_err(|err| {
|
||||
internal_error(format!(
|
||||
"failed to decode import item type {item_type}: {err}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn completed_notification(
|
||||
import_id: String,
|
||||
item_results: &[CoreImportItemResult],
|
||||
@@ -529,6 +717,22 @@ fn completed_notification(
|
||||
}
|
||||
}
|
||||
|
||||
fn protocol_import_type_result(item_result: &CoreImportItemResult) -> ProtocolImportTypeResult {
|
||||
ProtocolImportTypeResult {
|
||||
item_type: protocol_migration_item_type(item_result.item_type),
|
||||
successes: item_result
|
||||
.successes
|
||||
.iter()
|
||||
.map(protocol_import_success)
|
||||
.collect(),
|
||||
failures: item_result
|
||||
.raw_errors
|
||||
.iter()
|
||||
.map(protocol_import_raw_error)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn protocol_import_success(
|
||||
success: &crate::config::external_agent_config::ExternalAgentConfigImportSuccess,
|
||||
) -> ProtocolImportSuccess {
|
||||
@@ -543,6 +747,7 @@ fn protocol_import_success(
|
||||
fn protocol_import_raw_error(raw_error: &CoreImportRawError) -> ProtocolImportFailure {
|
||||
ProtocolImportFailure {
|
||||
item_type: protocol_migration_item_type(raw_error.item_type),
|
||||
error_type: raw_error.error_type.clone(),
|
||||
failure_stage: raw_error.failure_stage.clone(),
|
||||
message: raw_error.message.clone(),
|
||||
cwd: raw_error.cwd.clone(),
|
||||
|
||||
Reference in New Issue
Block a user