Make external session detection limits configurable (#34449)

## What changed

- Add `maxSessionAgeDays` and `maxSessions` to the external-agent config detection request.
- Apply the requested limits when discovering sessions from supported migration sources.
- Preserve the existing defaults of a 30-day maximum age and 50 sessions when either option is omitted.

GitOrigin-RevId: 46376e94b64f1f61d8776e34d82cdd2cdeb36ca0
This commit is contained in:
charlesgong-openai authored and copyberry committed 2026-07-21 02:55:11 +00:00
1 parent 9078e32371
commit 3bc49e1721
18 files changed
+195 -25

No files matched your search

@@ -876,6 +876,24 @@
"description": "If true, include detection under the user's home directory.", "description": "If true, include detection under the user's home directory.",
"type": "boolean" "type": "boolean"
}, },
"maxSessionAgeDays": {
"description": "Maximum age in days for detected sessions. Missing values use the default limit.",
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"maxSessions": {
"description": "Maximum number of sessions to detect. Missing values use the default limit.",
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"migrationSource": { "migrationSource": {
"description": "Optional migration-source selector. Missing or unrecognized values use the default source.", "description": "Optional migration-source selector. Missing or unrecognized values use the default source.",
"type": [ "type": [
@@ -9577,6 +9577,24 @@
"description": "If true, include detection under the user's home directory.", "description": "If true, include detection under the user's home directory.",
"type": "boolean" "type": "boolean"
}, },
"maxSessionAgeDays": {
"description": "Maximum age in days for detected sessions. Missing values use the default limit.",
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"maxSessions": {
"description": "Maximum number of sessions to detect. Missing values use the default limit.",
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"migrationSource": { "migrationSource": {
"description": "Optional migration-source selector. Missing or unrecognized values use the default source.", "description": "Optional migration-source selector. Missing or unrecognized values use the default source.",
"type": [ "type": [
@@ -5755,6 +5755,24 @@
"description": "If true, include detection under the user's home directory.", "description": "If true, include detection under the user's home directory.",
"type": "boolean" "type": "boolean"
}, },
"maxSessionAgeDays": {
"description": "Maximum age in days for detected sessions. Missing values use the default limit.",
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"maxSessions": {
"description": "Maximum number of sessions to detect. Missing values use the default limit.",
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"migrationSource": { "migrationSource": {
"description": "Optional migration-source selector. Missing or unrecognized values use the default source.", "description": "Optional migration-source selector. Missing or unrecognized values use the default source.",
"type": [ "type": [
@@ -15,6 +15,24 @@
"description": "If true, include detection under the user's home directory.", "description": "If true, include detection under the user's home directory.",
"type": "boolean" "type": "boolean"
}, },
"maxSessionAgeDays": {
"description": "Maximum age in days for detected sessions. Missing values use the default limit.",
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"maxSessions": {
"description": "Maximum number of sessions to detect. Missing values use the default limit.",
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"migrationSource": { "migrationSource": {
"description": "Optional migration-source selector. Missing or unrecognized values use the default source.", "description": "Optional migration-source selector. Missing or unrecognized values use the default source.",
"type": [ "type": [
@@ -11,6 +11,14 @@ includeHome?: boolean,
* Zero or more working directories to include for repo-scoped detection. * Zero or more working directories to include for repo-scoped detection.
*/ */
cwds?: Array<string> | null, cwds?: Array<string> | null,
/**
* Maximum age in days for detected sessions. Missing values use the default limit.
*/
maxSessionAgeDays?: number | null,
/**
* Maximum number of sessions to detect. Missing values use the default limit.
*/
maxSessions?: number | null,
/** /**
* Deprecated field retained for compatibility. This field is ignored; use `migrationSource` * Deprecated field retained for compatibility. This field is ignored; use `migrationSource`
* to select the migration source. * to select the migration source.
@@ -699,6 +699,12 @@ pub struct ExternalAgentConfigDetectParams {
/// Zero or more working directories to include for repo-scoped detection. /// Zero or more working directories to include for repo-scoped detection.
#[ts(optional = nullable)] #[ts(optional = nullable)]
pub cwds: Option<Vec<PathBuf>>, pub cwds: Option<Vec<PathBuf>>,
/// Maximum age in days for detected sessions. Missing values use the default limit.
#[ts(optional = nullable)]
pub max_session_age_days: Option<u32>,
/// Maximum number of sessions to detect. Missing values use the default limit.
#[ts(optional = nullable)]
pub max_sessions: Option<u32>,
/// Deprecated field retained for compatibility. This field is ignored; use `migrationSource` /// Deprecated field retained for compatibility. This field is ignored; use `migrationSource`
/// to select the migration source. /// to select the migration source.
#[ts(optional = nullable)] #[ts(optional = nullable)]
@@ -1,4 +1,5 @@
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use crate::config_manager::ConfigManager; use crate::config_manager::ConfigManager;
use crate::error_code::internal_error; use crate::error_code::internal_error;
@@ -30,6 +31,7 @@ use codex_external_agent_migration::ExternalAgentConfigImportItemResult as CoreI
use codex_external_agent_migration::ExternalAgentConfigImportOutcome as CoreImportOutcome; use codex_external_agent_migration::ExternalAgentConfigImportOutcome as CoreImportOutcome;
use codex_external_agent_migration::ExternalAgentConfigMigrationItemType as CoreMigrationItemType; use codex_external_agent_migration::ExternalAgentConfigMigrationItemType as CoreMigrationItemType;
use codex_external_agent_migration::ExternalAgentConfigService; use codex_external_agent_migration::ExternalAgentConfigService;
use codex_external_agent_migration::ExternalAgentSessionImportLimits;
use codex_external_agent_migration::PluginImportOutcome; use codex_external_agent_migration::PluginImportOutcome;
use codex_external_agent_migration::record_import_error; use codex_external_agent_migration::record_import_error;
use codex_external_agent_migration::sessions::ExternalAgentSessionMigration as CoreSessionMigration; use codex_external_agent_migration::sessions::ExternalAgentSessionMigration as CoreSessionMigration;
@@ -119,6 +121,18 @@ impl ExternalAgentConfigRequestProcessor {
let migration_service = self let migration_service = self
.migration_service .migration_service
.with_migration_source(params.migration_source.as_deref()); .with_migration_source(params.migration_source.as_deref());
let default_session_import_limits = ExternalAgentSessionImportLimits::default();
let migration_service =
migration_service.with_session_import_limits(ExternalAgentSessionImportLimits {
max_age: params
.max_session_age_days
.map(|days| Duration::from_secs(u64::from(days) * 24 * 60 * 60))
.unwrap_or(default_session_import_limits.max_age),
max_sessions: params
.max_sessions
.map(|max_sessions| max_sessions as usize)
.unwrap_or(default_session_import_limits.max_sessions),
});
let options = ExternalAgentConfigDetectOptions { let options = ExternalAgentConfigDetectOptions {
include_home: params.include_home, include_home: params.include_home,
include_memory: self.external_agent_memory_import_enabled().await, include_memory: self.external_agent_memory_import_enabled().await,
@@ -378,9 +378,11 @@ impl ExternalAgentConfigService {
} }
if scope.is_home() { if scope.is_home() {
let sessions = self let sessions = self.source.recent_sessions(
.source &self.external_agent_home,
.recent_sessions(&self.external_agent_home, &self.codex_home)?; &self.codex_home,
self.session_import_limits,
)?;
if !sessions.is_empty() { if !sessions.is_empty() {
items.push(ExternalAgentConfigMigrationItem { items.push(ExternalAgentConfigMigrationItem {
item_type: ExternalAgentConfigMigrationItemType::Sessions, item_type: ExternalAgentConfigMigrationItemType::Sessions,
@@ -1,7 +1,6 @@
#[cfg(test)]
use super::common::SESSION_IMPORT_MAX_COUNT;
use super::common::SessionFileCandidate; use super::common::SessionFileCandidate;
use super::common::detect_recent_sessions; use super::common::detect_recent_sessions;
use crate::model::ExternalAgentSessionImportLimits;
use crate::sessions::ExternalAgentSessionMigration; use crate::sessions::ExternalAgentSessionMigration;
use std::fs; use std::fs;
use std::io; use std::io;
@@ -10,6 +9,18 @@ use std::path::Path;
pub fn detect_recent_cla_sessions( pub fn detect_recent_cla_sessions(
external_agent_home: &Path, external_agent_home: &Path,
codex_home: &Path, codex_home: &Path,
) -> io::Result<Vec<ExternalAgentSessionMigration>> {
detect_recent_cla_sessions_with_limits(
external_agent_home,
codex_home,
ExternalAgentSessionImportLimits::default(),
)
}
pub(crate) fn detect_recent_cla_sessions_with_limits(
external_agent_home: &Path,
codex_home: &Path,
limits: ExternalAgentSessionImportLimits,
) -> io::Result<Vec<ExternalAgentSessionMigration>> { ) -> io::Result<Vec<ExternalAgentSessionMigration>> {
let projects_root = external_agent_home.join("projects"); let projects_root = external_agent_home.join("projects");
if !projects_root.is_dir() { if !projects_root.is_dir() {
@@ -42,7 +53,9 @@ pub fn detect_recent_cla_sessions(
}); });
} }
} }
detect_recent_sessions(codex_home, candidates, /*require_existing_cwd*/ true) detect_recent_sessions(
codex_home, candidates, /*require_existing_cwd*/ true, limits,
)
} }
#[cfg(test)] #[cfg(test)]
@@ -233,7 +246,8 @@ mod tests {
let timestamp = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true); let timestamp = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
let modified_at = SystemTime::now(); let modified_at = SystemTime::now();
let mut expected = Vec::new(); let mut expected = Vec::new();
for index in 0..=SESSION_IMPORT_MAX_COUNT { let default_limits = ExternalAgentSessionImportLimits::default();
for index in 0..=default_limits.max_sessions {
let file_name = format!("{index:02}-session.jsonl"); let file_name = format!("{index:02}-session.jsonl");
let title = format!("session {index}"); let title = format!("session {index}");
let path = write_session( let path = write_session(
@@ -275,7 +289,7 @@ mod tests {
} }
let changed_at = SystemTime::now() let changed_at = SystemTime::now()
+ Duration::from_secs(/*secs*/ SESSION_IMPORT_MAX_COUNT as u64 + 1); + Duration::from_secs(/*secs*/ default_limits.max_sessions as u64 + 1);
for (index, session) in all_sessions.iter().enumerate() { for (index, session) in all_sessions.iter().enumerate() {
let title = session.title.as_deref().expect("session title"); let title = session.title.as_deref().expect("session title");
std::fs::write( std::fs::write(
@@ -1,3 +1,4 @@
use crate::model::ExternalAgentSessionImportLimits;
use crate::sessions::ExternalAgentSessionMigration; use crate::sessions::ExternalAgentSessionMigration;
use crate::sessions::ledger::load_import_ledger; use crate::sessions::ledger::load_import_ledger;
use crate::sessions::ledger::save_import_ledger; use crate::sessions::ledger::save_import_ledger;
@@ -9,10 +10,6 @@ use std::fs;
use std::io; use std::io;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration;
pub(super) const SESSION_IMPORT_MAX_COUNT: usize = 50;
const SESSION_IMPORT_MAX_AGE: Duration = Duration::from_secs(30 * 24 * 60 * 60);
pub(super) struct SessionFileCandidate { pub(super) struct SessionFileCandidate {
pub path: PathBuf, pub path: PathBuf,
@@ -23,11 +20,12 @@ pub(super) fn detect_recent_sessions(
codex_home: &Path, codex_home: &Path,
candidates: impl IntoIterator<Item = SessionFileCandidate>, candidates: impl IntoIterator<Item = SessionFileCandidate>,
require_existing_cwd: bool, require_existing_cwd: bool,
limits: ExternalAgentSessionImportLimits,
) -> io::Result<Vec<ExternalAgentSessionMigration>> { ) -> io::Result<Vec<ExternalAgentSessionMigration>> {
let now = now_unix_seconds(); let now = now_unix_seconds();
let mut ledger = load_import_ledger(codex_home)?; let mut ledger = load_import_ledger(codex_home)?;
let source_states = ledger.source_states(); let source_states = ledger.source_states();
let mut recent = BinaryHeap::with_capacity(SESSION_IMPORT_MAX_COUNT + 1); let mut recent = BinaryHeap::new();
for candidate in candidates { for candidate in candidates {
let Ok(metadata) = fs::metadata(&candidate.path) else { let Ok(metadata) = fs::metadata(&candidate.path) else {
@@ -39,9 +37,7 @@ pub(super) fn detect_recent_sessions(
let Ok(modified_at) = modified_at.duration_since(std::time::UNIX_EPOCH) else { let Ok(modified_at) = modified_at.duration_since(std::time::UNIX_EPOCH) else {
continue; continue;
}; };
if (modified_at.as_secs() as i64) if (modified_at.as_secs() as i64) < now.saturating_sub(limits.max_age.as_secs() as i64) {
< now.saturating_sub(SESSION_IMPORT_MAX_AGE.as_secs() as i64)
{
continue; continue;
} }
let Ok(modified_at_nanos) = i64::try_from(modified_at.as_nanos()) else { let Ok(modified_at_nanos) = i64::try_from(modified_at.as_nanos()) else {
@@ -62,7 +58,7 @@ pub(super) fn detect_recent_sessions(
candidate.path, candidate.path,
candidate.fallback_cwd, candidate.fallback_cwd,
)); ));
if recent.len() > SESSION_IMPORT_MAX_COUNT { if recent.len() > limits.max_sessions {
recent.pop(); recent.pop();
} }
} }
@@ -1,7 +1,6 @@
#[cfg(test)]
use super::common::SESSION_IMPORT_MAX_COUNT;
use super::common::SessionFileCandidate; use super::common::SessionFileCandidate;
use super::common::detect_recent_sessions; use super::common::detect_recent_sessions;
use crate::model::ExternalAgentSessionImportLimits;
use crate::sessions::ExternalAgentSessionMigration; use crate::sessions::ExternalAgentSessionMigration;
use std::fs; use std::fs;
use std::io; use std::io;
@@ -11,6 +10,18 @@ use std::path::PathBuf;
pub fn detect_recent_cur_sessions( pub fn detect_recent_cur_sessions(
external_agent_home: &Path, external_agent_home: &Path,
codex_home: &Path, codex_home: &Path,
) -> io::Result<Vec<ExternalAgentSessionMigration>> {
detect_recent_cur_sessions_with_limits(
external_agent_home,
codex_home,
ExternalAgentSessionImportLimits::default(),
)
}
pub(crate) fn detect_recent_cur_sessions_with_limits(
external_agent_home: &Path,
codex_home: &Path,
limits: ExternalAgentSessionImportLimits,
) -> io::Result<Vec<ExternalAgentSessionMigration>> { ) -> io::Result<Vec<ExternalAgentSessionMigration>> {
let projects_root = external_agent_home.join("projects"); let projects_root = external_agent_home.join("projects");
if !projects_root.is_dir() { if !projects_root.is_dir() {
@@ -34,7 +45,9 @@ pub fn detect_recent_cur_sessions(
}); });
} }
} }
detect_recent_sessions(codex_home, candidates, /*require_existing_cwd*/ false) detect_recent_sessions(
codex_home, candidates, /*require_existing_cwd*/ false, limits,
)
} }
fn cur_transcript_files(transcripts_root: &Path) -> Vec<PathBuf> { fn cur_transcript_files(transcripts_root: &Path) -> Vec<PathBuf> {
@@ -171,7 +171,8 @@ fn detects_cur_sessions_in_batches_and_redetects_modified_imports() {
let encoded_project = encode_project_path(&project_root); let encoded_project = encode_project_path(&project_root);
let modified_at = SystemTime::now(); let modified_at = SystemTime::now();
let mut expected = Vec::new(); let mut expected = Vec::new();
for index in 0..=SESSION_IMPORT_MAX_COUNT { let default_limits = ExternalAgentSessionImportLimits::default();
for index in 0..=default_limits.max_sessions {
let session_id = format!("session-{index:02}"); let session_id = format!("session-{index:02}");
let title = format!("request {index}"); let title = format!("request {index}");
let path = write_transcript(&external_agent_home, &encoded_project, &session_id, &title); let path = write_transcript(&external_agent_home, &encoded_project, &session_id, &title);
@@ -4,6 +4,8 @@ mod connectors_cla;
mod cur; mod cur;
pub use cla::detect_recent_cla_sessions; pub use cla::detect_recent_cla_sessions;
pub(crate) use cla::detect_recent_cla_sessions_with_limits;
pub use connectors_cla::ImportedSessionConnectorAttribution; pub use connectors_cla::ImportedSessionConnectorAttribution;
pub use connectors_cla::detect_imported_cla_session_connectors; pub use connectors_cla::detect_imported_cla_session_connectors;
pub use cur::detect_recent_cur_sessions; pub use cur::detect_recent_cur_sessions;
pub(crate) use cur::detect_recent_cur_sessions_with_limits;
@@ -64,6 +64,7 @@ pub use service::ExternalAgentConfigImportSuccess;
pub use service::ExternalAgentConfigMigrationItem; pub use service::ExternalAgentConfigMigrationItem;
pub use service::ExternalAgentConfigMigrationItemType; pub use service::ExternalAgentConfigMigrationItemType;
pub use service::ExternalAgentConfigService; pub use service::ExternalAgentConfigService;
pub use service::ExternalAgentSessionImportLimits;
pub use service::MigrationDetails; pub use service::MigrationDetails;
pub use service::NamedMigration; pub use service::NamedMigration;
pub use service::PendingPluginImport; pub use service::PendingPluginImport;
@@ -2,8 +2,9 @@ use crate::ClaSource;
use crate::CurSource; use crate::CurSource;
use crate::RewriteProfile; use crate::RewriteProfile;
use crate::detect::plugins; use crate::detect::plugins;
use crate::detect::sessions::detect_recent_cla_sessions; use crate::detect::sessions::detect_recent_cla_sessions_with_limits;
use crate::detect::sessions::detect_recent_cur_sessions; use crate::detect::sessions::detect_recent_cur_sessions_with_limits;
use crate::model::ExternalAgentSessionImportLimits;
use crate::sessions::ExternalAgentSessionMigration; use crate::sessions::ExternalAgentSessionMigration;
use crate::sessions::SessionMetadataMode; use crate::sessions::SessionMetadataMode;
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
@@ -122,10 +123,15 @@ impl ExternalAgentSource {
self, self,
external_agent_home: &Path, external_agent_home: &Path,
codex_home: &Path, codex_home: &Path,
limits: ExternalAgentSessionImportLimits,
) -> io::Result<Vec<ExternalAgentSessionMigration>> { ) -> io::Result<Vec<ExternalAgentSessionMigration>> {
match self { match self {
Self::Cla => detect_recent_cla_sessions(external_agent_home, codex_home), Self::Cla => {
Self::Cur => detect_recent_cur_sessions(external_agent_home, codex_home), detect_recent_cla_sessions_with_limits(external_agent_home, codex_home, limits)
}
Self::Cur => {
detect_recent_cur_sessions_with_limits(external_agent_home, codex_home, limits)
}
} }
} }
@@ -1,5 +1,27 @@
use crate::sessions::ExternalAgentSessionMigration; use crate::sessions::ExternalAgentSessionMigration;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration;
const DEFAULT_SESSION_IMPORT_MAX_AGE: Duration = Duration::from_secs(30 * 24 * 60 * 60);
const DEFAULT_SESSION_IMPORT_MAX_COUNT: usize = 50;
/// Bounds session discovery for an external-agent import.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExternalAgentSessionImportLimits {
/// Oldest source-session modification age that remains eligible.
pub max_age: Duration,
/// Maximum number of eligible sessions returned by detection.
pub max_sessions: usize,
}
impl Default for ExternalAgentSessionImportLimits {
fn default() -> Self {
Self {
max_age: DEFAULT_SESSION_IMPORT_MAX_AGE,
max_sessions: DEFAULT_SESSION_IMPORT_MAX_COUNT,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExternalAgentConfigDetectOptions { pub struct ExternalAgentConfigDetectOptions {
@@ -13,6 +13,7 @@ pub use crate::model::ExternalAgentConfigImportRawError;
pub use crate::model::ExternalAgentConfigImportSuccess; pub use crate::model::ExternalAgentConfigImportSuccess;
pub use crate::model::ExternalAgentConfigMigrationItem; pub use crate::model::ExternalAgentConfigMigrationItem;
pub use crate::model::ExternalAgentConfigMigrationItemType; pub use crate::model::ExternalAgentConfigMigrationItemType;
pub use crate::model::ExternalAgentSessionImportLimits;
pub use crate::model::MigrationDetails; pub use crate::model::MigrationDetails;
pub use crate::model::NamedMigration; pub use crate::model::NamedMigration;
pub use crate::model::PendingPluginImport; pub use crate::model::PendingPluginImport;
@@ -64,6 +65,7 @@ pub struct ExternalAgentConfigService {
pub(crate) external_agent_home: PathBuf, pub(crate) external_agent_home: PathBuf,
pub(crate) analytics_events_client: Option<AnalyticsEventsClient>, pub(crate) analytics_events_client: Option<AnalyticsEventsClient>,
pub(crate) source: ExternalAgentSource, pub(crate) source: ExternalAgentSource,
pub(crate) session_import_limits: ExternalAgentSessionImportLimits,
state_db: Option<StateDbHandle>, state_db: Option<StateDbHandle>,
} }
@@ -82,6 +84,7 @@ impl ExternalAgentConfigService {
external_agent_home, external_agent_home,
analytics_events_client: Some(analytics_events_client), analytics_events_client: Some(analytics_events_client),
source, source,
session_import_limits: ExternalAgentSessionImportLimits::default(),
state_db, state_db,
} }
} }
@@ -96,10 +99,17 @@ impl ExternalAgentConfigService {
external_agent_home, external_agent_home,
analytics_events_client: self.analytics_events_client.clone(), analytics_events_client: self.analytics_events_client.clone(),
source, source,
session_import_limits: self.session_import_limits,
state_db: self.state_db.clone(), state_db: self.state_db.clone(),
} }
} }
pub fn with_session_import_limits(&self, limits: ExternalAgentSessionImportLimits) -> Self {
let mut service = self.clone();
service.session_import_limits = limits;
service
}
pub fn session_metadata_mode(&self) -> SessionMetadataMode { pub fn session_metadata_mode(&self) -> SessionMetadataMode {
self.source.session_metadata_mode() self.source.session_metadata_mode()
} }
@@ -122,6 +132,7 @@ impl ExternalAgentConfigService {
external_agent_home, external_agent_home,
analytics_events_client: None, analytics_events_client: None,
source, source,
session_import_limits: ExternalAgentSessionImportLimits::default(),
state_db: None, state_db: None,
} }
} }
@@ -274,6 +274,8 @@ pub(crate) async fn handle_external_agent_config_migration_prompt(
.external_agent_config_detect(ExternalAgentConfigDetectParams { .external_agent_config_detect(ExternalAgentConfigDetectParams {
include_home: true, include_home: true,
cwds: Some(vec![cwd.clone()]), cwds: Some(vec![cwd.clone()]),
max_session_age_days: None,
max_sessions: None,
source: None, source: None,
migration_source: Some(source.migration_source().to_string()), migration_source: Some(source.migration_source().to_string()),
}) })