Files
codex/codex-rs/external-agent-sessions/src/ledger.rs
T
stefanstokic-oai 4c68bd728f External agent session support (#19895)
## Summary

This extends external agent detection/import beyond config artifacts so
Codex can detect recent sessions files from the external agent home and
import them into Codex rollout history.

## What changed

- Added a focused `external_agent_sessions` module for:
  - session discovery
  - source-record parsing
  - rollout construction
  - import ledger tracking
- Wired session detection/import into the app-server external agent
config API.
- Added compaction handling so large imported sessions can be resumed
safely before the first follow-up turn.

## Testing

Added coverage for:
- recent-session detection
- custom-title handling
- recency filtering
- dedupe and re-detect-after-source-change behavior
- visible imported turn construction
- backward-compatible import payload deserialization
- end-to-end RPC import flow
- rejection of undetected session paths
- repeat-import behavior
- large-session compaction before first follow-up

Ran:
- `cargo test -p codex-app-server external_agent_config_import_ --test
all`
2026-04-28 17:42:36 +00:00

109 lines
3.2 KiB
Rust

use crate::now_unix_seconds;
use codex_protocol::ThreadId;
use serde::Deserialize;
use serde::Serialize;
use sha2::Digest;
use sha2::Sha256;
use std::fs;
use std::io;
use std::path::Path;
use std::path::PathBuf;
const SESSION_IMPORT_LEDGER_FILE: &str = "external_agent_session_imports.json";
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub(super) struct ImportedExternalAgentSessionLedger {
records: Vec<ImportedExternalAgentSessionRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct ImportedExternalAgentSessionRecord {
source_path: PathBuf,
content_sha256: String,
imported_thread_id: ThreadId,
imported_at: i64,
}
pub fn has_current_session_been_imported(
codex_home: &Path,
source_path: &Path,
) -> io::Result<bool> {
load_import_ledger(codex_home)?.contains_current_source(source_path)
}
pub fn record_imported_session(
codex_home: &Path,
source_path: &Path,
imported_thread_id: ThreadId,
) -> io::Result<()> {
let mut ledger = load_import_ledger(codex_home)?;
let source_path = canonical_source_path(source_path)?;
let content_sha256 = session_content_sha256(&source_path)?;
if ledger
.records
.iter()
.any(|record| record.source_path == source_path && record.content_sha256 == content_sha256)
{
return Ok(());
}
ledger.records.push(ImportedExternalAgentSessionRecord {
source_path,
content_sha256,
imported_thread_id,
imported_at: now_unix_seconds(),
});
save_import_ledger(codex_home, &ledger)
}
impl ImportedExternalAgentSessionLedger {
pub(super) fn contains_current_source(&self, source_path: &Path) -> io::Result<bool> {
let source_path = canonical_source_path(source_path)?;
let content_sha256 = session_content_sha256(&source_path)?;
Ok(self.records.iter().any(|record| {
record.source_path == source_path && record.content_sha256 == content_sha256
}))
}
}
pub(super) fn load_import_ledger(
codex_home: &Path,
) -> io::Result<ImportedExternalAgentSessionLedger> {
let path = import_ledger_path(codex_home);
let raw = match fs::read_to_string(path) {
Ok(raw) => raw,
Err(err) if err.kind() == io::ErrorKind::NotFound => {
return Ok(ImportedExternalAgentSessionLedger::default());
}
Err(err) => return Err(err),
};
serde_json::from_str(&raw).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("invalid external agent session import ledger: {err}"),
)
})
}
fn save_import_ledger(
codex_home: &Path,
ledger: &ImportedExternalAgentSessionLedger,
) -> io::Result<()> {
fs::create_dir_all(codex_home)?;
let path = import_ledger_path(codex_home);
let raw = serde_json::to_vec_pretty(ledger).map_err(io::Error::other)?;
fs::write(path, raw)
}
fn import_ledger_path(codex_home: &Path) -> PathBuf {
codex_home.join(SESSION_IMPORT_LEDGER_FILE)
}
fn canonical_source_path(path: &Path) -> io::Result<PathBuf> {
fs::canonicalize(path)
}
fn session_content_sha256(path: &Path) -> io::Result<String> {
let contents = fs::read(path)?;
Ok(format!("{:x}", Sha256::digest(contents)))
}