Codex Handbook
tui/src/app_server_session.rs 2575 lines
//! App-server session facade used by the TUI event loop.//!//! This module owns the typed JSON-RPC calls needed by the TUI and keeps//! request/response plumbing out of `App` and `ChatWidget`.mod fs;use crate::bottom_pane::FeedbackAudience;use crate::legacy_core::config::Config;use crate::permission_compat::legacy_compatible_permission_profile;use crate::service_tier_resolution;use crate::session_state::MessageHistoryMetadata;use crate::session_state::ThreadSessionState;use crate::status::StatusAccountDisplay;use crate::status::plan_type_display_name;use crate::terminal_visualization_instructions::with_terminal_visualization_instructions;use codex_app_server_client::AppServerClient;use codex_app_server_client::AppServerEvent;use codex_app_server_client::AppServerPath;use codex_app_server_client::AppServerRequestHandle;use codex_app_server_client::TypedRequestError;use codex_app_server_protocol::Account;use codex_app_server_protocol::AskForApproval;use codex_app_server_protocol::AuthMode;use codex_app_server_protocol::ClientRequest;use codex_app_server_protocol::ConfigBatchWriteParams;use codex_app_server_protocol::ConfigWriteResponse;use codex_app_server_protocol::ExternalAgentConfigDetectParams;use codex_app_server_protocol::ExternalAgentConfigDetectResponse;use codex_app_server_protocol::ExternalAgentConfigImportParams;use codex_app_server_protocol::ExternalAgentConfigImportResponse;use codex_app_server_protocol::ExternalAgentConfigMigrationItem;use codex_app_server_protocol::GetAccountParams;use codex_app_server_protocol::GetAccountRateLimitsResponse;use codex_app_server_protocol::GetAccountResponse;use codex_app_server_protocol::JSONRPCErrorError;use codex_app_server_protocol::LogoutAccountResponse;use codex_app_server_protocol::MemoryResetResponse;use codex_app_server_protocol::Model as ApiModel;use codex_app_server_protocol::ModelListParams;use codex_app_server_protocol::ModelListResponse;use codex_app_server_protocol::RateLimitSnapshot;use codex_app_server_protocol::RequestId;use codex_app_server_protocol::ReviewDelivery;use codex_app_server_protocol::ReviewStartParams;use codex_app_server_protocol::ReviewStartResponse;use codex_app_server_protocol::ReviewTarget;use codex_app_server_protocol::SkillsListParams;use codex_app_server_protocol::SkillsListResponse;use codex_app_server_protocol::Thread;use codex_app_server_protocol::ThreadApproveGuardianDeniedActionParams;use codex_app_server_protocol::ThreadApproveGuardianDeniedActionResponse;use codex_app_server_protocol::ThreadArchiveParams;use codex_app_server_protocol::ThreadArchiveResponse;use codex_app_server_protocol::ThreadBackgroundTerminalsCleanParams;use codex_app_server_protocol::ThreadBackgroundTerminalsCleanResponse;use codex_app_server_protocol::ThreadCompactStartParams;use codex_app_server_protocol::ThreadCompactStartResponse;use codex_app_server_protocol::ThreadDeleteParams;use codex_app_server_protocol::ThreadDeleteResponse;use codex_app_server_protocol::ThreadForkParams;use codex_app_server_protocol::ThreadForkResponse;use codex_app_server_protocol::ThreadGoalClearParams;use codex_app_server_protocol::ThreadGoalClearResponse;use codex_app_server_protocol::ThreadGoalGetParams;use codex_app_server_protocol::ThreadGoalGetResponse;use codex_app_server_protocol::ThreadGoalSetParams;use codex_app_server_protocol::ThreadGoalSetResponse;use codex_app_server_protocol::ThreadGoalStatus;use codex_app_server_protocol::ThreadInjectItemsParams;use codex_app_server_protocol::ThreadInjectItemsResponse;use codex_app_server_protocol::ThreadListParams;use codex_app_server_protocol::ThreadListResponse;use codex_app_server_protocol::ThreadLoadedListParams;use codex_app_server_protocol::ThreadLoadedListResponse;use codex_app_server_protocol::ThreadMemoryMode;use codex_app_server_protocol::ThreadMemoryModeSetParams;use codex_app_server_protocol::ThreadMemoryModeSetResponse;use codex_app_server_protocol::ThreadMetadataGitInfoUpdateParams;use codex_app_server_protocol::ThreadMetadataUpdateParams;use codex_app_server_protocol::ThreadMetadataUpdateResponse;use codex_app_server_protocol::ThreadReadParams;use codex_app_server_protocol::ThreadReadResponse;use codex_app_server_protocol::ThreadResumeParams;use codex_app_server_protocol::ThreadResumeResponse;use codex_app_server_protocol::ThreadRollbackParams;use codex_app_server_protocol::ThreadRollbackResponse;use codex_app_server_protocol::ThreadSetNameParams;use codex_app_server_protocol::ThreadSetNameResponse;use codex_app_server_protocol::ThreadSettingsUpdateParams;use codex_app_server_protocol::ThreadSettingsUpdateResponse;use codex_app_server_protocol::ThreadShellCommandParams;use codex_app_server_protocol::ThreadShellCommandResponse;use codex_app_server_protocol::ThreadSource;use codex_app_server_protocol::ThreadStartParams;use codex_app_server_protocol::ThreadStartResponse;use codex_app_server_protocol::ThreadStartSource;use codex_app_server_protocol::ThreadUnarchiveParams;use codex_app_server_protocol::ThreadUnarchiveResponse;use codex_app_server_protocol::ThreadUnsubscribeParams;use codex_app_server_protocol::ThreadUnsubscribeResponse;use codex_app_server_protocol::Turn;use codex_app_server_protocol::TurnInterruptParams;use codex_app_server_protocol::TurnInterruptResponse;use codex_app_server_protocol::TurnStartParams;use codex_app_server_protocol::TurnStartResponse;use codex_app_server_protocol::TurnSteerParams;use codex_app_server_protocol::TurnSteerResponse;use codex_app_server_protocol::UserInput;use codex_otel::TelemetryAuthMode;use codex_protocol::ThreadId;use codex_protocol::approvals::GuardianAssessmentEvent;use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;use codex_protocol::models::ActivePermissionProfile;use codex_protocol::models::PermissionProfile;use codex_protocol::models::ResponseItem;use codex_protocol::openai_models::ModelAvailabilityNux;use codex_protocol::openai_models::ModelPreset;use codex_protocol::openai_models::ModelServiceTier;use codex_protocol::openai_models::ModelUpgrade;use codex_protocol::openai_models::ReasoningEffortPreset;use codex_utils_absolute_path::AbsolutePathBuf;use color_eyre::eyre::ContextCompat;use color_eyre::eyre::Result;use color_eyre::eyre::WrapErr;use std::collections::HashMap;use std::path::PathBuf;use std::sync::atomic::AtomicBool;use std::sync::atomic::Ordering;use std::time::Duration;use std::time::Instant;use uuid::Uuid;const JSONRPC_INVALID_REQUEST: i64 = -32600;const JSONRPC_METHOD_NOT_FOUND: i64 = -32601;pub(crate) const EXTERNAL_AGENT_CONFIG_IMPORT_IN_PROGRESS_MESSAGE: &str =    "A previous Claude Code import is still running. Wait for it to finish before importing again.";const THREAD_SETTINGS_UPDATE_METHOD: &str = "thread/settings/update";fn bootstrap_request_error(context: &'static str, err: TypedRequestError) -> color_eyre::Report {    color_eyre::eyre::eyre!("{context}: {err}")}fn is_thread_settings_update_unsupported(source: &JSONRPCErrorError) -> bool {    source.code == JSONRPC_METHOD_NOT_FOUND        || (source.code == JSONRPC_INVALID_REQUEST            && source.message.contains(THREAD_SETTINGS_UPDATE_METHOD))}/// Data collected during the TUI bootstrap phase that the main event loop/// needs to configure the UI, telemetry, and initial rate-limit prefetch.////// Rate-limit snapshots are intentionally **not** included here; they are/// fetched asynchronously after bootstrap returns so that the TUI can render/// its first frame without waiting for the rate-limit round-trip.pub(crate) struct AppServerBootstrap {    pub(crate) duration: Duration,    pub(crate) account_email: Option<String>,    pub(crate) auth_mode: Option<TelemetryAuthMode>,    pub(crate) status_account_display: Option<StatusAccountDisplay>,    pub(crate) plan_type: Option<codex_protocol::account::PlanType>,    /// Whether the configured model provider needs OpenAI-style auth. Combined    /// with `has_chatgpt_account` to decide if a startup rate-limit prefetch    /// should be fired.    pub(crate) requires_openai_auth: bool,    pub(crate) default_model: String,    pub(crate) feedback_audience: FeedbackAudience,    pub(crate) has_chatgpt_account: bool,    pub(crate) available_models: Vec<ModelPreset>,}pub(crate) struct AppServerSession {    client: AppServerClient,    next_request_id: i64,    remote_cwd_override: Option<PathBuf>,    thread_params_mode: ThreadParamsMode,    thread_settings_update_supported: bool,    default_model: Option<String>,    available_models: Vec<ModelPreset>,    external_agent_config_import_completion_pending: AtomicBool,}#[derive(Clone, Copy, Debug, PartialEq, Eq)]pub(crate) enum ThreadParamsMode {    Embedded,    Remote,}impl ThreadParamsMode {    fn model_provider_from_config(self, config: &Config) -> Option<String> {        match self {            Self::Embedded => Some(config.model_provider_id.clone()),            Self::Remote => None,        }    }}#[derive(Debug)]pub(crate) struct AppServerStartedThread {    pub(crate) session: ThreadSessionState,    pub(crate) turns: Vec<Turn>,}#[derive(Debug, Clone, PartialEq, Eq)]pub(crate) enum TurnPermissionsOverride {    /// Leave the app-server thread's sticky permission profile unchanged.    Preserve,    /// Select a named or built-in profile by id.    ActiveProfile(ActivePermissionProfile),    /// Apply a user-selected legacy/custom permission profile.    LegacySandbox(PermissionProfile),}impl AppServerSession {    pub(crate) fn new(client: AppServerClient, thread_params_mode: ThreadParamsMode) -> Self {        Self {            client,            next_request_id: 1,            remote_cwd_override: None,            thread_params_mode,            thread_settings_update_supported: true,            default_model: None,            available_models: Vec::new(),            external_agent_config_import_completion_pending: AtomicBool::new(false),        }    }    pub(crate) fn with_remote_cwd_override(mut self, remote_cwd_override: Option<PathBuf>) -> Self {        self.remote_cwd_override = remote_cwd_override;        self    }    pub(crate) fn remote_cwd_override(&self) -> Option<&std::path::Path> {        self.remote_cwd_override.as_deref()    }    pub(crate) fn uses_remote_workspace(&self) -> bool {        matches!(self.thread_params_mode, ThreadParamsMode::Remote)    }    pub(crate) fn uses_embedded_app_server(&self) -> bool {        matches!(&self.client, AppServerClient::InProcess(_))    }    pub(crate) fn codex_home_path(        &self,        local_codex_home: &AbsolutePathBuf,    ) -> Option<AppServerPath> {        self.client.codex_home(local_codex_home)    }    pub(crate) fn server_version(&self) -> Option<&str> {        let AppServerClient::Remote(client) = &self.client else {            return None;        };        client.server_version()    }    pub(crate) async fn bootstrap(&mut self, config: &Config) -> Result<AppServerBootstrap> {        let started_at = Instant::now();        let account = self.read_account().await?;        let model_request_id = self.next_request_id();        let models: ModelListResponse = self            .client            .request_typed(ClientRequest::ModelList {                request_id: model_request_id,                params: ModelListParams {                    cursor: None,                    limit: None,                    include_hidden: Some(true),                },            })            .await            .map_err(|err| {                bootstrap_request_error("model/list failed during TUI bootstrap", err)            })?;        let available_models = models            .data            .into_iter()            .map(model_preset_from_api_model)            .collect::<Vec<_>>();        let default_model = config            .model            .clone()            .or_else(|| {                available_models                    .iter()                    .find(|model| model.is_default)                    .map(|model| model.model.clone())            })            .or_else(|| available_models.first().map(|model| model.model.clone()))            .wrap_err("model/list returned no models for TUI bootstrap")?;        self.default_model = Some(default_model.clone());        self.available_models = available_models.clone();        let (            account_email,            auth_mode,            status_account_display,            plan_type,            feedback_audience,            has_chatgpt_account,        ) = match account.account {            Some(Account::ApiKey {}) => (                None,                Some(TelemetryAuthMode::ApiKey),                Some(StatusAccountDisplay::ApiKey),                None,                FeedbackAudience::External,                false,            ),            Some(Account::Chatgpt { email, plan_type }) => {                let feedback_audience = if email.ends_with("@openai.com") {                    FeedbackAudience::OpenAiEmployee                } else {                    FeedbackAudience::External                };                (                    Some(email.clone()),                    Some(TelemetryAuthMode::Chatgpt),                    Some(StatusAccountDisplay::ChatGpt {                        email: Some(email),                        plan: Some(plan_type_display_name(plan_type)),                    }),                    Some(plan_type),                    feedback_audience,                    true,                )            }            Some(Account::AmazonBedrock { .. }) => {                (None, None, None, None, FeedbackAudience::External, false)            }            None => (None, None, None, None, FeedbackAudience::External, false),        };        Ok(AppServerBootstrap {            duration: started_at.elapsed(),            account_email,            auth_mode,            status_account_display,            plan_type,            requires_openai_auth: account.requires_openai_auth,            default_model,            feedback_audience,            has_chatgpt_account,            available_models,        })    }    /// Fetches the current account info without refreshing the auth token.    ///    /// Used by both `bootstrap` (to populate the initial UI) and `get_login_status`    /// (to check auth mode without the overhead of a full bootstrap).    pub(crate) async fn read_account(&mut self) -> Result<GetAccountResponse> {        let account_request_id = self.next_request_id();        self.client            .request_typed(ClientRequest::GetAccount {                request_id: account_request_id,                params: GetAccountParams {                    refresh_token: false,                },            })            .await            .map_err(|err| bootstrap_request_error("account/read failed during TUI bootstrap", err))    }    pub(crate) async fn external_agent_config_detect(        &mut self,        params: ExternalAgentConfigDetectParams,    ) -> Result<ExternalAgentConfigDetectResponse> {        let request_id = self.next_request_id();        self.client            .request_typed(ClientRequest::ExternalAgentConfigDetect { request_id, params })            .await            .wrap_err("externalAgentConfig/detect failed during Claude Code import")    }    pub(crate) async fn external_agent_config_import(        &mut self,        migration_items: Vec<ExternalAgentConfigMigrationItem>,    ) -> Result<()> {        // Mark the import active before sending the request so a fast completion notification        // cannot arrive before the TUI records it.        if self            .external_agent_config_import_completion_pending            .swap(true, Ordering::Relaxed)        {            color_eyre::eyre::bail!(EXTERNAL_AGENT_CONFIG_IMPORT_IN_PROGRESS_MESSAGE);        }        let request_id = self.next_request_id();        let response: Result<ExternalAgentConfigImportResponse> = self            .client            .request_typed(ClientRequest::ExternalAgentConfigImport {                request_id,                params: ExternalAgentConfigImportParams { migration_items },            })            .await            .wrap_err("externalAgentConfig/import failed during Claude Code import");        match response {            Ok(_) => Ok(()),            Err(err) => {                self.external_agent_config_import_completion_pending                    .store(false, Ordering::Relaxed);                Err(err)            }        }    }    pub(crate) fn external_agent_config_import_in_progress(&self) -> bool {        self.external_agent_config_import_completion_pending            .load(Ordering::Relaxed)    }    pub(crate) fn consume_external_agent_config_import_completion(&self) -> bool {        self.external_agent_config_import_completion_pending            .swap(false, Ordering::Relaxed)    }    pub(crate) async fn next_event(&mut self) -> Option<AppServerEvent> {        self.client.next_event().await    }    #[cfg(test)]    pub(crate) async fn start_thread(&mut self, config: &Config) -> Result<AppServerStartedThread> {        self.start_thread_with_session_start_source(config, /*session_start_source*/ None)            .await    }    pub(crate) async fn start_thread_with_session_start_source(        &mut self,        config: &Config,        session_start_source: Option<ThreadStartSource>,    ) -> Result<AppServerStartedThread> {        let request_id = self.next_request_id();        let session_config = self.session_config_with_effective_service_tier(config);        let response: ThreadStartResponse = self            .client            .request_typed(ClientRequest::ThreadStart {                request_id,                params: thread_start_params_from_config(                    &session_config,                    self.thread_params_mode(),                    self.remote_cwd_override.as_deref(),                    session_start_source,                ),            })            .await            .map_err(|err| {                bootstrap_request_error("thread/start failed during TUI bootstrap", err)            })?;        started_thread_from_start_response(response, config, self.thread_params_mode()).await    }    pub(crate) async fn resume_thread(        &mut self,        config: Config,        thread_id: ThreadId,    ) -> Result<AppServerStartedThread> {        let request_id = self.next_request_id();        let session_config = self.session_config_with_effective_service_tier(&config);        let response: ThreadResumeResponse = self            .client            .request_typed(ClientRequest::ThreadResume {                request_id,                params: thread_resume_params_from_config(                    session_config,                    thread_id,                    self.thread_params_mode(),                    self.remote_cwd_override.as_deref(),                ),            })            .await            .map_err(|err| {                bootstrap_request_error("thread/resume failed during TUI bootstrap", err)            })?;        let fork_parent_title = self            .fork_parent_title_from_app_server(response.thread.forked_from_id.as_deref())            .await;        let mut started =            started_thread_from_resume_response(response, &config, self.thread_params_mode())                .await?;        started.session.fork_parent_title = fork_parent_title;        Ok(started)    }    pub(crate) async fn fork_thread(        &mut self,        config: Config,        thread_id: ThreadId,    ) -> Result<AppServerStartedThread> {        let request_id = self.next_request_id();        let session_config = self.session_config_with_effective_service_tier(&config);        let response: ThreadForkResponse = self            .client            .request_typed(ClientRequest::ThreadFork {                request_id,                params: thread_fork_params_from_config(                    session_config,                    thread_id,                    self.thread_params_mode(),                    self.remote_cwd_override.as_deref(),                ),            })            .await            .map_err(|err| {                bootstrap_request_error("thread/fork failed during TUI bootstrap", err)            })?;        let fork_parent_title = self            .fork_parent_title_from_app_server(response.thread.forked_from_id.as_deref())            .await;        let mut started =            started_thread_from_fork_response(response, &config, self.thread_params_mode()).await?;        started.session.fork_parent_title = fork_parent_title;        Ok(started)    }    pub(crate) fn thread_params_mode(&self) -> ThreadParamsMode {        self.thread_params_mode    }    fn session_config_with_effective_service_tier(&self, config: &Config) -> Config {        let Some(model) = config.model.as_deref().or(self.default_model.as_deref()) else {            return config.clone();        };        let mut session_config = config.clone();        match service_tier_resolution::service_tier_update_for_core(            config,            model,            &self.available_models,        ) {            Some(Some(service_tier)) => {                session_config.service_tier = Some(service_tier);                session_config.notices.fast_default_opt_out = None;            }            Some(None) => {                session_config.service_tier = Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string());                session_config.notices.fast_default_opt_out = None;            }            None => {                session_config.service_tier = None;                session_config.notices.fast_default_opt_out = None;            }        }        session_config    }    async fn fork_parent_title_from_app_server(        &mut self,        forked_from_id: Option<&str>,    ) -> Option<String> {        let forked_from_id = forked_from_id?;        let forked_from_id = match ThreadId::from_string(forked_from_id) {            Ok(thread_id) => thread_id,            Err(err) => {                tracing::warn!("Failed to parse fork parent thread id from app server: {err}");                return None;            }        };        match self            .thread_read(forked_from_id, /*include_turns*/ false)            .await        {            Ok(thread) => thread.name,            Err(err) => {                tracing::warn!("Failed to read fork parent metadata from app server: {err}");                None            }        }    }    pub(crate) async fn thread_list(        &mut self,        params: ThreadListParams,    ) -> Result<ThreadListResponse> {        let request_id = self.next_request_id();        self.client            .request_typed(ClientRequest::ThreadList { request_id, params })            .await            .wrap_err("thread/list failed during TUI session lookup")    }    /// Lists thread ids that the app server currently holds in memory.    ///    /// Used by `App::backfill_loaded_subagent_threads` to discover subagent threads that were    /// spawned before the TUI connected. The caller then fetches full metadata per thread via    /// `thread_read` and walks the spawn tree.    pub(crate) async fn thread_loaded_list(        &mut self,        params: ThreadLoadedListParams,    ) -> Result<ThreadLoadedListResponse> {        let request_id = self.next_request_id();        self.client            .request_typed(ClientRequest::ThreadLoadedList { request_id, params })            .await            .wrap_err("failed to list loaded threads from app server")    }    pub(crate) async fn thread_read(        &mut self,        thread_id: ThreadId,        include_turns: bool,    ) -> Result<Thread> {        let request_id = self.next_request_id();        let response: ThreadReadResponse = self            .client            .request_typed(ClientRequest::ThreadRead {                request_id,                params: ThreadReadParams {                    thread_id: thread_id.to_string(),                    include_turns,                },            })            .await            .wrap_err("thread/read failed during TUI session lookup")?;        Ok(response.thread)    }    pub(crate) async fn thread_archive(&mut self, thread_id: ThreadId) -> Result<()> {        let request_id = self.next_request_id();        let _: ThreadArchiveResponse = self            .client            .request_typed(ClientRequest::ThreadArchive {                request_id,                params: ThreadArchiveParams {                    thread_id: thread_id.to_string(),                },            })            .await            .wrap_err("failed to archive session")?;        Ok(())    }    pub(crate) async fn thread_delete(&mut self, thread_id: ThreadId) -> Result<()> {        let request_id = self.next_request_id();        let _: ThreadDeleteResponse = self            .client            .request_typed(ClientRequest::ThreadDelete {                request_id,                params: ThreadDeleteParams {                    thread_id: thread_id.to_string(),                },            })            .await            .wrap_err("failed to delete session")?;        Ok(())    }    pub(crate) async fn thread_unarchive(&mut self, thread_id: ThreadId) -> Result<Thread> {        let request_id = self.next_request_id();        let response: ThreadUnarchiveResponse = self            .client            .request_typed(ClientRequest::ThreadUnarchive {                request_id,                params: ThreadUnarchiveParams {                    thread_id: thread_id.to_string(),                },            })            .await            .wrap_err("failed to unarchive session")?;        Ok(response.thread)    }    pub(crate) async fn thread_metadata_update_branch(        &mut self,        thread_id: ThreadId,        branch: String,    ) -> Result<ThreadMetadataUpdateResponse> {        let request_id = self.next_request_id();        self.client            .request_typed(ClientRequest::ThreadMetadataUpdate {                request_id,                params: ThreadMetadataUpdateParams {                    thread_id: thread_id.to_string(),                    git_info: Some(ThreadMetadataGitInfoUpdateParams {                        sha: None,                        branch: Some(Some(branch)),                        origin_url: None,                    }),                },            })            .await            .wrap_err("thread/metadata/update failed while syncing git branch")    }    pub(crate) async fn thread_settings_update(        &mut self,        params: ThreadSettingsUpdateParams,    ) -> Result<()> {        if !self.thread_settings_update_supported {            return Ok(());        }        let request_id = self.next_request_id();        match self            .client            .request_typed::<ThreadSettingsUpdateResponse>(ClientRequest::ThreadSettingsUpdate {                request_id,                params,            })            .await        {            Ok(_) => Ok(()),            Err(TypedRequestError::Server { source, .. })                if is_thread_settings_update_unsupported(&source) =>            {                // Older remote app servers can reject this experimental method as                // method-not-found, experimental-capability-gated, or an unknown                // request variant. Treat those as a session-level capability                // downgrade so local TUI setting changes stay best-effort instead                // of showing an error every time the user changes model, effort,                // personality, or mode.                self.thread_settings_update_supported = false;                Ok(())            }            Err(err) => Err(err).wrap_err("thread/settings/update failed in TUI"),        }    }    pub(crate) async fn thread_inject_items(        &mut self,        thread_id: ThreadId,        items: Vec<ResponseItem>,    ) -> Result<ThreadInjectItemsResponse> {        let items = items            .into_iter()            .map(serde_json::to_value)            .collect::<std::result::Result<Vec<_>, _>>()            .wrap_err("failed to encode thread/inject_items payload")?;        let request_id = self.next_request_id();        self.client            .request_typed(ClientRequest::ThreadInjectItems {                request_id,                params: ThreadInjectItemsParams {                    thread_id: thread_id.to_string(),                    items,                },            })            .await            .wrap_err("thread/inject_items failed during TUI side conversation setup")    }    #[allow(clippy::too_many_arguments)]    pub(crate) async fn turn_start(        &mut self,        thread_id: ThreadId,        items: Vec<UserInput>,        cwd: PathBuf,        approval_policy: AskForApproval,        approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,        permissions_override: TurnPermissionsOverride,        workspace_roots: &[AbsolutePathBuf],        model: String,        effort: Option<codex_protocol::openai_models::ReasoningEffort>,        summary: Option<codex_protocol::config_types::ReasoningSummary>,        service_tier: Option<Option<String>>,        collaboration_mode: Option<codex_protocol::config_types::CollaborationMode>,        personality: Option<codex_protocol::config_types::Personality>,        output_schema: Option<serde_json::Value>,    ) -> Result<TurnStartResponse> {        let request_id = self.next_request_id();        let (sandbox_policy, permissions) =            turn_permissions_overrides(permissions_override, cwd.as_path());        self.client            .request_typed(ClientRequest::TurnStart {                request_id,                params: TurnStartParams {                    thread_id: thread_id.to_string(),                    client_user_message_id: None,                    input: items,                    responsesapi_client_metadata: None,                    additional_context: None,                    environments: None,                    cwd: Some(cwd),                    runtime_workspace_roots: Some(workspace_roots.to_vec()),                    approval_policy: Some(approval_policy),                    approvals_reviewer: Some(approvals_reviewer.into()),                    sandbox_policy,                    permissions,                    model: Some(model),                    service_tier,                    effort,                    summary,                    personality,                    output_schema,                    collaboration_mode,                },            })            .await            .wrap_err("turn/start failed in TUI")    }    pub(crate) async fn turn_interrupt(        &mut self,        thread_id: ThreadId,        turn_id: String,    ) -> std::result::Result<(), TypedRequestError> {        let request_id = self.next_request_id();        let _: TurnInterruptResponse = self            .client            .request_typed(ClientRequest::TurnInterrupt {                request_id,                params: TurnInterruptParams {                    thread_id: thread_id.to_string(),                    turn_id,                },            })            .await?;        Ok(())    }    pub(crate) async fn startup_interrupt(        &mut self,        thread_id: ThreadId,    ) -> std::result::Result<(), TypedRequestError> {        self.turn_interrupt(thread_id, String::new()).await    }    pub(crate) async fn turn_steer(        &mut self,        thread_id: ThreadId,        turn_id: String,        items: Vec<UserInput>,    ) -> std::result::Result<TurnSteerResponse, TypedRequestError> {        let request_id = self.next_request_id();        self.client            .request_typed(ClientRequest::TurnSteer {                request_id,                params: TurnSteerParams {                    thread_id: thread_id.to_string(),                    client_user_message_id: None,                    input: items,                    responsesapi_client_metadata: None,                    additional_context: None,                    expected_turn_id: turn_id,                },            })            .await    }    pub(crate) async fn thread_set_name(        &mut self,        thread_id: ThreadId,        name: String,    ) -> Result<()> {        let request_id = self.next_request_id();        let _: ThreadSetNameResponse = self            .client            .request_typed(ClientRequest::ThreadSetName {                request_id,                params: ThreadSetNameParams {                    thread_id: thread_id.to_string(),                    name,                },            })            .await            .wrap_err("thread/name/set failed in TUI")?;        Ok(())    }    pub(crate) async fn thread_memory_mode_set(        &mut self,        thread_id: ThreadId,        mode: ThreadMemoryMode,    ) -> Result<()> {        let request_id = self.next_request_id();        let _: ThreadMemoryModeSetResponse = self            .client            .request_typed(ClientRequest::ThreadMemoryModeSet {                request_id,                params: ThreadMemoryModeSetParams {                    thread_id: thread_id.to_string(),                    mode,                },            })            .await            .wrap_err("thread/memoryMode/set failed in TUI")?;        Ok(())    }    pub(crate) async fn memory_reset(&mut self) -> Result<()> {        let request_id = self.next_request_id();        let _: MemoryResetResponse = self            .client            .request_typed(ClientRequest::MemoryReset {                request_id,                params: None,            })            .await            .wrap_err("memory/reset failed in TUI")?;        Ok(())    }    pub(crate) async fn thread_goal_get(        &mut self,        thread_id: ThreadId,    ) -> Result<ThreadGoalGetResponse> {        let request_id = self.next_request_id();        self.client            .request_typed(ClientRequest::ThreadGoalGet {                request_id,                params: ThreadGoalGetParams {                    thread_id: thread_id.to_string(),                },            })            .await            .wrap_err("thread/goal/get failed in TUI")    }    pub(crate) async fn thread_goal_set(        &mut self,        thread_id: ThreadId,        objective: Option<String>,        status: Option<ThreadGoalStatus>,        token_budget: Option<Option<i64>>,    ) -> Result<ThreadGoalSetResponse> {        let request_id = self.next_request_id();        self.client            .request_typed(ClientRequest::ThreadGoalSet {                request_id,                params: ThreadGoalSetParams {                    thread_id: thread_id.to_string(),                    objective,                    status,                    token_budget,                },            })            .await            .wrap_err("thread/goal/set failed in TUI")    }    pub(crate) async fn thread_goal_clear(        &mut self,        thread_id: ThreadId,    ) -> Result<ThreadGoalClearResponse> {        let request_id = self.next_request_id();        self.client            .request_typed(ClientRequest::ThreadGoalClear {                request_id,                params: ThreadGoalClearParams {                    thread_id: thread_id.to_string(),                },            })            .await            .wrap_err("thread/goal/clear failed in TUI")    }    pub(crate) async fn logout_account(&mut self) -> Result<()> {        let request_id = self.next_request_id();        let _: LogoutAccountResponse = self            .client            .request_typed(ClientRequest::LogoutAccount {                request_id,                params: None,            })            .await            .wrap_err("account/logout failed in TUI")?;        Ok(())    }    pub(crate) async fn thread_unsubscribe(&mut self, thread_id: ThreadId) -> Result<()> {        let request_id = self.next_request_id();        let _: ThreadUnsubscribeResponse = self            .client            .request_typed(ClientRequest::ThreadUnsubscribe {                request_id,                params: ThreadUnsubscribeParams {                    thread_id: thread_id.to_string(),                },            })            .await            .wrap_err("thread/unsubscribe failed in TUI")?;        Ok(())    }    pub(crate) async fn thread_compact_start(&mut self, thread_id: ThreadId) -> Result<()> {        let request_id = self.next_request_id();        let _: ThreadCompactStartResponse = self            .client            .request_typed(ClientRequest::ThreadCompactStart {                request_id,                params: ThreadCompactStartParams {                    thread_id: thread_id.to_string(),                },            })            .await            .wrap_err("thread/compact/start failed in TUI")?;        Ok(())    }    pub(crate) async fn thread_shell_command(        &mut self,        thread_id: ThreadId,        command: String,    ) -> Result<()> {        let request_id = self.next_request_id();        let _: ThreadShellCommandResponse = self            .client            .request_typed(ClientRequest::ThreadShellCommand {                request_id,                params: ThreadShellCommandParams {                    thread_id: thread_id.to_string(),                    command,                },            })            .await            .wrap_err("thread/shellCommand failed in TUI")?;        Ok(())    }    pub(crate) async fn thread_approve_guardian_denied_action(        &mut self,        thread_id: ThreadId,        event: &GuardianAssessmentEvent,    ) -> Result<()> {        let request_id = self.next_request_id();        let _: ThreadApproveGuardianDeniedActionResponse = self            .client            .request_typed(ClientRequest::ThreadApproveGuardianDeniedAction {                request_id,                params: ThreadApproveGuardianDeniedActionParams {                    thread_id: thread_id.to_string(),                    event: serde_json::to_value(event)                        .wrap_err("failed to serialize Auto Review denial event")?,                },            })            .await            .wrap_err("thread/approveGuardianDeniedAction failed in TUI")?;        Ok(())    }    pub(crate) async fn thread_background_terminals_clean(        &mut self,        thread_id: ThreadId,    ) -> Result<()> {        let request_id = self.next_request_id();        let _: ThreadBackgroundTerminalsCleanResponse = self            .client            .request_typed(ClientRequest::ThreadBackgroundTerminalsClean {                request_id,                params: ThreadBackgroundTerminalsCleanParams {                    thread_id: thread_id.to_string(),                },            })            .await            .wrap_err("thread/backgroundTerminals/clean failed in TUI")?;        Ok(())    }    pub(crate) async fn thread_rollback(        &mut self,        thread_id: ThreadId,        num_turns: u32,    ) -> Result<ThreadRollbackResponse> {        let request_id = self.next_request_id();        self.client            .request_typed(ClientRequest::ThreadRollback {                request_id,                params: ThreadRollbackParams {                    thread_id: thread_id.to_string(),                    num_turns,                },            })            .await            .wrap_err("thread/rollback failed in TUI")    }    pub(crate) async fn review_start(        &mut self,        thread_id: ThreadId,        target: ReviewTarget,    ) -> Result<ReviewStartResponse> {        let request_id = self.next_request_id();        self.client            .request_typed(ClientRequest::ReviewStart {                request_id,                params: ReviewStartParams {                    thread_id: thread_id.to_string(),                    target,                    delivery: Some(ReviewDelivery::Inline),                },            })            .await            .wrap_err("review/start failed in TUI")    }    pub(crate) async fn skills_list(        &mut self,        params: SkillsListParams,    ) -> Result<SkillsListResponse> {        let request_id = self.next_request_id();        self.client            .request_typed(ClientRequest::SkillsList { request_id, params })            .await            .wrap_err("skills/list failed in TUI")    }    pub(crate) async fn reload_user_config(&mut self) -> Result<()> {        let request_id = self.next_request_id();        let _: ConfigWriteResponse = self            .client            .request_typed(ClientRequest::ConfigBatchWrite {                request_id,                params: ConfigBatchWriteParams {                    edits: Vec::new(),                    file_path: None,                    expected_version: None,                    reload_user_config: true,                },            })            .await            .wrap_err("config/batchWrite failed while reloading user config in TUI")?;        Ok(())    }    pub(crate) async fn reject_server_request(        &self,        request_id: RequestId,        error: JSONRPCErrorError,    ) -> std::io::Result<()> {        self.client.reject_server_request(request_id, error).await    }    pub(crate) async fn resolve_server_request(        &self,        request_id: RequestId,        result: serde_json::Value,    ) -> std::io::Result<()> {        self.client.resolve_server_request(request_id, result).await    }    pub(crate) async fn shutdown(self) -> std::io::Result<()> {        self.client.shutdown().await    }    pub(crate) fn request_handle(&self) -> AppServerRequestHandle {        self.client.request_handle()    }    fn next_request_id(&mut self) -> RequestId {        let request_id = self.next_request_id;        self.next_request_id += 1;        RequestId::Integer(request_id)    }}pub(crate) async fn start_thread_with_request_handle(    request_handle: AppServerRequestHandle,    config: Config,    thread_params_mode: ThreadParamsMode,    remote_cwd_override: Option<PathBuf>,) -> Result<AppServerStartedThread> {    let response: ThreadStartResponse = request_handle        .request_typed(ClientRequest::ThreadStart {            request_id: RequestId::String(format!("startup-thread-start-{}", Uuid::new_v4())),            params: thread_start_params_from_config(                &config,                thread_params_mode,                remote_cwd_override.as_deref(),                /*session_start_source*/ None,            ),        })        .await        .map_err(|err| bootstrap_request_error("thread/start failed during TUI bootstrap", err))?;    started_thread_from_start_response(response, &config, thread_params_mode).await}pub(crate) fn status_account_display_from_auth_mode(    auth_mode: Option<AuthMode>,    plan_type: Option<codex_protocol::account::PlanType>,) -> Option<StatusAccountDisplay> {    match auth_mode {        Some(AuthMode::ApiKey) => Some(StatusAccountDisplay::ApiKey),        Some(AuthMode::Chatgpt)        | Some(AuthMode::ChatgptAuthTokens)        | Some(AuthMode::AgentIdentity)        | Some(AuthMode::PersonalAccessToken) => Some(StatusAccountDisplay::ChatGpt {            email: None,            plan: plan_type.map(plan_type_display_name),        }),        Some(AuthMode::BedrockApiKey) => None,        None => None,    }}fn model_preset_from_api_model(model: ApiModel) -> ModelPreset {    let upgrade = model.upgrade.map(|upgrade_id| {        let upgrade_info = model.upgrade_info.clone();        ModelUpgrade {            id: upgrade_id,            migration_config_key: model.model.clone(),            model_link: upgrade_info                .as_ref()                .and_then(|info| info.model_link.clone()),            upgrade_copy: upgrade_info                .as_ref()                .and_then(|info| info.upgrade_copy.clone()),            migration_markdown: upgrade_info.and_then(|info| info.migration_markdown),        }    });    ModelPreset {        id: model.id,        model: model.model,        display_name: model.display_name,        description: model.description,        default_reasoning_effort: model.default_reasoning_effort,        supported_reasoning_efforts: model            .supported_reasoning_efforts            .into_iter()            .map(|effort| ReasoningEffortPreset {                effort: effort.reasoning_effort,                description: effort.description,            })            .collect(),        supports_personality: model.supports_personality,        additional_speed_tiers: model.additional_speed_tiers,        service_tiers: model            .service_tiers            .into_iter()            .map(|service_tier| ModelServiceTier {                id: service_tier.id,                name: service_tier.name,                description: service_tier.description,            })            .collect(),        default_service_tier: model.default_service_tier,        is_default: model.is_default,        upgrade,        show_in_picker: !model.hidden,        availability_nux: model.availability_nux.map(|nux| ModelAvailabilityNux {            message: nux.message,        }),        // `model/list` already returns models filtered for the active client/auth context.        supported_in_api: true,        input_modalities: model.input_modalities,    }}fn approvals_reviewer_override_from_config(    config: &Config,) -> Option<codex_app_server_protocol::ApprovalsReviewer> {    Some(config.approvals_reviewer.into())}fn config_request_overrides_from_config(    config: &Config,) -> Option<HashMap<String, serde_json::Value>> {    let mut overrides = HashMap::new();    let mut insert = |key: &str, value: Option<String>| {        if let Some(value) = value {            overrides.insert(key.to_string(), serde_json::Value::String(value));        }    };    insert(        "model_reasoning_effort",        config            .model_reasoning_effort            .as_ref()            .map(std::string::ToString::to_string),    );    insert(        "model_reasoning_summary",        config            .model_reasoning_summary            .map(|summary| summary.to_string()),    );    insert(        "model_verbosity",        config            .model_verbosity            .map(|verbosity| verbosity.to_string()),    );    insert(        "personality",        config            .personality            .map(|personality| personality.to_string()),    );    insert(        "web_search",        Some(config.web_search_mode.value().to_string()),    );    if config.bypass_hook_trust {        overrides.insert("bypass_hook_trust".to_string(), true.into());    }    Some(overrides)}fn service_tier_override_from_config(config: &Config) -> Option<Option<String>> {    config.service_tier.clone().map(Some).or_else(|| {        (config.notices.fast_default_opt_out == Some(true))            .then(|| Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string()))    })}fn sandbox_mode_from_permission_profile(    permission_profile: &PermissionProfile,    cwd: &std::path::Path,) -> Option<codex_app_server_protocol::SandboxMode> {    match permission_profile {        PermissionProfile::Disabled => {            Some(codex_app_server_protocol::SandboxMode::DangerFullAccess)        }        PermissionProfile::External { .. } => None,        PermissionProfile::Managed { .. } => {            let file_system_policy = permission_profile.file_system_sandbox_policy();            if file_system_policy.has_full_disk_write_access() {                permission_profile                    .network_sandbox_policy()                    .is_enabled()                    .then_some(codex_app_server_protocol::SandboxMode::DangerFullAccess)            } else if file_system_policy.can_write_path_with_cwd(cwd, cwd) {                Some(codex_app_server_protocol::SandboxMode::WorkspaceWrite)            } else {                Some(codex_app_server_protocol::SandboxMode::ReadOnly)            }        }    }}fn permission_profile_id_from_active_profile(active: ActivePermissionProfile) -> String {    active.id}fn turn_permissions_overrides(    permissions_override: TurnPermissionsOverride,    cwd: &std::path::Path,) -> (    Option<codex_app_server_protocol::SandboxPolicy>,    Option<String>,) {    match permissions_override {        TurnPermissionsOverride::Preserve => (None, None),        TurnPermissionsOverride::ActiveProfile(active_permission_profile) => (            None,            Some(permission_profile_id_from_active_profile(                active_permission_profile,            )),        ),        TurnPermissionsOverride::LegacySandbox(permission_profile) => {            let legacy_profile = legacy_compatible_permission_profile(&permission_profile, cwd);            let policy = legacy_profile                .to_legacy_sandbox_policy(cwd)                .unwrap_or_else(|err| {                    unreachable!(                        "legacy-compatible permissions must project to legacy policy: {err}"                    )                });            (Some(policy.into()), None)        }    }}fn permissions_selection_from_config(    config: &Config,    thread_params_mode: ThreadParamsMode,) -> Option<String> {    if matches!(thread_params_mode, ThreadParamsMode::Remote) {        return None;    }    config        .permissions        .active_permission_profile()        .map(permission_profile_id_from_active_profile)}fn thread_start_params_from_config(    config: &Config,    thread_params_mode: ThreadParamsMode,    remote_cwd_override: Option<&std::path::Path>,    session_start_source: Option<ThreadStartSource>,) -> ThreadStartParams {    let permissions = permissions_selection_from_config(config, thread_params_mode);    let sandbox = permissions        .is_none()        .then(|| {            sandbox_mode_from_permission_profile(                &config.permissions.effective_permission_profile(),                config.cwd.as_path(),            )        })        .flatten();    ThreadStartParams {        model: config.model.clone(),        model_provider: thread_params_mode.model_provider_from_config(config),        service_tier: service_tier_override_from_config(config),        cwd: thread_cwd_from_config(config, thread_params_mode, remote_cwd_override),        runtime_workspace_roots: Some(config.workspace_roots.clone()),        approval_policy: Some(config.permissions.approval_policy.value().into()),        approvals_reviewer: approvals_reviewer_override_from_config(config),        sandbox,        permissions,        config: config_request_overrides_from_config(config),        ephemeral: Some(config.ephemeral),        session_start_source,        thread_source: Some(ThreadSource::User),        developer_instructions: with_terminal_visualization_instructions(            config, /*control_instructions*/ None,        ),        ..ThreadStartParams::default()    }}fn thread_resume_params_from_config(    config: Config,    thread_id: ThreadId,    thread_params_mode: ThreadParamsMode,    remote_cwd_override: Option<&std::path::Path>,) -> ThreadResumeParams {    let permissions = permissions_selection_from_config(&config, thread_params_mode);    let sandbox = permissions        .is_none()        .then(|| {            sandbox_mode_from_permission_profile(                &config.permissions.effective_permission_profile(),                config.cwd.as_path(),            )        })        .flatten();    ThreadResumeParams {        thread_id: thread_id.to_string(),        model: config.model.clone(),        model_provider: thread_params_mode.model_provider_from_config(&config),        service_tier: service_tier_override_from_config(&config),        cwd: thread_cwd_from_config(&config, thread_params_mode, remote_cwd_override),        runtime_workspace_roots: Some(config.workspace_roots.clone()),        approval_policy: Some(config.permissions.approval_policy.value().into()),        approvals_reviewer: approvals_reviewer_override_from_config(&config),        sandbox,        permissions,        config: config_request_overrides_from_config(&config),        developer_instructions: with_terminal_visualization_instructions(            &config, /*control_instructions*/ None,        ),        ..ThreadResumeParams::default()    }}fn thread_fork_params_from_config(    config: Config,    thread_id: ThreadId,    thread_params_mode: ThreadParamsMode,    remote_cwd_override: Option<&std::path::Path>,) -> ThreadForkParams {    let permissions = permissions_selection_from_config(&config, thread_params_mode);    let sandbox = permissions        .is_none()        .then(|| {            sandbox_mode_from_permission_profile(                &config.permissions.effective_permission_profile(),                config.cwd.as_path(),            )        })        .flatten();    ThreadForkParams {        thread_id: thread_id.to_string(),        model: config.model.clone(),        model_provider: thread_params_mode.model_provider_from_config(&config),        service_tier: service_tier_override_from_config(&config),        cwd: thread_cwd_from_config(&config, thread_params_mode, remote_cwd_override),        runtime_workspace_roots: Some(config.workspace_roots.clone()),        approval_policy: Some(config.permissions.approval_policy.value().into()),        approvals_reviewer: approvals_reviewer_override_from_config(&config),        sandbox,        permissions,        config: config_request_overrides_from_config(&config),        base_instructions: config.base_instructions.clone(),        developer_instructions: with_terminal_visualization_instructions(            &config,            config.developer_instructions.clone(),        ),        ephemeral: config.ephemeral,        thread_source: Some(ThreadSource::User),        ..ThreadForkParams::default()    }}fn thread_cwd_from_config(    config: &Config,    thread_params_mode: ThreadParamsMode,    remote_cwd_override: Option<&std::path::Path>,) -> Option<String> {    match thread_params_mode {        ThreadParamsMode::Embedded => Some(config.cwd.to_string_lossy().to_string()),        ThreadParamsMode::Remote => {            remote_cwd_override.map(|cwd| cwd.to_string_lossy().to_string())        }    }}async fn started_thread_from_start_response(    response: ThreadStartResponse,    config: &Config,    thread_params_mode: ThreadParamsMode,) -> Result<AppServerStartedThread> {    let session =        thread_session_state_from_thread_start_response(&response, config, thread_params_mode)            .await            .map_err(color_eyre::eyre::Report::msg)?;    Ok(AppServerStartedThread {        session,        turns: response.thread.turns,    })}async fn started_thread_from_resume_response(    response: ThreadResumeResponse,    config: &Config,    thread_params_mode: ThreadParamsMode,) -> Result<AppServerStartedThread> {    let session =        thread_session_state_from_thread_resume_response(&response, config, thread_params_mode)            .await            .map_err(color_eyre::eyre::Report::msg)?;    Ok(AppServerStartedThread {        session,        turns: response.thread.turns,    })}async fn started_thread_from_fork_response(    response: ThreadForkResponse,    config: &Config,    thread_params_mode: ThreadParamsMode,) -> Result<AppServerStartedThread> {    let session =        thread_session_state_from_thread_fork_response(&response, config, thread_params_mode)            .await            .map_err(color_eyre::eyre::Report::msg)?;    Ok(AppServerStartedThread {        session,        turns: response.thread.turns,    })}async fn thread_session_state_from_thread_start_response(    response: &ThreadStartResponse,    config: &Config,    thread_params_mode: ThreadParamsMode,) -> Result<ThreadSessionState, String> {    let permission_profile = display_permission_profile_from_thread_response(        &response.sandbox,        response.cwd.as_path(),        config,        thread_params_mode,    );    thread_session_state_from_thread_response(        &response.thread.id,        response.thread.forked_from_id.clone(),        response.thread.name.clone(),        response.thread.path.clone(),        response.model.clone(),        response.model_provider.clone(),        response.service_tier.clone(),        response.approval_policy,        response.approvals_reviewer.to_core(),        permission_profile,        response.active_permission_profile.clone().map(Into::into),        response.cwd.clone(),        response.runtime_workspace_roots.clone(),        response.instruction_sources.clone(),        response.reasoning_effort.clone(),        config,    )    .await}async fn thread_session_state_from_thread_resume_response(    response: &ThreadResumeResponse,    config: &Config,    thread_params_mode: ThreadParamsMode,) -> Result<ThreadSessionState, String> {    let permission_profile = if matches!(thread_params_mode, ThreadParamsMode::Embedded)        && response.active_permission_profile.is_none()    {        PermissionProfile::from_legacy_sandbox_policy_for_cwd(            &response.sandbox.to_core(),            response.cwd.as_path(),        )    } else {        display_permission_profile_from_thread_response(            &response.sandbox,            response.cwd.as_path(),            config,            thread_params_mode,        )    };    thread_session_state_from_thread_response(        &response.thread.id,        response.thread.forked_from_id.clone(),        response.thread.name.clone(),        response.thread.path.clone(),        response.model.clone(),        response.model_provider.clone(),        response.service_tier.clone(),        response.approval_policy,        response.approvals_reviewer.to_core(),        permission_profile,        response.active_permission_profile.clone().map(Into::into),        response.cwd.clone(),        response.runtime_workspace_roots.clone(),        response.instruction_sources.clone(),        response.reasoning_effort.clone(),        config,    )    .await}async fn thread_session_state_from_thread_fork_response(    response: &ThreadForkResponse,    config: &Config,    thread_params_mode: ThreadParamsMode,) -> Result<ThreadSessionState, String> {    let permission_profile = display_permission_profile_from_thread_response(        &response.sandbox,        response.cwd.as_path(),        config,        thread_params_mode,    );    thread_session_state_from_thread_response(        &response.thread.id,        response.thread.forked_from_id.clone(),        response.thread.name.clone(),        response.thread.path.clone(),        response.model.clone(),        response.model_provider.clone(),        response.service_tier.clone(),        response.approval_policy,        response.approvals_reviewer.to_core(),        permission_profile,        response.active_permission_profile.clone().map(Into::into),        response.cwd.clone(),        response.runtime_workspace_roots.clone(),        response.instruction_sources.clone(),        response.reasoning_effort.clone(),        config,    )    .await}fn display_permission_profile_from_thread_response(    sandbox: &codex_app_server_protocol::SandboxPolicy,    cwd: &std::path::Path,    config: &Config,    thread_params_mode: ThreadParamsMode,) -> PermissionProfile {    match thread_params_mode {        ThreadParamsMode::Embedded => config.permissions.effective_permission_profile(),        ThreadParamsMode::Remote => {            PermissionProfile::from_legacy_sandbox_policy_for_cwd(&sandbox.to_core(), cwd)        }    }}#[expect(    clippy::too_many_arguments,    reason = "session mapping keeps explicit fields")]async fn thread_session_state_from_thread_response(    thread_id: &str,    forked_from_id: Option<String>,    thread_name: Option<String>,    rollout_path: Option<PathBuf>,    model: String,    model_provider_id: String,    service_tier: Option<String>,    approval_policy: AskForApproval,    approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,    permission_profile: PermissionProfile,    active_permission_profile: Option<ActivePermissionProfile>,    cwd: AbsolutePathBuf,    runtime_workspace_roots: Vec<AbsolutePathBuf>,    instruction_source_paths: Vec<AbsolutePathBuf>,    reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,    config: &Config,) -> Result<ThreadSessionState, String> {    let thread_id = ThreadId::from_string(thread_id)        .map_err(|err| format!("thread id `{thread_id}` is invalid: {err}"))?;    let forked_from_id = forked_from_id        .as_deref()        .map(ThreadId::from_string)        .transpose()        .map_err(|err| format!("forked_from_id is invalid: {err}"))?;    let history_config =        codex_message_history::HistoryConfig::new(config.codex_home.clone(), &config.history);    let (log_id, entry_count) = codex_message_history::history_metadata(&history_config).await;    Ok(ThreadSessionState {        thread_id,        forked_from_id,        fork_parent_title: None,        thread_name,        model,        model_provider_id,        service_tier,        approval_policy,        approvals_reviewer,        permission_profile,        active_permission_profile,        cwd,        runtime_workspace_roots,        instruction_source_paths,        reasoning_effort,        collaboration_mode: None,        personality: config.personality,        message_history: Some(MessageHistoryMetadata {            log_id,            entry_count,        }),        network_proxy: None,        rollout_path,    })}pub(crate) fn app_server_rate_limit_snapshots(    response: GetAccountRateLimitsResponse,) -> Vec<RateLimitSnapshot> {    let primary_limit_id = response.rate_limits.limit_id.clone();    let mut snapshots = vec![response.rate_limits];    if let Some(by_limit_id) = response.rate_limits_by_limit_id {        snapshots.extend(by_limit_id.into_iter().filter_map(|(limit_id, snapshot)| {            if primary_limit_id.as_deref().is_some_and(|primary_limit_id| {                primary_limit_id == limit_id                    || Some(primary_limit_id) == snapshot.limit_id.as_deref()            }) {                None            } else {                Some(snapshot)            }        }));    }    snapshots}#[cfg(test)]mod tests {    use super::*;    use crate::legacy_core::config::ConfigBuilder;    use crate::legacy_core::config::ConfigOverrides;    use codex_app_server_protocol::ThreadStatus;    use codex_app_server_protocol::Turn;    use codex_app_server_protocol::TurnStatus;    use codex_features::Feature;    use codex_protocol::config_types::Personality;    use codex_protocol::config_types::ReasoningSummary;    use codex_protocol::config_types::ServiceTier;    use codex_protocol::config_types::Verbosity;    use codex_protocol::config_types::WebSearchMode;    use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;    use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;    use codex_protocol::models::ManagedFileSystemPermissions;    use codex_protocol::openai_models::ReasoningEffort;    use codex_protocol::permissions::FileSystemAccessMode;    use codex_protocol::permissions::FileSystemPath;    use codex_protocol::permissions::FileSystemSandboxEntry;    use codex_protocol::permissions::FileSystemSpecialPath;    use codex_protocol::permissions::NetworkSandboxPolicy;    use codex_utils_absolute_path::test_support::PathBufExt;    use codex_utils_absolute_path::test_support::test_path_buf;    use pretty_assertions::assert_eq;    use tempfile::TempDir;    async fn build_config(temp_dir: &TempDir) -> Config {        ConfigBuilder::default()            .codex_home(temp_dir.path().to_path_buf())            .build()            .await            .expect("config should build")    }    fn rate_limit_snapshot(limit_id: &str) -> RateLimitSnapshot {        RateLimitSnapshot {            limit_id: Some(limit_id.to_string()),            limit_name: None,            primary: Some(codex_app_server_protocol::RateLimitWindow {                used_percent: 0,                window_duration_mins: Some(10_080),                resets_at: None,            }),            secondary: None,            credits: None,            individual_limit: None,            plan_type: None,            rate_limit_reached_type: None,        }    }    #[test]    fn app_server_rate_limit_snapshots_deduplicates_top_level_limit_from_map() {        let response = GetAccountRateLimitsResponse {            rate_limits: rate_limit_snapshot("codex"),            rate_limits_by_limit_id: Some(HashMap::from([                ("codex".to_string(), rate_limit_snapshot("codex")),                ("other".to_string(), rate_limit_snapshot("other")),            ])),            rate_limit_reset_credits: None,        };        let snapshots = app_server_rate_limit_snapshots(response);        assert_eq!(            snapshots                .iter()                .map(|snapshot| snapshot.limit_id.as_deref())                .collect::<Vec<_>>(),            vec![Some("codex"), Some("other")]        );    }    #[test]    fn thread_settings_update_compat_detects_unsupported_errors() {        let cases = [            (JSONRPC_METHOD_NOT_FOUND, "method not found", true),            (                JSONRPC_INVALID_REQUEST,                "thread/settings/update requires experimentalApi capability",                true,            ),            (                JSONRPC_INVALID_REQUEST,                "Invalid request: unknown variant `thread/settings/update`",                true,            ),            (JSONRPC_INVALID_REQUEST, "invalid thread id", false),        ];        for (code, message, expected) in cases {            let source = JSONRPCErrorError {                code,                data: None,                message: message.to_string(),            };            assert_eq!(                is_thread_settings_update_unsupported(&source),                expected,                "{message}"            );        }    }    #[tokio::test]    async fn thread_start_params_include_cwd_for_embedded_sessions() {        let temp_dir = tempfile::tempdir().expect("tempdir");        let config = ConfigBuilder::default()            .codex_home(temp_dir.path().to_path_buf())            .harness_overrides(ConfigOverrides {                default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()),                ..ConfigOverrides::default()            })            .build()            .await            .expect("config should build");        let params = thread_start_params_from_config(            &config,            ThreadParamsMode::Embedded,            /*remote_cwd_override*/ None,            /*session_start_source*/ None,        );        assert_eq!(params.cwd, Some(config.cwd.to_string_lossy().to_string()));        assert_eq!(            params.runtime_workspace_roots,            Some(config.workspace_roots.clone())        );        assert_eq!(params.sandbox, None);        assert_eq!(            params.permissions,            config                .permissions                .active_permission_profile()                .map(permission_profile_id_from_active_profile)        );        assert_eq!(params.model_provider, Some(config.model_provider_id));        assert_eq!(params.thread_source, Some(ThreadSource::User));    }    #[tokio::test]    async fn thread_start_params_can_mark_clear_source() {        let temp_dir = tempfile::tempdir().expect("tempdir");        let config = build_config(&temp_dir).await;        let params = thread_start_params_from_config(            &config,            ThreadParamsMode::Embedded,            /*remote_cwd_override*/ None,            Some(ThreadStartSource::Clear),        );        assert_eq!(params.session_start_source, Some(ThreadStartSource::Clear));    }    #[test]    fn embedded_turn_permissions_use_active_profile_selection() {        let cwd = test_path_buf("/workspace/project").abs();        let active_permission_profile =            ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE);        let expected_permissions =            permission_profile_id_from_active_profile(active_permission_profile.clone());        let (sandbox_policy, permissions) = turn_permissions_overrides(            TurnPermissionsOverride::ActiveProfile(active_permission_profile),            cwd.as_path(),        );        assert_eq!(sandbox_policy, None);        assert_eq!(permissions, Some(expected_permissions));    }    #[test]    fn embedded_turn_permissions_select_profile_id_only() {        let cwd = test_path_buf("/workspace/project").abs();        let active_permission_profile =            ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE);        let (sandbox_policy, permissions) = turn_permissions_overrides(            TurnPermissionsOverride::ActiveProfile(active_permission_profile),            cwd.as_path(),        );        assert_eq!(sandbox_policy, None);        assert_eq!(            permissions,            Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string())        );    }    #[test]    fn turn_permissions_preserve_thread_permissions_without_override() {        let cwd = test_path_buf("/workspace/project").abs();        let (sandbox_policy, permissions) =            turn_permissions_overrides(TurnPermissionsOverride::Preserve, cwd.as_path());        assert_eq!(sandbox_policy, None);        assert_eq!(permissions, None);    }    #[test]    fn legacy_turn_permissions_project_to_sandbox_when_explicitly_overridden() {        let cwd = test_path_buf("/workspace/project").abs();        let (sandbox_policy, permissions) = turn_permissions_overrides(            TurnPermissionsOverride::LegacySandbox(PermissionProfile::read_only()),            cwd.as_path(),        );        assert_eq!(            sandbox_policy,            Some(codex_app_server_protocol::SandboxPolicy::ReadOnly {                network_access: false            })        );        assert_eq!(permissions, None);    }    #[test]    fn remote_turn_permissions_preserve_active_profile_selection() {        let cwd = test_path_buf("/workspace/project").abs();        let active_permission_profile = ActivePermissionProfile::new("strict");        let expected_permissions =            permission_profile_id_from_active_profile(active_permission_profile.clone());        let (sandbox_policy, permissions) = turn_permissions_overrides(            TurnPermissionsOverride::ActiveProfile(active_permission_profile),            cwd.as_path(),        );        assert_eq!(sandbox_policy, None);        assert_eq!(permissions, Some(expected_permissions));    }    #[tokio::test]    async fn thread_lifecycle_params_omit_cwd_without_remote_override_for_remote_sessions() {        let temp_dir = tempfile::tempdir().expect("tempdir");        let config = build_config(&temp_dir).await;        let thread_id = ThreadId::new();        let expected_sandbox = sandbox_mode_from_permission_profile(            &config.permissions.effective_permission_profile(),            config.cwd.as_path(),        );        let expected_runtime_workspace_roots = Some(config.workspace_roots.clone());        let start = thread_start_params_from_config(            &config,            ThreadParamsMode::Remote,            /*remote_cwd_override*/ None,            /*session_start_source*/ None,        );        let resume = thread_resume_params_from_config(            config.clone(),            thread_id,            ThreadParamsMode::Remote,            /*remote_cwd_override*/ None,        );        let fork = thread_fork_params_from_config(            config,            thread_id,            ThreadParamsMode::Remote,            /*remote_cwd_override*/ None,        );        assert_eq!(start.cwd, None);        assert_eq!(resume.cwd, None);        assert_eq!(fork.cwd, None);        assert_eq!(            start.runtime_workspace_roots,            expected_runtime_workspace_roots        );        assert_eq!(            resume.runtime_workspace_roots,            expected_runtime_workspace_roots        );        assert_eq!(            fork.runtime_workspace_roots,            expected_runtime_workspace_roots        );        assert_eq!(start.model_provider, None);        assert_eq!(resume.model_provider, None);        assert_eq!(fork.model_provider, None);        assert_eq!(start.sandbox, expected_sandbox);        assert_eq!(resume.sandbox, expected_sandbox);        assert_eq!(fork.sandbox, expected_sandbox);        assert_eq!(start.permissions, None);        assert_eq!(resume.permissions, None);        assert_eq!(fork.permissions, None);        assert_eq!(start.thread_source, Some(ThreadSource::User));        assert_eq!(fork.thread_source, Some(ThreadSource::User));    }    #[test]    fn sandbox_mode_does_not_project_non_cwd_write_roots_for_remote_sessions() {        let cwd = test_path_buf("/workspace/project").abs();        let extra_root = test_path_buf("/workspace/cache").abs();        let permission_profile: PermissionProfile = PermissionProfile::Managed {            network: NetworkSandboxPolicy::Restricted,            file_system: ManagedFileSystemPermissions::Restricted {                entries: vec![                    FileSystemSandboxEntry {                        path: FileSystemPath::Special {                            value: FileSystemSpecialPath::Root,                        },                        access: FileSystemAccessMode::Read,                    },                    FileSystemSandboxEntry {                        path: FileSystemPath::Path { path: extra_root },                        access: FileSystemAccessMode::Write,                    },                ],                glob_scan_max_depth: None,            },        };        assert_eq!(            sandbox_mode_from_permission_profile(&permission_profile, cwd.as_path()),            Some(codex_app_server_protocol::SandboxMode::ReadOnly)        );    }    #[test]    fn sandbox_mode_projects_cwd_write_for_remote_sessions() {        let cwd = test_path_buf("/workspace/project").abs();        let permission_profile: PermissionProfile = PermissionProfile::Managed {            network: NetworkSandboxPolicy::Restricted,            file_system: ManagedFileSystemPermissions::Restricted {                entries: vec![                    FileSystemSandboxEntry {                        path: FileSystemPath::Special {                            value: FileSystemSpecialPath::Root,                        },                        access: FileSystemAccessMode::Read,                    },                    FileSystemSandboxEntry {                        path: FileSystemPath::Special {                            value: FileSystemSpecialPath::ProjectRoots { subpath: None },                        },                        access: FileSystemAccessMode::Write,                    },                ],                glob_scan_max_depth: None,            },        };        assert_eq!(            sandbox_mode_from_permission_profile(&permission_profile, cwd.as_path()),            Some(codex_app_server_protocol::SandboxMode::WorkspaceWrite)        );    }    #[tokio::test]    async fn thread_lifecycle_params_forward_explicit_remote_cwd_override_for_remote_sessions() {        let temp_dir = tempfile::tempdir().expect("tempdir");        let config = build_config(&temp_dir).await;        let thread_id = ThreadId::new();        let remote_cwd = PathBuf::from("repo/on/server");        let expected_sandbox = sandbox_mode_from_permission_profile(            &config.permissions.effective_permission_profile(),            config.cwd.as_path(),        );        let start = thread_start_params_from_config(            &config,            ThreadParamsMode::Remote,            Some(remote_cwd.as_path()),            /*session_start_source*/ None,        );        let resume = thread_resume_params_from_config(            config.clone(),            thread_id,            ThreadParamsMode::Remote,            Some(remote_cwd.as_path()),        );        let fork = thread_fork_params_from_config(            config,            thread_id,            ThreadParamsMode::Remote,            Some(remote_cwd.as_path()),        );        assert_eq!(start.cwd.as_deref(), Some("repo/on/server"));        assert_eq!(resume.cwd.as_deref(), Some("repo/on/server"));        assert_eq!(fork.cwd.as_deref(), Some("repo/on/server"));        assert_eq!(start.model_provider, None);        assert_eq!(resume.model_provider, None);        assert_eq!(fork.model_provider, None);        assert_eq!(start.sandbox, expected_sandbox);        assert_eq!(resume.sandbox, expected_sandbox);        assert_eq!(fork.sandbox, expected_sandbox);        assert_eq!(start.permissions, None);        assert_eq!(resume.permissions, None);        assert_eq!(fork.permissions, None);        assert_eq!(start.thread_source, Some(ThreadSource::User));        assert_eq!(fork.thread_source, Some(ThreadSource::User));    }    #[tokio::test]    async fn thread_lifecycle_params_forward_config_overrides_and_service_tier() {        let temp_dir = tempfile::tempdir().expect("tempdir");        let mut config = build_config(&temp_dir).await;        config.model_reasoning_effort = Some(ReasoningEffort::High);        config.model_reasoning_summary = Some(ReasoningSummary::Detailed);        config.model_verbosity = Some(Verbosity::Low);        config.personality = Some(Personality::Pragmatic);        config            .web_search_mode            .set(WebSearchMode::Disabled)            .expect("test web search mode should be allowed");        config.bypass_hook_trust = true;        config.service_tier = Some(ServiceTier::Fast.request_value().to_string());        let thread_id = ThreadId::new();        let start = thread_start_params_from_config(            &config,            ThreadParamsMode::Embedded,            /*remote_cwd_override*/ None,            /*session_start_source*/ None,        );        let resume = thread_resume_params_from_config(            config.clone(),            thread_id,            ThreadParamsMode::Embedded,            /*remote_cwd_override*/ None,        );        let fork = thread_fork_params_from_config(            config,            thread_id,            ThreadParamsMode::Embedded,            /*remote_cwd_override*/ None,        );        let expected_service_tier = Some(Some(ServiceTier::Fast.request_value().to_string()));        assert_eq!(start.service_tier, expected_service_tier);        assert_eq!(resume.service_tier, expected_service_tier);        assert_eq!(fork.service_tier, expected_service_tier);        let string = |value: &str| serde_json::Value::String(value.to_string());        let expected_config = HashMap::from([            ("model_reasoning_effort".to_string(), string("high")),            ("model_reasoning_summary".to_string(), string("detailed")),            ("model_verbosity".to_string(), string("low")),            ("personality".to_string(), string("pragmatic")),            ("web_search".to_string(), string("disabled")),            ("bypass_hook_trust".to_string(), true.into()),        ]);        assert_eq!(start.config, Some(expected_config.clone()));        assert_eq!(resume.config, Some(expected_config.clone()));        assert_eq!(fork.config, Some(expected_config));    }    #[tokio::test]    async fn config_request_overrides_preserve_implicit_personality_default() {        let temp_dir = tempfile::tempdir().expect("tempdir");        let mut config = build_config(&temp_dir).await;        config.personality = None;        let implicit_overrides =            config_request_overrides_from_config(&config).expect("config overrides");        assert!(!implicit_overrides.contains_key("personality"));        config.personality = Some(Personality::None);        let explicit_overrides =            config_request_overrides_from_config(&config).expect("config overrides");        assert_eq!(            explicit_overrides.get("personality"),            Some(&serde_json::Value::String("none".to_string()))        );    }    #[tokio::test]    async fn thread_fork_params_forward_instruction_overrides() {        let temp_dir = tempfile::tempdir().expect("tempdir");        let mut config = build_config(&temp_dir).await;        config.base_instructions = Some("Base override.".to_string());        config.developer_instructions = Some("Developer override.".to_string());        let thread_id = ThreadId::new();        let params = thread_fork_params_from_config(            config,            thread_id,            ThreadParamsMode::Embedded,            /*remote_cwd_override*/ None,        );        assert_eq!(params.base_instructions.as_deref(), Some("Base override."));        assert_eq!(            params.developer_instructions.as_deref(),            Some("Developer override.")        );    }    #[tokio::test]    async fn terminal_visualization_instructions_are_gated_for_all_tui_thread_flows() {        let temp_dir = tempfile::tempdir().expect("tempdir");        let mut config = build_config(&temp_dir).await;        config.developer_instructions = Some("Developer override.".to_string());        let thread_id = ThreadId::new();        let control_start = thread_start_params_from_config(            &config,            ThreadParamsMode::Embedded,            /*remote_cwd_override*/ None,            /*session_start_source*/ None,        );        let control_resume = thread_resume_params_from_config(            config.clone(),            thread_id,            ThreadParamsMode::Embedded,            /*remote_cwd_override*/ None,        );        let control_fork = thread_fork_params_from_config(            config.clone(),            thread_id,            ThreadParamsMode::Embedded,            /*remote_cwd_override*/ None,        );        assert_eq!(control_start.developer_instructions, None);        assert_eq!(control_resume.developer_instructions, None);        assert_eq!(            control_fork.developer_instructions.as_deref(),            Some("Developer override.")        );        let _ = config            .features            .enable(Feature::TerminalVisualizationInstructions);        let treatment_start = thread_start_params_from_config(            &config,            ThreadParamsMode::Embedded,            /*remote_cwd_override*/ None,            /*session_start_source*/ None,        );        let treatment_resume = thread_resume_params_from_config(            config.clone(),            thread_id,            ThreadParamsMode::Embedded,            /*remote_cwd_override*/ None,        );        let treatment_fork = thread_fork_params_from_config(            config,            thread_id,            ThreadParamsMode::Embedded,            /*remote_cwd_override*/ None,        );        let expected = format!(            "Developer override.\n\n{}",            crate::terminal_visualization_instructions::TERMINAL_VISUALIZATION_INSTRUCTIONS        );        assert_eq!(            treatment_start.developer_instructions.as_deref(),            Some(expected.as_str())        );        assert_eq!(            treatment_resume.developer_instructions.as_deref(),            Some(expected.as_str())        );        assert_eq!(            treatment_fork.developer_instructions.as_deref(),            Some(expected.as_str())        );    }    #[tokio::test]    async fn resume_response_restores_turns_from_thread_items() {        let temp_dir = tempfile::tempdir().expect("tempdir");        let config = build_config(&temp_dir).await;        let thread_id = ThreadId::new();        let forked_from_id = ThreadId::new();        let read_only_profile = PermissionProfile::read_only();        let response = ThreadResumeResponse {            thread: codex_app_server_protocol::Thread {                id: thread_id.to_string(),                session_id: ThreadId::new().to_string(),                forked_from_id: Some(forked_from_id.to_string()),                parent_thread_id: None,                preview: "hello".to_string(),                ephemeral: false,                model_provider: "openai".to_string(),                created_at: 1,                updated_at: 2,                status: ThreadStatus::Idle,                path: None,                cwd: test_path_buf("/tmp/project").abs(),                cli_version: "0.0.0".to_string(),                source: codex_app_server_protocol::SessionSource::Cli,                thread_source: None,                agent_nickname: None,                agent_role: None,                git_info: None,                name: None,                turns: vec![Turn {                    id: "turn-1".to_string(),                    items_view: codex_app_server_protocol::TurnItemsView::Full,                    items: vec![                        codex_app_server_protocol::ThreadItem::UserMessage {                            id: "user-1".to_string(),                            client_id: None,                            content: vec![codex_app_server_protocol::UserInput::Text {                                text: "hello from history".to_string(),                                text_elements: Vec::new(),                            }],                        },                        codex_app_server_protocol::ThreadItem::AgentMessage {                            id: "assistant-1".to_string(),                            text: "assistant reply".to_string(),                            phase: None,                            memory_citation: None,                        },                    ],                    status: TurnStatus::Completed,                    error: None,                    started_at: None,                    completed_at: None,                    duration_ms: None,                }],            },            model: "gpt-5.4".to_string(),            model_provider: "openai".to_string(),            service_tier: None,            cwd: test_path_buf("/tmp/project").abs(),            runtime_workspace_roots: vec![                test_path_buf("/tmp/project").abs(),                test_path_buf("/tmp/project/extra").abs(),            ],            instruction_sources: vec![test_path_buf("/tmp/project/AGENTS.md").abs()],            approval_policy: codex_app_server_protocol::AskForApproval::Never,            approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::User,            sandbox: read_only_profile                .to_legacy_sandbox_policy(test_path_buf("/tmp/project").as_path())                .expect("read-only profile must be legacy-compatible")                .into(),            active_permission_profile: None,            reasoning_effort: None,            initial_turns_page: None,        };        let started = started_thread_from_resume_response(            response.clone(),            &config,            ThreadParamsMode::Remote,        )        .await        .expect("resume response should map");        assert_eq!(started.session.forked_from_id, Some(forked_from_id));        assert_eq!(            started.session.runtime_workspace_roots,            response.runtime_workspace_roots        );        assert_eq!(            started.session.instruction_source_paths,            response.instruction_sources        );        assert_eq!(started.session.permission_profile, read_only_profile);        assert_eq!(started.turns.len(), 1);        assert_eq!(started.turns[0], response.thread.turns[0]);        let embedded_config = ConfigBuilder::default()            .codex_home(temp_dir.path().join("embedded-codex-home"))            .harness_overrides(ConfigOverrides {                default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()),                ..ConfigOverrides::default()            })            .build()            .await            .expect("config should build");        let started = started_thread_from_resume_response(            response.clone(),            &embedded_config,            ThreadParamsMode::Embedded,        )        .await        .expect("embedded resume response should map");        assert_eq!(started.session.permission_profile, read_only_profile);        let mut empty_roots_response = response;        empty_roots_response.runtime_workspace_roots = Vec::new();        let started = started_thread_from_resume_response(            empty_roots_response,            &config,            ThreadParamsMode::Remote,        )        .await        .expect("resume response should map");        assert_eq!(started.session.runtime_workspace_roots, Vec::new());    }    #[tokio::test]    async fn remote_thread_response_uses_legacy_sandbox_fallback() {        let temp_dir = tempfile::tempdir().expect("tempdir");        let config = build_config(&temp_dir).await;        let cwd = test_path_buf("/tmp/project").abs();        let sandbox = PermissionProfile::read_only()            .to_legacy_sandbox_policy(cwd.as_path())            .expect("read-only profile must be legacy-compatible")            .into();        assert_eq!(            display_permission_profile_from_thread_response(                &sandbox,                cwd.as_path(),                &config,                ThreadParamsMode::Remote,            ),            PermissionProfile::read_only()        );    }    #[tokio::test]    async fn embedded_thread_response_uses_local_config_profile() {        let temp_dir = tempfile::tempdir().expect("tempdir");        let config = ConfigBuilder::default()            .codex_home(temp_dir.path().to_path_buf())            .harness_overrides(ConfigOverrides {                default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string()),                ..ConfigOverrides::default()            })            .build()            .await            .expect("config should build");        let cwd = test_path_buf("/tmp/project").abs();        assert_eq!(            display_permission_profile_from_thread_response(                &codex_app_server_protocol::SandboxPolicy::DangerFullAccess,                cwd.as_path(),                &config,                ThreadParamsMode::Embedded,            ),            PermissionProfile::read_only()        );    }    #[tokio::test]    async fn session_configured_populates_history_metadata() {        let temp_dir = tempfile::tempdir().expect("tempdir");        let config = build_config(&temp_dir).await;        let thread_id = ThreadId::new();        let history_config =            codex_message_history::HistoryConfig::new(config.codex_home.clone(), &config.history);        codex_message_history::append_entry("older", &thread_id, &history_config)            .await            .expect("history append should succeed");        codex_message_history::append_entry("newer", &thread_id, &history_config)            .await            .expect("history append should succeed");        let session = thread_session_state_from_thread_response(            &thread_id.to_string(),            /*forked_from_id*/ None,            Some("restore".to_string()),            /*rollout_path*/ None,            "gpt-5.4".to_string(),            "openai".to_string(),            /*service_tier*/ None,            AskForApproval::Never,            codex_protocol::config_types::ApprovalsReviewer::User,            PermissionProfile::read_only(),            /*active_permission_profile*/ None,            test_path_buf("/tmp/project").abs(),            Vec::new(),            Vec::new(),            /*reasoning_effort*/ None,            &config,        )        .await        .expect("session should map");        let metadata = session            .message_history            .expect("session should include message-history metadata");        assert_ne!(metadata.log_id, 0);        assert_eq!(metadata.entry_count, 2);    }    #[tokio::test]    async fn session_configured_preserves_fork_source_thread_id() {        let temp_dir = tempfile::tempdir().expect("tempdir");        let config = build_config(&temp_dir).await;        let thread_id = ThreadId::new();        let forked_from_id = ThreadId::new();        let session = thread_session_state_from_thread_response(            &thread_id.to_string(),            Some(forked_from_id.to_string()),            Some("restore".to_string()),            /*rollout_path*/ None,            "gpt-5.4".to_string(),            "openai".to_string(),            /*service_tier*/ None,            AskForApproval::Never,            codex_protocol::config_types::ApprovalsReviewer::User,            PermissionProfile::read_only(),            /*active_permission_profile*/ None,            test_path_buf("/tmp/project").abs(),            Vec::new(),            Vec::new(),            /*reasoning_effort*/ None,            &config,        )        .await        .expect("session should map");        assert_eq!(session.forked_from_id, Some(forked_from_id));    }    #[test]    fn status_account_display_from_auth_mode_uses_remapped_plan_labels() {        let business = status_account_display_from_auth_mode(            Some(AuthMode::Chatgpt),            Some(codex_protocol::account::PlanType::EnterpriseCbpUsageBased),        );        assert!(matches!(            business,            Some(StatusAccountDisplay::ChatGpt {                email: None,                plan: Some(ref plan),            }) if plan == "Enterprise"        ));        let team = status_account_display_from_auth_mode(            Some(AuthMode::Chatgpt),            Some(codex_protocol::account::PlanType::SelfServeBusinessUsageBased),        );        assert!(matches!(            team,            Some(StatusAccountDisplay::ChatGpt {                email: None,                plan: Some(ref plan),            }) if plan == "Business"        ));    }}