//! Shared implementation for `codex archive`, `codex delete`, and `codex unarchive`.//!//! The CLI commands are thin app-server clients: resolve a user-provided UUID or exact session//! name, then call the corresponding app-server RPC.use std::io::IsTerminal;use std::io::Write;use std::sync::Arc;use crate::Cli;use crate::app_server_session::AppServerSession;use crate::legacy_core::config::ConfigBuilder;use crate::legacy_core::config::ConfigOverrides;use crate::legacy_core::config::load_config_toml_with_layer_stack;use crate::legacy_core::config::resolve_bootstrap_auth_keyring_backend_kind;use crate::legacy_core::config::resolve_oss_provider;use crate::legacy_core::config::resolve_profile_v2_config_path;use codex_app_server_protocol::Thread as AppServerThread;use codex_app_server_protocol::ThreadListParams;use codex_app_server_protocol::ThreadSortKey;use codex_arg0::Arg0DispatchPaths;use codex_cloud_config::cloud_config_bundle_loader_for_storage;use codex_config::CloudConfigBundleLoader;use codex_config::ConfigLoadOptions;use codex_config::LoaderOverrides;use codex_exec_server::EnvironmentManager;use codex_exec_server::ExecServerRuntimePaths;use codex_protocol::ThreadId;use codex_utils_cli::CliConfigOverrides;use codex_utils_home_dir::find_codex_home;use codex_utils_oss::get_default_model_for_oss_provider;use color_eyre::eyre::Result;use color_eyre::eyre::WrapErr;use color_eyre::eyre::eyre;use super::RemoteAppServerEndpoint;#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum DeleteConfirmation { Prompt, Skip,}#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum SessionArchiveAction { Archive, Delete(DeleteConfirmation), Unarchive,}pub struct SessionArchiveCommandOptions { pub cli: Cli, pub arg0_paths: Arg0DispatchPaths, pub explicit_remote_endpoint: Option<RemoteAppServerEndpoint>,}fn success_message( action: SessionArchiveAction, session_id: ThreadId, session_name: Option<&str>,) -> String { let action = match action { SessionArchiveAction::Archive => "Archived", SessionArchiveAction::Delete(_) => "Deleted", SessionArchiveAction::Unarchive => "Unarchived", }; match session_name { Some(name) => format!("{action} session {name} ({session_id})."), None => format!("{action} session {session_id}."), }}struct ResolvedSessionTarget { session_id: ThreadId, session_name: Option<String>,}pub async fn run_session_archive_command( action: SessionArchiveAction, target: String, options: SessionArchiveCommandOptions,) -> Result<String> { let mut app_server = start_app_server_for_archive_command(options).await?; run_session_archive_action_with_app_server(&mut app_server, action, &target).await}async fn run_session_archive_action_with_app_server( app_server: &mut AppServerSession, action: SessionArchiveAction, target: &str,) -> Result<String> { let resolved = resolve_session_target(app_server, action, target).await?; let session_name = match action { SessionArchiveAction::Archive => { app_server.thread_archive(resolved.session_id).await?; resolved.session_name } SessionArchiveAction::Delete(confirmation) => { if matches!(confirmation, DeleteConfirmation::Prompt) && !confirm_session_delete(&resolved)? { return Ok("Delete cancelled.".to_string()); } app_server.thread_delete(resolved.session_id).await?; resolved.session_name } SessionArchiveAction::Unarchive => { let thread = app_server.thread_unarchive(resolved.session_id).await?; thread.name.or(resolved.session_name) } }; Ok(success_message( action, resolved.session_id, session_name.as_deref(), ))}async fn resolve_session_target( app_server: &mut AppServerSession, action: SessionArchiveAction, target: &str,) -> Result<ResolvedSessionTarget> { if let Ok(session_id) = ThreadId::from_string(target) { if matches!( action, SessionArchiveAction::Delete(DeleteConfirmation::Prompt) ) { let thread = app_server .thread_read(session_id, /*include_turns*/ false) .await .with_context(|| { format!("No active or archived session found matching '{target}'.") })?; return Ok(ResolvedSessionTarget { session_id, session_name: thread.name, }); } return Ok(ResolvedSessionTarget { session_id, session_name: None, }); } let (search_scope, archived_values): (&str, &[bool]) = match action { SessionArchiveAction::Archive => ("active", &[false]), SessionArchiveAction::Delete(_) => ("active or archived", &[false, true]), SessionArchiveAction::Unarchive => ("archived", &[true]), }; for &archived in archived_values { if let Some(thread) = lookup_session_by_exact_name(app_server, target, archived).await? { return session_target_from_app_server_thread(thread); } } Err(eyre!( "No {search_scope} session found matching '{target}'." ))}async fn lookup_session_by_exact_name( app_server: &mut AppServerSession, name: &str, archived: bool,) -> Result<Option<AppServerThread>> { // Search is the fast path, but some stores attach renamed titles after applying the filter. for search_term in [Some(name), None] { let mut cursor = None; loop { let response = app_server .thread_list(ThreadListParams { cursor: cursor.clone(), limit: Some(100), sort_key: Some(ThreadSortKey::UpdatedAt), sort_direction: None, model_providers: None, source_kinds: Some(super::resume_source_kinds( /*include_non_interactive*/ false, )), archived: Some(archived), parent_thread_id: None, cwd: None, use_state_db_only: false, search_term: search_term.map(str::to_string), }) .await .wrap_err("failed to list sessions while resolving session name")?; if let Some(thread) = response .data .into_iter() .find(|thread| thread.name.as_deref() == Some(name)) { return Ok(Some(thread)); } let Some(next_cursor) = response.next_cursor else { break; }; cursor = Some(next_cursor); } } Ok(None)}fn session_target_from_app_server_thread(thread: AppServerThread) -> Result<ResolvedSessionTarget> { let session_id = ThreadId::from_string(&thread.id) .wrap_err_with(|| format!("app server returned invalid session id `{}`", thread.id))?; Ok(ResolvedSessionTarget { session_id, session_name: thread.name, })}fn confirm_session_delete(target: &ResolvedSessionTarget) -> Result<bool> { if !(std::io::stdin().is_terminal() && std::io::stderr().is_terminal()) { return Err(eyre!( "cannot confirm session deletion without an interactive terminal; rerun with --force and a session UUID" )); } let mut stderr = std::io::stderr().lock(); match target.session_name.as_deref() { Some(name) => writeln!( stderr, "Permanently delete session '{name}' ({})?", target.session_id ), None => writeln!(stderr, "Permanently delete session {}?", target.session_id), }?; writeln!( stderr, "This cannot be undone. Subagent threads will also be deleted." )?; write!(stderr, "Continue? [y/N]: ")?; stderr.flush()?; let mut input = String::new(); std::io::stdin().read_line(&mut input)?; let answer = input.trim(); Ok(answer.eq_ignore_ascii_case("y") || answer.eq_ignore_ascii_case("yes"))}async fn start_app_server_for_archive_command( options: SessionArchiveCommandOptions,) -> Result<AppServerSession> { let SessionArchiveCommandOptions { cli, arg0_paths, explicit_remote_endpoint, } = options; let loader_overrides = LoaderOverrides::default(); let strict_config = cli.strict_config; let raw_overrides = cli.config_overrides.raw_overrides.clone(); let overrides_cli = CliConfigOverrides { raw_overrides }; let cli_kv_overrides = overrides_cli .parse_overrides() .map_err(|err| eyre!("failed to parse -c overrides: {err}"))?; let codex_home = find_codex_home().wrap_err("failed to find Codex home")?; let mut launch_loader_overrides = loader_overrides.clone(); if let Some(profile_v2) = cli.config_profile_v2.as_ref() { launch_loader_overrides.user_config_path = Some(resolve_profile_v2_config_path( codex_home.as_path(), profile_v2, )); launch_loader_overrides.user_config_profile = Some(profile_v2.clone()); } let reuse_implicit_local_daemon = super::can_reuse_implicit_local_daemon( &cli_kv_overrides, &launch_loader_overrides, strict_config, cli.bypass_hook_trust, ); let default_daemon = if explicit_remote_endpoint.is_none() && reuse_implicit_local_daemon { super::maybe_probe_default_daemon_socket(codex_home.as_path()).await } else { None }; let app_server_target = super::app_server_target_for_launch( explicit_remote_endpoint, default_daemon, reuse_implicit_local_daemon, ); let remote_cwd_override = cli .cwd .clone() .filter(|_| app_server_target.uses_remote_workspace()); let local_runtime_paths = ExecServerRuntimePaths::from_optional_paths( arg0_paths.codex_self_exe.clone(), arg0_paths.codex_linux_sandbox_exe.clone(), ) .wrap_err("failed to resolve local runtime paths")?; let environment_manager = EnvironmentManager::from_env(Some(local_runtime_paths)) .await .map(Arc::new) .wrap_err("failed to initialize environment manager")?; let config_cwd = super::config_cwd_for_app_server_target( cli.cwd.as_deref(), &app_server_target, &environment_manager, ) .wrap_err("failed to resolve config cwd")?; let mut loader_overrides = loader_overrides; if let Some(profile_v2) = cli.config_profile_v2.as_ref() { loader_overrides.user_config_path = Some(resolve_profile_v2_config_path( codex_home.as_path(), profile_v2, )); loader_overrides.user_config_profile = Some(profile_v2.clone()); } let bootstrap_config = load_config_toml_with_layer_stack( codex_home.as_path(), config_cwd.as_ref(), cli_kv_overrides.clone(), ConfigLoadOptions { loader_overrides: loader_overrides.clone(), strict_config, cloud_config_bundle: CloudConfigBundleLoader::default(), }, ) .await .wrap_err("failed to load config.toml")?; let config_toml = &bootstrap_config.config_toml; let chatgpt_base_url = config_toml .chatgpt_base_url .clone() .unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string()); let cloud_config_bundle = cloud_config_bundle_loader_for_storage( codex_home.to_path_buf(), /*enable_codex_api_key_env*/ false, config_toml.cli_auth_credentials_store.unwrap_or_default(), resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)?, chatgpt_base_url, ) .await; let model_provider = if cli.oss { resolve_oss_provider(cli.oss_provider.as_deref(), config_toml) } else { None }; let model = cli.model.clone().or_else(|| { model_provider .as_deref() .and_then(get_default_model_for_oss_provider) .map(ToOwned::to_owned) }); let cwd = cli.cwd.clone(); let config = ConfigBuilder::default() .cli_overrides(cli_kv_overrides.clone()) .harness_overrides(ConfigOverrides { model, cwd: if app_server_target.uses_remote_workspace() { None } else { cwd }, model_provider, codex_self_exe: arg0_paths.codex_self_exe.clone(), codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(), main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(), show_raw_agent_reasoning: cli.oss.then_some(true), bypass_hook_trust: cli.bypass_hook_trust.then_some(true), ..Default::default() }) .loader_overrides(loader_overrides.clone()) .strict_config(strict_config) .cloud_config_bundle(cloud_config_bundle.clone()) .build() .await .wrap_err("failed to load configuration")?; let state_db = super::init_state_db_for_app_server_target(&config, &app_server_target) .await .wrap_err("failed to initialize state database")?; let app_server = super::start_app_server( &app_server_target, arg0_paths, config, cli_kv_overrides, loader_overrides, strict_config, cloud_config_bundle, codex_feedback::CodexFeedback::new(), /*log_db*/ None, state_db, environment_manager, ) .await?; Ok( AppServerSession::new(app_server, app_server_target.thread_params_mode()) .with_remote_cwd_override(remote_cwd_override), )}