Skip to content

[codex] Record external agent import results#28396

Merged
charlesgong-openai merged 5 commits into
mainfrom
codex/external-agent-import-results
Jun 16, 2026
Merged

[codex] Record external agent import results#28396
charlesgong-openai merged 5 commits into
mainfrom
codex/external-agent-import-results

Conversation

@charlesgong-openai

@charlesgong-openai charlesgong-openai commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

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

@charlesgong-openai
charlesgong-openai force-pushed the codex/external-agent-import-results branch 3 times, most recently from 4918f16 to 5f4020e Compare June 15, 2026 23:09
@@ -1602,6 +1602,7 @@ server_notification_definitions! {
AccountRateLimitsUpdated => "account/rateLimits/updated" (v2::AccountRateLimitsUpdatedNotification),
AppListUpdated => "app/list/updated" (v2::AppListUpdatedNotification),
RemoteControlStatusChanged => "remoteControl/status/changed" (v2::RemoteControlStatusChangedNotification),
ExternalAgentConfigImportProgress => "externalAgentConfig/import/progress" (v2::ExternalAgentConfigImportProgressNotification),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registers the new best-effort import progress notification method in the server notification union so app-server clients can receive per-item import updates before the final completion notification.

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ExternalAgentConfigImportProgressNotification {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds the progress notification payload. The itemTypeResults array is currently sent with one result per progress event, but leaves room for batching later without changing the wire shape.

if let Some((source, target)) =
self.import_config(migration_item.cwd.as_deref())?
{
item_result.record_success(Some(source), Some(target));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Records config imports as a concrete source -> target success when config.toml is created or merged, instead of only incrementing a count.

);
item_result.record_successes(skills_count);
for skill_name in imported_skills {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeps the existing metric as a count, but records each imported skill name individually so success details can show exactly which skills migrated.

self.import_mcp_server_config(migration_item.cwd.as_deref())?;
emit_migration_metric(
EXTERNAL_AGENT_CONFIG_IMPORT_METRIC,
ExternalAgentConfigMigrationItemType::McpServerConfig,
/*skills_count*/ None,
);
item_result.record_successes(migrated_count);
for server_name in migrated_server_names {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Records migrated MCP server names individually, which gives the completion payload and DB row the exact server ids instead of only the number of merged servers.

emit_migration_metric(
EXTERNAL_AGENT_CONFIG_IMPORT_METRIC,
ExternalAgentConfigMigrationItemType::Hooks,
/*skills_count*/ None,
);
item_result.record_successes(migrated_count);
for hook_name in migrated_hook_names {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converts hook import success from a boolean/count into the concrete hook event names that were copied into hooks.json.

);
item_result.record_successes(commands_count);
for command_name in imported_commands {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Records each imported slash command name as an individual success after command files are converted into skill directories.

);
item_result.record_successes(subagents_count);
for subagent_name in imported_subagents {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Records each imported subagent name as a success, while still using the list length for telemetry count compatibility.

}

let Some(target_parent) = target_config.parent() else {
return Err(invalid_data_error("config target path has no parent"));
};
fs::create_dir_all(target_parent)?;
if !target_config.exists() {
let migrated_count = migrated_mcp_server_names(&migrated).len();
let migrated_server_names = migrated_mcp_server_names(&migrated);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Captures the MCP server names before writing a new config file, so a fresh config migration reports the same concrete server ids as a merge migration.

&source_external_agent_dir,
&target_hooks,
)?))
let hook_names = hook_migration_event_names(&source_external_agent_dir, &target_hooks)?;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reads the hook event names before importing hooks so the method can return the specific events copied when hooks.json is created.

@@ -986,7 +999,10 @@ impl ExternalAgentConfigService {
fs::create_dir_all(target_parent)?;
if !target_config.exists() {
write_toml_file(&target_config, &migrated)?;
return Ok(1);
return Ok(Some((

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returns the source settings path and target config path when config migration actually writes data; callers use this pair as the structured success detail.

@@ -1150,20 +1171,20 @@ impl ExternalAgentConfigService {
}

copy_dir_recursive(&entry.path(), &target)?;
copied_count += 1;
copied_names.push(entry.file_name().to_string_lossy().to_string());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collects copied skill directory names as success details; skipped existing directories remain omitted from the success list.

state_db: Option<StateDbHandle>,
}

pub(crate) struct ExternalAgentConfigRequestProcessorArgs {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collects constructor dependencies into a named args struct. That keeps the state_db addition readable at the callsite and avoids a long positional constructor.

arg0_paths,
config.codex_home.to_path_buf(),
);
let external_agent_config_processor =

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Threads the state DB handle into the external-agent import processor so completed import notifications can be persisted locally by import id.

@@ -207,23 +227,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;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sends a best-effort progress event as each synchronous item finishes, before adding that item to the eventual completion payload.

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Background session imports also emit progress when they finish, so long-running async work is visible before the final must-deliver completion notification.

import_id: String,
item_results: &[CoreImportItemResult],
) {
let notification = completed_notification(import_id, item_results);
if let Some(state_db) = state_db

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Persists the must-deliver completion notification before sending it. Persistence failures are logged and do not prevent the client notification from being sent.

@@ -265,6 +299,12 @@ impl ExternalAgentConfigRequestProcessor {
);
}
}
send_import_progress(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Background plugin imports emit their own progress item after each marketplace/plugin group finishes, then those results are folded into the final completion payload.

.as_str()
.unwrap_or_default()
.to_string();
Ok(ExternalAgentConfigImportResultRecord {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converts the protocol completion payload into typed state DB records, preserving the full successes/failures arrays instead of derived success/failure counts.

@@ -155,13 +155,13 @@ pub fn missing_subagent_names(
Ok(names)
}

pub fn import_subagents(source_agents: &Path, target_agents: &Path) -> io::Result<usize> {
pub fn import_subagents(source_agents: &Path, target_agents: &Path) -> io::Result<Vec<String>> {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes subagent import to return the names it actually wrote, allowing the app-server import result to list migrated subagents rather than only a count.

@@ -195,13 +195,13 @@ pub fn missing_command_names(
.collect())
}

pub fn import_commands(source_commands: &Path, target_skills: &Path) -> io::Result<usize> {
pub fn import_commands(source_commands: &Path, target_skills: &Path) -> io::Result<Vec<String>> {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes command import to return the skill names it created from command files, so command migration successes can be reported by name.

@@ -0,0 +1,18 @@
CREATE TABLE external_agent_config_imports (

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creates the top-level import completion table keyed by import_id. This stores the full completion notification JSON for later lookup or diagnostics.

use sqlx::Row;
use std::path::PathBuf;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defines the DB-facing success object. It mirrors the notification success shape but uses plain strings for item_type so stored JSON is decoupled from protocol enum serialization.

pub target: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defines the DB-facing failure object with stage, message, cwd, and source so all failed migration items are stored with actionable context.

}

impl StateRuntime {
pub async fn record_external_agent_config_import_completed(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds the state runtime write API for import completion. It stores the full completion notification plus structured per-type result records inside one transaction.

completed_notification_json TEXT NOT NULL
);

CREATE TABLE external_agent_config_import_results (

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creates one ordered row per migration item type result. Successes and failures are JSON arrays of the full typed objects rather than separate count columns.

.map_err(Into::into)
}

pub async fn external_agent_config_import_result_records(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds the read API for structured result rows, deserializing successes/failures back into typed arrays and preserving original result ordering.

Ok(())
}

pub async fn external_agent_config_import_completed_notification_json(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds a lookup for the exact stored completion notification JSON by import id. This gives callers a durable copy of the must-deliver completion event.

@@ -161,6 +161,7 @@ pub(super) fn server_notification_thread_target(
| ServerNotification::AccountRateLimitsUpdated(_)
| ServerNotification::AppListUpdated(_)
| ServerNotification::RemoteControlStatusChanged(_)
| ServerNotification::ExternalAgentConfigImportProgress(_)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Treats import progress as a global/non-thread-targeted notification in the TUI routing layer, matching the completed import notification behavior.

Comment thread codex-rs/state/src/lib.rs
@@ -56,6 +56,9 @@ pub use model::ThreadGoalStatus;
pub use model::ThreadMetadata;
pub use model::ThreadMetadataBuilder;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-exports the new import-result record types from codex-state so app-server can persist typed successes and failures without depending on private runtime modules.

@@ -205,6 +205,7 @@ impl ChatWidget {
| ServerNotification::McpServerOauthLoginCompleted(_)
| ServerNotification::AppListUpdated(_)
| ServerNotification::RemoteControlStatusChanged(_)
| ServerNotification::ExternalAgentConfigImportProgress(_)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignores import progress in the chat widget protocol handler because this notification is app-server state/progress metadata, not a thread item to render in the transcript.

@charlesgong-openai
charlesgong-openai force-pushed the codex/external-agent-import-results branch 3 times, most recently from 5170105 to 09d54a0 Compare June 15, 2026 23:42
@charlesgong-openai
charlesgong-openai marked this pull request as ready for review June 15, 2026 23:44

@alexsong-oai alexsong-oai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we going to show more detailed import results in the client? including TUI, desktop app, etc?

@charlesgong-openai

Copy link
Copy Markdown
Contributor Author

Are we going to show more detailed import results in the client? including TUI, desktop app, etc?

yep

@charlesgong-openai
charlesgong-openai force-pushed the codex/external-agent-import-results branch from 1833bcf to 5b76cae Compare June 16, 2026 03:39
@charlesgong-openai
charlesgong-openai merged commit 314fa3d into main Jun 16, 2026
31 checks passed
@charlesgong-openai
charlesgong-openai deleted the codex/external-agent-import-results branch June 16, 2026 06:17
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants