Import external agent sessions in background (#20284)

Summary:
- Return from external agent import before session history import
finishes
- Run session import work in the background and emit the existing
completion notification when it is done
- Serialize session imports so duplicate requests do not create
duplicate imported threads

Verification:
- cargo test -p codex-app-server external_agent_config_
- cargo test -p codex-external-agent-sessions
- just fix -p codex-app-server
- just fix -p codex-external-agent-sessions
- git diff --check
This commit is contained in:
stefanstokic-oai
2026-04-29 17:00:41 -07:00
committed by GitHub
Unverified
parent 7bcd4626c4
commit c8abcbf925
8 changed files with 465 additions and 57 deletions
@@ -94,6 +94,37 @@ pub fn prepare_pending_session_imports(
Ok(pending_session_imports)
}
pub fn prepare_validated_session_imports(
codex_home: &Path,
requested_sessions: Vec<ExternalAgentSessionMigration>,
) -> Vec<PendingSessionImport> {
requested_sessions
.into_iter()
.filter_map(|session| pending_session_import(codex_home, session))
.collect()
}
fn pending_session_import(
codex_home: &Path,
session: ExternalAgentSessionMigration,
) -> Option<PendingSessionImport> {
let has_been_imported = match has_current_session_been_imported(codex_home, &session.path) {
Ok(has_been_imported) => has_been_imported,
Err(_) => return None,
};
if has_been_imported {
return None;
}
let imported_session = match load_importable_session(&session.path) {
Ok(Some(imported_session)) => imported_session,
Ok(None) | Err(_) => return None,
};
Some(PendingSessionImport {
source_path: session.path,
session: imported_session,
})
}
fn load_importable_session(path: &Path) -> io::Result<Option<ImportedExternalAgentSession>> {
let Some(imported_session) = load_session_for_import(path)? else {
return Ok(None);
@@ -13,6 +13,8 @@ use std::path::PathBuf;
const NOTE_MAX_LEN: usize = 2_000;
const TOOL_RESULT_MAX_LEN: usize = 4_000;
const EXTERNAL_AGENT_TOOL_CALL_TAG: &str = "external_agent_tool_call";
const EXTERNAL_AGENT_TOOL_RESULT_TAG: &str = "external_agent_tool_result";
pub struct SessionSummary {
pub latest_timestamp: i64,
@@ -252,7 +254,7 @@ fn tool_call_note(block: &JsonValue) -> String {
.get("name")
.and_then(JsonValue::as_str)
.unwrap_or("unknown");
let mut lines = vec![format!("[external tool call: {name}]")];
let mut lines = vec![format!("[{EXTERNAL_AGENT_TOOL_CALL_TAG}: {name}]")];
if let Some(input) = block.get("input").and_then(JsonValue::as_object) {
if let Some(description) = input.get("description").and_then(JsonValue::as_str) {
lines.push(format!("description: {description}"));
@@ -279,20 +281,24 @@ fn tool_call_note(block: &JsonValue) -> String {
truncate(&input.to_string(), NOTE_MAX_LEN)
));
}
lines.push(format!("[/{EXTERNAL_AGENT_TOOL_CALL_TAG}]"));
lines.join("\n")
}
fn tool_result_note(block: &JsonValue) -> String {
let label = if block.get("is_error").and_then(JsonValue::as_bool) == Some(true) {
"[external tool result: error]"
format!("[{EXTERNAL_AGENT_TOOL_RESULT_TAG}: error]")
} else {
"[external tool result]"
format!("[{EXTERNAL_AGENT_TOOL_RESULT_TAG}]")
};
let text = tool_result_text(block.get("content"));
if text.is_empty() {
label.to_string()
format!("{label}\n[/{EXTERNAL_AGENT_TOOL_RESULT_TAG}]")
} else {
format!("{label}\n{}", truncate(&text, TOOL_RESULT_MAX_LEN))
format!(
"{label}\n{}\n[/{EXTERNAL_AGENT_TOOL_RESULT_TAG}]",
truncate(&text, TOOL_RESULT_MAX_LEN)
)
}
}
@@ -314,3 +320,59 @@ fn parse_timestamp(timestamp: &str) -> Option<i64> {
.ok()
.map(|value| value.timestamp())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn converts_tool_use_blocks_to_bounded_external_agent_tags() {
let block = serde_json::json!({
"type": "tool_use",
"name": "Bash",
"input": {
"description": "Check repo status",
"command": "git status --short"
}
});
assert_eq!(
tool_call_note(&block),
"[external_agent_tool_call: Bash]\n\
description: Check repo status\n\
command: git status --short\n\
[/external_agent_tool_call]"
);
}
#[test]
fn converts_tool_result_blocks_to_bounded_external_agent_tags() {
let block = serde_json::json!({
"type": "tool_result",
"content": "codex-rs/external-agent-sessions/src/records.rs"
});
assert_eq!(
tool_result_note(&block),
"[external_agent_tool_result]\n\
codex-rs/external-agent-sessions/src/records.rs\n\
[/external_agent_tool_result]"
);
}
#[test]
fn converts_error_tool_result_blocks_to_bounded_external_agent_tags() {
let block = serde_json::json!({
"type": "tool_result",
"is_error": true,
"content": "command failed"
});
assert_eq!(
tool_result_note(&block),
"[external_agent_tool_result: error]\n\
command failed\n\
[/external_agent_tool_result]"
);
}
}