mirror of
https://github.com/openai/codex.git
synced 2026-05-26 05:55:36 +00:00
1306 lines
45 KiB
Rust
1306 lines
45 KiB
Rust
use crate::realtime_conversation::handle_audio as handle_realtime_conversation_audio;
|
|
use crate::realtime_conversation::handle_close as handle_realtime_conversation_close;
|
|
use crate::realtime_conversation::handle_start as handle_realtime_conversation_start;
|
|
use crate::realtime_conversation::handle_text as handle_realtime_conversation_text;
|
|
use async_channel::Receiver;
|
|
use codex_otel::set_parent_from_w3c_trace_context;
|
|
use codex_protocol::protocol::Submission;
|
|
use tracing::Instrument;
|
|
use tracing::debug_span;
|
|
use tracing::info_span;
|
|
|
|
use crate::session::SteerInputError;
|
|
use crate::session::session::Session;
|
|
use crate::session::session::SessionSettingsUpdate;
|
|
|
|
use crate::config::Config;
|
|
use crate::realtime_context::REALTIME_TURN_TOKEN_BUDGET;
|
|
use crate::realtime_context::truncate_realtime_text_to_token_budget;
|
|
use crate::realtime_conversation::REALTIME_USER_TEXT_PREFIX;
|
|
use crate::realtime_conversation::prefix_realtime_v2_text;
|
|
use crate::session::spawn_review_thread;
|
|
use codex_config::CloudRequirementsLoader;
|
|
use codex_config::LoaderOverrides;
|
|
use codex_config::loader::load_config_layers_state;
|
|
use codex_exec_server::LOCAL_FS;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
|
|
use crate::review_prompts::resolve_review_request;
|
|
use crate::tasks::CompactTask;
|
|
use crate::tasks::UndoTask;
|
|
use crate::tasks::UserShellCommandMode;
|
|
use crate::tasks::UserShellCommandTask;
|
|
use crate::tasks::execute_user_shell_command;
|
|
use codex_mcp::collect_mcp_snapshot_from_manager;
|
|
use codex_mcp::compute_auth_statuses;
|
|
use codex_protocol::models::ContentItem;
|
|
use codex_protocol::models::PermissionProfile;
|
|
use codex_protocol::models::ResponseInputItem;
|
|
use codex_protocol::protocol::CodexErrorInfo;
|
|
use codex_protocol::protocol::ErrorEvent;
|
|
use codex_protocol::protocol::Event;
|
|
use codex_protocol::protocol::EventMsg;
|
|
use codex_protocol::protocol::GuardianAssessmentEvent;
|
|
use codex_protocol::protocol::GuardianAssessmentStatus;
|
|
use codex_protocol::protocol::InterAgentCommunication;
|
|
use codex_protocol::protocol::ListSkillsResponseEvent;
|
|
use codex_protocol::protocol::McpServerRefreshConfig;
|
|
use codex_protocol::protocol::Op;
|
|
use codex_protocol::protocol::RealtimeConversationListVoicesResponseEvent;
|
|
use codex_protocol::protocol::RealtimeVoicesList;
|
|
use codex_protocol::protocol::ReviewDecision;
|
|
use codex_protocol::protocol::ReviewRequest;
|
|
use codex_protocol::protocol::RolloutItem;
|
|
use codex_protocol::protocol::SandboxPolicy;
|
|
use codex_protocol::protocol::SkillErrorInfo;
|
|
use codex_protocol::protocol::SkillsListEntry;
|
|
use codex_protocol::protocol::ThreadMemoryMode;
|
|
use codex_protocol::protocol::ThreadNameUpdatedEvent;
|
|
use codex_protocol::protocol::ThreadRolledBackEvent;
|
|
use codex_protocol::protocol::TurnAbortReason;
|
|
use codex_protocol::protocol::WarningEvent;
|
|
use codex_protocol::request_permissions::RequestPermissionsResponse;
|
|
use codex_protocol::request_user_input::RequestUserInputResponse;
|
|
|
|
use crate::context_manager::is_user_turn_boundary;
|
|
use codex_protocol::config_types::CollaborationMode;
|
|
use codex_protocol::config_types::ModeKind;
|
|
use codex_protocol::config_types::Settings;
|
|
use codex_protocol::dynamic_tools::DynamicToolResponse;
|
|
use codex_protocol::items::UserMessageItem;
|
|
use codex_protocol::mcp::RequestId as ProtocolRequestId;
|
|
use codex_protocol::user_input::UserInput;
|
|
use codex_rmcp_client::ElicitationAction;
|
|
use codex_rmcp_client::ElicitationResponse;
|
|
use serde_json::Value;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use tracing::debug;
|
|
use tracing::info;
|
|
use tracing::warn;
|
|
|
|
pub async fn interrupt(sess: &Arc<Session>) {
|
|
sess.interrupt_task().await;
|
|
}
|
|
|
|
pub async fn clean_background_terminals(sess: &Arc<Session>) {
|
|
sess.close_unified_exec_processes().await;
|
|
}
|
|
|
|
pub async fn realtime_conversation_list_voices(sess: &Session, sub_id: String) {
|
|
sess.send_event_raw(Event {
|
|
id: sub_id,
|
|
msg: EventMsg::RealtimeConversationListVoicesResponse(
|
|
RealtimeConversationListVoicesResponseEvent {
|
|
voices: RealtimeVoicesList::builtin(),
|
|
},
|
|
),
|
|
})
|
|
.await;
|
|
}
|
|
|
|
pub async fn override_turn_context(sess: &Session, sub_id: String, updates: SessionSettingsUpdate) {
|
|
if let Err(err) = sess.update_settings(updates).await {
|
|
sess.send_event_raw(Event {
|
|
id: sub_id,
|
|
msg: EventMsg::Error(ErrorEvent {
|
|
message: err.to_string(),
|
|
codex_error_info: Some(CodexErrorInfo::BadRequest),
|
|
}),
|
|
})
|
|
.await;
|
|
}
|
|
}
|
|
|
|
pub async fn user_input_or_turn(sess: &Arc<Session>, sub_id: String, op: Op) {
|
|
user_input_or_turn_inner(
|
|
sess,
|
|
sub_id,
|
|
op,
|
|
/*mirror_user_text_to_realtime*/ Some(()),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
pub(super) async fn user_input_or_turn_inner(
|
|
sess: &Arc<Session>,
|
|
sub_id: String,
|
|
op: Op,
|
|
mirror_user_text_to_realtime: Option<()>,
|
|
) {
|
|
let (items, updates, responsesapi_client_metadata) = match op {
|
|
Op::UserTurn {
|
|
cwd,
|
|
approval_policy,
|
|
approvals_reviewer,
|
|
sandbox_policy,
|
|
permission_profile,
|
|
model,
|
|
effort,
|
|
summary,
|
|
service_tier,
|
|
final_output_json_schema,
|
|
items,
|
|
collaboration_mode,
|
|
personality,
|
|
environments,
|
|
} => {
|
|
let collaboration_mode = collaboration_mode.or_else(|| {
|
|
Some(CollaborationMode {
|
|
mode: ModeKind::Default,
|
|
settings: Settings {
|
|
model: model.clone(),
|
|
reasoning_effort: effort,
|
|
developer_instructions: None,
|
|
},
|
|
})
|
|
});
|
|
let clear_active_permission_profile =
|
|
permission_profile.is_none() && sandbox_policy.is_some();
|
|
let permission_profile = permission_profile_with_legacy_fallback(
|
|
sess,
|
|
sandbox_policy.as_ref(),
|
|
permission_profile,
|
|
Some(cwd.as_path()),
|
|
)
|
|
.await;
|
|
(
|
|
items,
|
|
SessionSettingsUpdate {
|
|
cwd: Some(cwd),
|
|
approval_policy: Some(approval_policy),
|
|
approvals_reviewer,
|
|
permission_profile,
|
|
active_permission_profile: None,
|
|
clear_active_permission_profile,
|
|
windows_sandbox_level: None,
|
|
collaboration_mode,
|
|
reasoning_summary: summary,
|
|
service_tier,
|
|
final_output_json_schema: Some(final_output_json_schema),
|
|
environments,
|
|
personality,
|
|
app_server_client_name: None,
|
|
app_server_client_version: None,
|
|
},
|
|
None,
|
|
)
|
|
}
|
|
Op::UserInputWithTurnContext {
|
|
cwd,
|
|
approval_policy,
|
|
approvals_reviewer,
|
|
sandbox_policy,
|
|
permission_profile,
|
|
active_permission_profile,
|
|
windows_sandbox_level,
|
|
model,
|
|
effort,
|
|
summary,
|
|
service_tier,
|
|
final_output_json_schema,
|
|
items,
|
|
responsesapi_client_metadata,
|
|
collaboration_mode,
|
|
personality,
|
|
environments,
|
|
} => {
|
|
let collaboration_mode = if let Some(collab_mode) = collaboration_mode {
|
|
Some(collab_mode)
|
|
} else {
|
|
let state = sess.state.lock().await;
|
|
Some(
|
|
state
|
|
.session_configuration
|
|
.collaboration_mode
|
|
.with_updates(model, effort, /*developer_instructions*/ None),
|
|
)
|
|
};
|
|
let clear_active_permission_profile =
|
|
permission_profile.is_none() && sandbox_policy.is_some();
|
|
let permission_profile = permission_profile_with_legacy_fallback(
|
|
sess,
|
|
sandbox_policy.as_ref(),
|
|
permission_profile,
|
|
cwd.as_deref(),
|
|
)
|
|
.await;
|
|
(
|
|
items,
|
|
SessionSettingsUpdate {
|
|
cwd,
|
|
approval_policy,
|
|
approvals_reviewer,
|
|
permission_profile,
|
|
active_permission_profile,
|
|
clear_active_permission_profile,
|
|
windows_sandbox_level,
|
|
collaboration_mode,
|
|
reasoning_summary: summary,
|
|
service_tier,
|
|
final_output_json_schema: Some(final_output_json_schema),
|
|
environments,
|
|
personality,
|
|
app_server_client_name: None,
|
|
app_server_client_version: None,
|
|
},
|
|
responsesapi_client_metadata,
|
|
)
|
|
}
|
|
Op::UserInput {
|
|
items,
|
|
environments,
|
|
final_output_json_schema,
|
|
responsesapi_client_metadata,
|
|
} => (
|
|
items,
|
|
SessionSettingsUpdate {
|
|
final_output_json_schema: Some(final_output_json_schema),
|
|
environments,
|
|
..Default::default()
|
|
},
|
|
responsesapi_client_metadata,
|
|
),
|
|
_ => unreachable!(),
|
|
};
|
|
|
|
let Ok(current_context) = sess.new_turn_with_sub_id(sub_id.clone(), updates).await else {
|
|
// new_turn_with_sub_id already emits the error event.
|
|
return;
|
|
};
|
|
sess.maybe_emit_unknown_model_warning_for_turn(current_context.as_ref())
|
|
.await;
|
|
let accepted_items = match sess
|
|
.steer_input(
|
|
items.clone(),
|
|
/*expected_turn_id*/ None,
|
|
responsesapi_client_metadata.clone(),
|
|
)
|
|
.await
|
|
{
|
|
Ok(_) => {
|
|
current_context.session_telemetry.user_prompt(&items);
|
|
Some(items)
|
|
}
|
|
Err(SteerInputError::NoActiveTurn(items)) => {
|
|
if let Some(responsesapi_client_metadata) = responsesapi_client_metadata {
|
|
current_context
|
|
.turn_metadata_state
|
|
.set_responsesapi_client_metadata(responsesapi_client_metadata);
|
|
}
|
|
current_context.session_telemetry.user_prompt(&items);
|
|
sess.refresh_mcp_servers_if_requested(¤t_context)
|
|
.await;
|
|
let accepted_items = items.clone();
|
|
sess.spawn_task(
|
|
Arc::clone(¤t_context),
|
|
items,
|
|
crate::tasks::RegularTask::new(),
|
|
)
|
|
.await;
|
|
Some(accepted_items)
|
|
}
|
|
Err(err) => {
|
|
sess.send_event_raw(Event {
|
|
id: sub_id,
|
|
msg: EventMsg::Error(err.to_error_event()),
|
|
})
|
|
.await;
|
|
None
|
|
}
|
|
};
|
|
if let (Some(items), Some(())) = (accepted_items, mirror_user_text_to_realtime) {
|
|
self::mirror_user_text_to_realtime(sess, &items).await;
|
|
}
|
|
}
|
|
|
|
async fn permission_profile_with_legacy_fallback(
|
|
sess: &Session,
|
|
sandbox_policy: Option<&SandboxPolicy>,
|
|
permission_profile: Option<PermissionProfile>,
|
|
cwd: Option<&Path>,
|
|
) -> Option<PermissionProfile> {
|
|
match (permission_profile, sandbox_policy) {
|
|
(Some(permission_profile), _) => Some(permission_profile),
|
|
(None, Some(sandbox_policy)) => Some(
|
|
sess.permission_profile_from_legacy_sandbox_update(sandbox_policy, cwd)
|
|
.await,
|
|
),
|
|
(None, None) => None,
|
|
}
|
|
}
|
|
|
|
async fn mirror_user_text_to_realtime(sess: &Arc<Session>, items: &[UserInput]) {
|
|
let text = UserMessageItem::new(items).message();
|
|
if text.is_empty() {
|
|
return;
|
|
}
|
|
let text = if sess.conversation.is_running_v2().await {
|
|
prefix_realtime_v2_text(text, REALTIME_USER_TEXT_PREFIX)
|
|
} else {
|
|
text
|
|
};
|
|
let text = truncate_realtime_text_to_token_budget(&text, REALTIME_TURN_TOKEN_BUDGET);
|
|
if text.is_empty() {
|
|
return;
|
|
}
|
|
if sess.conversation.running_state().await.is_none() {
|
|
return;
|
|
}
|
|
if let Err(err) = sess.conversation.text_in(text).await {
|
|
debug!("failed to mirror user text to realtime conversation: {err}");
|
|
}
|
|
}
|
|
|
|
/// Records an inter-agent assistant envelope, then lets the shared pending-work scheduler
|
|
/// decide whether an idle session should start a regular turn.
|
|
pub async fn inter_agent_communication(
|
|
sess: &Arc<Session>,
|
|
sub_id: String,
|
|
communication: InterAgentCommunication,
|
|
) {
|
|
let trigger_turn = communication.trigger_turn;
|
|
sess.enqueue_mailbox_communication(communication);
|
|
if trigger_turn {
|
|
sess.maybe_start_turn_for_pending_work_with_sub_id(sub_id)
|
|
.await;
|
|
}
|
|
}
|
|
|
|
pub async fn run_user_shell_command(sess: &Arc<Session>, sub_id: String, command: String) {
|
|
if let Some((turn_context, cancellation_token)) =
|
|
sess.active_turn_context_and_cancellation_token().await
|
|
{
|
|
let session = Arc::clone(sess);
|
|
tokio::spawn(async move {
|
|
execute_user_shell_command(
|
|
session,
|
|
turn_context,
|
|
command,
|
|
cancellation_token,
|
|
UserShellCommandMode::ActiveTurnAuxiliary,
|
|
)
|
|
.await;
|
|
});
|
|
return;
|
|
}
|
|
|
|
let turn_context = sess.new_default_turn_with_sub_id(sub_id).await;
|
|
sess.spawn_task(
|
|
Arc::clone(&turn_context),
|
|
Vec::new(),
|
|
UserShellCommandTask::new(command),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
pub async fn resolve_elicitation(
|
|
sess: &Arc<Session>,
|
|
server_name: String,
|
|
request_id: ProtocolRequestId,
|
|
decision: codex_protocol::approvals::ElicitationAction,
|
|
content: Option<Value>,
|
|
meta: Option<Value>,
|
|
) {
|
|
let action = match decision {
|
|
codex_protocol::approvals::ElicitationAction::Accept => ElicitationAction::Accept,
|
|
codex_protocol::approvals::ElicitationAction::Decline => ElicitationAction::Decline,
|
|
codex_protocol::approvals::ElicitationAction::Cancel => ElicitationAction::Cancel,
|
|
};
|
|
let content = match action {
|
|
// Preserve the legacy fallback for clients that only send an action.
|
|
ElicitationAction::Accept => Some(content.unwrap_or_else(|| serde_json::json!({}))),
|
|
ElicitationAction::Decline | ElicitationAction::Cancel => None,
|
|
};
|
|
let response = ElicitationResponse {
|
|
action,
|
|
content,
|
|
meta,
|
|
};
|
|
let request_id = match request_id {
|
|
ProtocolRequestId::String(value) => {
|
|
rmcp::model::NumberOrString::String(std::sync::Arc::from(value))
|
|
}
|
|
ProtocolRequestId::Integer(value) => rmcp::model::NumberOrString::Number(value),
|
|
};
|
|
if let Err(err) = sess
|
|
.resolve_elicitation(server_name, request_id, response)
|
|
.await
|
|
{
|
|
warn!(
|
|
error = %err,
|
|
"failed to resolve elicitation request in session"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Propagate a user's exec approval decision to the session.
|
|
/// Also optionally applies an execpolicy amendment.
|
|
pub async fn exec_approval(
|
|
sess: &Arc<Session>,
|
|
approval_id: String,
|
|
turn_id: Option<String>,
|
|
decision: ReviewDecision,
|
|
) {
|
|
let event_turn_id = turn_id.unwrap_or_else(|| approval_id.clone());
|
|
if let ReviewDecision::ApprovedExecpolicyAmendment {
|
|
proposed_execpolicy_amendment,
|
|
} = &decision
|
|
{
|
|
match sess
|
|
.persist_execpolicy_amendment(proposed_execpolicy_amendment)
|
|
.await
|
|
{
|
|
Ok(()) => {
|
|
sess.record_execpolicy_amendment_message(
|
|
&event_turn_id,
|
|
proposed_execpolicy_amendment,
|
|
)
|
|
.await;
|
|
}
|
|
Err(err) => {
|
|
let message = format!("Failed to apply execpolicy amendment: {err}");
|
|
tracing::warn!("{message}");
|
|
let warning = EventMsg::Warning(WarningEvent { message });
|
|
sess.send_event_raw(Event {
|
|
id: event_turn_id.clone(),
|
|
msg: warning,
|
|
})
|
|
.await;
|
|
}
|
|
}
|
|
}
|
|
match decision {
|
|
ReviewDecision::Abort => {
|
|
sess.interrupt_task().await;
|
|
}
|
|
other => sess.notify_approval(&approval_id, other).await,
|
|
}
|
|
}
|
|
|
|
pub async fn patch_approval(sess: &Arc<Session>, id: String, decision: ReviewDecision) {
|
|
match decision {
|
|
ReviewDecision::Abort => {
|
|
sess.interrupt_task().await;
|
|
}
|
|
other => sess.notify_approval(&id, other).await,
|
|
}
|
|
}
|
|
|
|
pub async fn request_user_input_response(
|
|
sess: &Arc<Session>,
|
|
id: String,
|
|
response: RequestUserInputResponse,
|
|
) {
|
|
sess.notify_user_input_response(&id, response).await;
|
|
}
|
|
|
|
pub async fn request_permissions_response(
|
|
sess: &Arc<Session>,
|
|
id: String,
|
|
response: RequestPermissionsResponse,
|
|
) {
|
|
sess.notify_request_permissions_response(&id, response)
|
|
.await;
|
|
}
|
|
|
|
pub async fn dynamic_tool_response(sess: &Arc<Session>, id: String, response: DynamicToolResponse) {
|
|
sess.notify_dynamic_tool_response(&id, response).await;
|
|
}
|
|
|
|
pub async fn add_to_history(sess: &Arc<Session>, config: &Arc<Config>, text: String) {
|
|
let id = sess.conversation_id;
|
|
let config = Arc::clone(config);
|
|
tokio::spawn(async move {
|
|
if let Err(e) = crate::message_history::append_entry(&text, &id, &config).await {
|
|
warn!("failed to append to message history: {e}");
|
|
}
|
|
});
|
|
}
|
|
|
|
pub async fn get_history_entry_request(
|
|
sess: &Arc<Session>,
|
|
config: &Arc<Config>,
|
|
sub_id: String,
|
|
offset: usize,
|
|
log_id: u64,
|
|
) {
|
|
let config = Arc::clone(config);
|
|
let sess_clone = Arc::clone(sess);
|
|
|
|
tokio::spawn(async move {
|
|
// Run lookup in blocking thread because it does file IO + locking.
|
|
let entry_opt = tokio::task::spawn_blocking(move || {
|
|
crate::message_history::lookup(log_id, offset, &config)
|
|
})
|
|
.await
|
|
.unwrap_or(None);
|
|
|
|
let event = Event {
|
|
id: sub_id,
|
|
msg: EventMsg::GetHistoryEntryResponse(
|
|
codex_protocol::protocol::GetHistoryEntryResponseEvent {
|
|
offset,
|
|
log_id,
|
|
entry: entry_opt.map(|e| codex_protocol::message_history::HistoryEntry {
|
|
conversation_id: e.session_id,
|
|
ts: e.ts,
|
|
text: e.text,
|
|
}),
|
|
},
|
|
),
|
|
};
|
|
|
|
sess_clone.send_event_raw(event).await;
|
|
});
|
|
}
|
|
|
|
pub async fn refresh_mcp_servers(sess: &Arc<Session>, refresh_config: McpServerRefreshConfig) {
|
|
let mut guard = sess.pending_mcp_server_refresh_config.lock().await;
|
|
*guard = Some(refresh_config);
|
|
}
|
|
|
|
pub async fn reload_user_config(sess: &Arc<Session>) {
|
|
sess.reload_user_config_layer().await;
|
|
}
|
|
|
|
#[expect(
|
|
clippy::await_holding_invalid_type,
|
|
reason = "MCP tool listing reads through the session-owned manager guard"
|
|
)]
|
|
pub async fn list_mcp_tools(sess: &Session, config: &Arc<Config>, sub_id: String) {
|
|
let mcp_connection_manager = sess.services.mcp_connection_manager.read().await;
|
|
let auth = sess.services.auth_manager.auth().await;
|
|
let mcp_servers = sess
|
|
.services
|
|
.mcp_manager
|
|
.effective_servers(config, auth.as_ref())
|
|
.await;
|
|
let snapshot = collect_mcp_snapshot_from_manager(
|
|
&mcp_connection_manager,
|
|
compute_auth_statuses(
|
|
mcp_servers.iter(),
|
|
config.mcp_oauth_credentials_store_mode,
|
|
auth.as_ref(),
|
|
)
|
|
.await,
|
|
)
|
|
.await;
|
|
let event = Event {
|
|
id: sub_id,
|
|
msg: EventMsg::McpListToolsResponse(snapshot),
|
|
};
|
|
sess.send_event_raw(event).await;
|
|
}
|
|
|
|
pub async fn list_skills(sess: &Session, sub_id: String, cwds: Vec<PathBuf>, force_reload: bool) {
|
|
let default_cwd = {
|
|
let state = sess.state.lock().await;
|
|
state.session_configuration.cwd.to_path_buf()
|
|
};
|
|
let cwds = if cwds.is_empty() {
|
|
vec![default_cwd]
|
|
} else {
|
|
cwds
|
|
};
|
|
|
|
let skills_manager = &sess.services.skills_manager;
|
|
let plugins_manager = &sess.services.plugins_manager;
|
|
let fs = sess
|
|
.services
|
|
.environment_manager
|
|
.default_environment()
|
|
.map(|environment| environment.get_filesystem());
|
|
let config = sess.get_config().await;
|
|
let codex_home = sess.codex_home().await;
|
|
let mut skills = Vec::new();
|
|
let empty_cli_overrides: &[(String, toml::Value)] = &[];
|
|
for cwd in cwds {
|
|
let cwd_abs = match AbsolutePathBuf::relative_to_current_dir(cwd.as_path()) {
|
|
Ok(path) => path,
|
|
Err(err) => {
|
|
let error_path = cwd.clone();
|
|
skills.push(SkillsListEntry {
|
|
cwd,
|
|
skills: Vec::new(),
|
|
errors: vec![SkillErrorInfo {
|
|
path: error_path,
|
|
message: err.to_string(),
|
|
}],
|
|
});
|
|
continue;
|
|
}
|
|
};
|
|
let config_layer_stack = match load_config_layers_state(
|
|
LOCAL_FS.as_ref(),
|
|
&codex_home,
|
|
Some(cwd_abs.clone()),
|
|
empty_cli_overrides,
|
|
LoaderOverrides::default(),
|
|
CloudRequirementsLoader::default(),
|
|
&codex_config::NoopThreadConfigLoader,
|
|
)
|
|
.await
|
|
{
|
|
Ok(config_layer_stack) => config_layer_stack,
|
|
Err(err) => {
|
|
let error_path = cwd.clone();
|
|
skills.push(SkillsListEntry {
|
|
cwd,
|
|
skills: Vec::new(),
|
|
errors: vec![SkillErrorInfo {
|
|
path: error_path,
|
|
message: err.to_string(),
|
|
}],
|
|
});
|
|
continue;
|
|
}
|
|
};
|
|
let effective_skill_roots = plugins_manager
|
|
.effective_skill_roots_for_layer_stack(&config_layer_stack, &config)
|
|
.await;
|
|
let skills_input = crate::SkillsLoadInput::new(
|
|
cwd_abs.clone(),
|
|
effective_skill_roots,
|
|
config_layer_stack,
|
|
config.bundled_skills_enabled(),
|
|
);
|
|
let outcome = skills_manager
|
|
.skills_for_cwd(&skills_input, force_reload, fs.clone())
|
|
.await;
|
|
let errors = super::errors_to_info(&outcome.errors);
|
|
let skills_metadata = super::skills_to_info(&outcome.skills, &outcome.disabled_paths);
|
|
skills.push(SkillsListEntry {
|
|
cwd,
|
|
skills: skills_metadata,
|
|
errors,
|
|
});
|
|
}
|
|
|
|
let event = Event {
|
|
id: sub_id,
|
|
msg: EventMsg::ListSkillsResponse(ListSkillsResponseEvent { skills }),
|
|
};
|
|
sess.send_event_raw(event).await;
|
|
}
|
|
|
|
pub async fn undo(sess: &Arc<Session>, sub_id: String) {
|
|
let turn_context = sess.new_default_turn_with_sub_id(sub_id).await;
|
|
sess.spawn_task(turn_context, Vec::new(), UndoTask::new())
|
|
.await;
|
|
}
|
|
|
|
pub async fn compact(sess: &Arc<Session>, sub_id: String) {
|
|
let turn_context = sess.new_default_turn_with_sub_id(sub_id).await;
|
|
|
|
sess.spawn_task(
|
|
Arc::clone(&turn_context),
|
|
vec![UserInput::Text {
|
|
text: turn_context.compact_prompt().to_string(),
|
|
// Compaction prompt is synthesized; no UI element ranges to preserve.
|
|
text_elements: Vec::new(),
|
|
}],
|
|
CompactTask,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
pub async fn thread_rollback(sess: &Arc<Session>, sub_id: String, num_turns: u32) {
|
|
if num_turns == 0 {
|
|
sess.send_event_raw(Event {
|
|
id: sub_id,
|
|
msg: EventMsg::Error(ErrorEvent {
|
|
message: "num_turns must be >= 1".to_string(),
|
|
codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
|
|
}),
|
|
})
|
|
.await;
|
|
return;
|
|
}
|
|
|
|
let has_active_turn = { sess.active_turn.lock().await.is_some() };
|
|
if has_active_turn {
|
|
sess.send_event_raw(Event {
|
|
id: sub_id,
|
|
msg: EventMsg::Error(ErrorEvent {
|
|
message: "Cannot rollback while a turn is in progress.".to_string(),
|
|
codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
|
|
}),
|
|
})
|
|
.await;
|
|
return;
|
|
}
|
|
|
|
let turn_context = sess.new_default_turn_with_sub_id(sub_id).await;
|
|
let live_thread = match sess.live_thread_for_persistence("rollback thread") {
|
|
Ok(live_thread) => live_thread,
|
|
Err(_) => {
|
|
sess.send_event_raw(Event {
|
|
id: turn_context.sub_id.clone(),
|
|
msg: EventMsg::Error(ErrorEvent {
|
|
message: "thread rollback requires persisted thread history".to_string(),
|
|
codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
|
|
}),
|
|
})
|
|
.await;
|
|
return;
|
|
}
|
|
};
|
|
if let Err(err) = live_thread.flush().await {
|
|
sess.send_event_raw(Event {
|
|
id: turn_context.sub_id.clone(),
|
|
msg: EventMsg::Error(ErrorEvent {
|
|
message: format!("failed to flush thread persistence for rollback replay: {err}"),
|
|
codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
|
|
}),
|
|
})
|
|
.await;
|
|
return;
|
|
}
|
|
|
|
let stored_history = match live_thread.load_history(/*include_archived*/ false).await {
|
|
Ok(history) => history,
|
|
Err(err) => {
|
|
sess.send_event_raw(Event {
|
|
id: turn_context.sub_id.clone(),
|
|
msg: EventMsg::Error(ErrorEvent {
|
|
message: format!("failed to load thread history for rollback replay: {err}"),
|
|
codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
|
|
}),
|
|
})
|
|
.await;
|
|
return;
|
|
}
|
|
};
|
|
|
|
let rollback_event = ThreadRolledBackEvent { num_turns };
|
|
let rollback_msg = EventMsg::ThreadRolledBack(rollback_event.clone());
|
|
let replay_items = stored_history
|
|
.items
|
|
.into_iter()
|
|
.chain(std::iter::once(RolloutItem::EventMsg(rollback_msg.clone())))
|
|
.collect::<Vec<_>>();
|
|
sess.apply_rollout_reconstruction(turn_context.as_ref(), replay_items.as_slice())
|
|
.await;
|
|
sess.recompute_token_usage(turn_context.as_ref()).await;
|
|
|
|
sess.persist_rollout_items(&[RolloutItem::EventMsg(rollback_msg.clone())])
|
|
.await;
|
|
if let Err(err) = sess.flush_rollout().await {
|
|
sess.send_event(
|
|
turn_context.as_ref(),
|
|
EventMsg::Warning(WarningEvent {
|
|
message: format!(
|
|
"Rolled the thread back, but failed to save the rollback marker. Codex will continue retrying. Error: {err}"
|
|
),
|
|
}),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
sess.deliver_event_raw(Event {
|
|
id: turn_context.sub_id.clone(),
|
|
msg: rollback_msg,
|
|
})
|
|
.await;
|
|
}
|
|
|
|
async fn persist_thread_name_update(
|
|
sess: &Arc<Session>,
|
|
event: ThreadNameUpdatedEvent,
|
|
) -> anyhow::Result<EventMsg> {
|
|
let msg = EventMsg::ThreadNameUpdated(event);
|
|
let item = RolloutItem::EventMsg(msg.clone());
|
|
let live_thread = sess.live_thread_for_persistence("rename thread")?;
|
|
live_thread.persist().await?;
|
|
live_thread
|
|
.append_items(std::slice::from_ref(&item))
|
|
.await?;
|
|
live_thread.flush().await?;
|
|
Ok(msg)
|
|
}
|
|
|
|
pub(super) async fn persist_thread_memory_mode_update(
|
|
sess: &Arc<Session>,
|
|
mode: ThreadMemoryMode,
|
|
) -> anyhow::Result<()> {
|
|
let live_thread = sess.live_thread_for_persistence("update thread memory mode")?;
|
|
live_thread.persist().await?;
|
|
live_thread.flush().await?;
|
|
live_thread
|
|
.update_memory_mode(mode, /*include_archived*/ false)
|
|
.await?;
|
|
live_thread.flush().await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Persists the thread name in the rollout and state database, updates in-memory state, and
|
|
/// emits a `ThreadNameUpdated` event on success.
|
|
pub async fn set_thread_name(sess: &Arc<Session>, sub_id: String, name: String) {
|
|
let Some(name) = crate::util::normalize_thread_name(&name) else {
|
|
let event = Event {
|
|
id: sub_id,
|
|
msg: EventMsg::Error(ErrorEvent {
|
|
message: "Thread name cannot be empty.".to_string(),
|
|
codex_error_info: Some(CodexErrorInfo::BadRequest),
|
|
}),
|
|
};
|
|
sess.send_event_raw(event).await;
|
|
return;
|
|
};
|
|
|
|
let updated = ThreadNameUpdatedEvent {
|
|
thread_id: sess.conversation_id,
|
|
thread_name: Some(name.clone()),
|
|
};
|
|
|
|
let msg = match persist_thread_name_update(sess, updated).await {
|
|
Ok(msg) => msg,
|
|
Err(err) => {
|
|
warn!("Failed to persist thread name update to rollout: {err}");
|
|
let event = Event {
|
|
id: sub_id,
|
|
msg: EventMsg::Error(ErrorEvent {
|
|
message: err.to_string(),
|
|
codex_error_info: Some(CodexErrorInfo::Other),
|
|
}),
|
|
};
|
|
sess.send_event_raw(event).await;
|
|
return;
|
|
}
|
|
};
|
|
|
|
if let Some(state_db) = sess.services.state_db.as_deref()
|
|
&& let Err(err) = state_db
|
|
.update_thread_title(sess.conversation_id, &name)
|
|
.await
|
|
{
|
|
warn!("Failed to update thread title in state db: {err}");
|
|
}
|
|
|
|
{
|
|
let mut state = sess.state.lock().await;
|
|
state.session_configuration.thread_name = Some(name.clone());
|
|
}
|
|
|
|
let codex_home = sess.codex_home().await;
|
|
if let Err(err) =
|
|
crate::rollout::append_thread_name(&codex_home, sess.conversation_id, &name).await
|
|
{
|
|
warn!("Failed to update legacy thread name index: {err}");
|
|
}
|
|
|
|
sess.deliver_event_raw(Event { id: sub_id, msg }).await;
|
|
}
|
|
|
|
/// Persists thread-level memory mode metadata for the active session.
|
|
///
|
|
/// This does not involve the model and only affects whether the thread is
|
|
/// eligible for future memory generation.
|
|
pub async fn set_thread_memory_mode(sess: &Arc<Session>, sub_id: String, mode: ThreadMemoryMode) {
|
|
if let Err(err) = persist_thread_memory_mode_update(sess, mode).await {
|
|
warn!("Failed to persist thread memory mode update to rollout: {err}");
|
|
let event = Event {
|
|
id: sub_id,
|
|
msg: EventMsg::Error(ErrorEvent {
|
|
message: err.to_string(),
|
|
codex_error_info: Some(CodexErrorInfo::Other),
|
|
}),
|
|
};
|
|
sess.send_event_raw(event).await;
|
|
}
|
|
}
|
|
|
|
pub async fn shutdown(sess: &Arc<Session>, sub_id: String) -> bool {
|
|
sess.abort_all_tasks(TurnAbortReason::Interrupted).await;
|
|
let _ = sess.conversation.shutdown().await;
|
|
sess.services
|
|
.unified_exec_manager
|
|
.terminate_all_processes()
|
|
.await;
|
|
let mcp_shutdown = {
|
|
let mut manager = sess.services.mcp_connection_manager.write().await;
|
|
manager.begin_shutdown()
|
|
};
|
|
mcp_shutdown.await;
|
|
sess.guardian_review_session.shutdown().await;
|
|
info!("Shutting down Codex instance");
|
|
let history = sess.clone_history().await;
|
|
let turn_count = history
|
|
.raw_items()
|
|
.iter()
|
|
.filter(|item| is_user_turn_boundary(item))
|
|
.count();
|
|
sess.services.session_telemetry.counter(
|
|
"codex.conversation.turn.count",
|
|
i64::try_from(turn_count).unwrap_or(0),
|
|
&[],
|
|
);
|
|
|
|
// Gracefully flush and shutdown thread persistence on session end so tests
|
|
// that inspect durable state do not race with the background writer.
|
|
if let Some(live_thread) = sess.live_thread()
|
|
&& let Err(e) = live_thread.shutdown().await
|
|
{
|
|
warn!("failed to shutdown thread persistence: {e}");
|
|
let event = Event {
|
|
id: sub_id.clone(),
|
|
msg: EventMsg::Error(ErrorEvent {
|
|
message: "Failed to shutdown thread persistence".to_string(),
|
|
codex_error_info: Some(CodexErrorInfo::Other),
|
|
}),
|
|
};
|
|
sess.send_event_raw(event).await;
|
|
}
|
|
|
|
let event = Event {
|
|
id: sub_id,
|
|
msg: EventMsg::ShutdownComplete,
|
|
};
|
|
sess.services
|
|
.rollout_thread_trace
|
|
.record_protocol_event(&event.msg);
|
|
sess.deliver_event_raw(event).await;
|
|
sess.services
|
|
.rollout_thread_trace
|
|
.record_ended(codex_rollout_trace::RolloutStatus::Completed);
|
|
true
|
|
}
|
|
|
|
pub async fn review(
|
|
sess: &Arc<Session>,
|
|
config: &Arc<Config>,
|
|
sub_id: String,
|
|
review_request: ReviewRequest,
|
|
) {
|
|
let turn_context = sess.new_default_turn_with_sub_id(sub_id.clone()).await;
|
|
sess.maybe_emit_unknown_model_warning_for_turn(turn_context.as_ref())
|
|
.await;
|
|
sess.refresh_mcp_servers_if_requested(&turn_context).await;
|
|
match resolve_review_request(review_request, &turn_context.cwd) {
|
|
Ok(resolved) => {
|
|
spawn_review_thread(
|
|
Arc::clone(sess),
|
|
Arc::clone(config),
|
|
turn_context.clone(),
|
|
sub_id,
|
|
resolved,
|
|
)
|
|
.await;
|
|
}
|
|
Err(err) => {
|
|
let event = Event {
|
|
id: sub_id,
|
|
msg: EventMsg::Error(ErrorEvent {
|
|
message: err.to_string(),
|
|
codex_error_info: Some(CodexErrorInfo::Other),
|
|
}),
|
|
};
|
|
sess.send_event(&turn_context, event.msg).await;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) async fn submission_loop(
|
|
sess: Arc<Session>,
|
|
config: Arc<Config>,
|
|
rx_sub: Receiver<Submission>,
|
|
) {
|
|
// To break out of this loop, send Op::Shutdown.
|
|
while let Ok(sub) = rx_sub.recv().await {
|
|
debug!(?sub, "Submission");
|
|
let dispatch_span = submission_dispatch_span(&sub);
|
|
let should_exit = async {
|
|
match sub.op.clone() {
|
|
Op::Interrupt => {
|
|
interrupt(&sess).await;
|
|
false
|
|
}
|
|
Op::CleanBackgroundTerminals => {
|
|
clean_background_terminals(&sess).await;
|
|
false
|
|
}
|
|
Op::RealtimeConversationStart(params) => {
|
|
if let Err(err) =
|
|
handle_realtime_conversation_start(&sess, sub.id.clone(), params).await
|
|
{
|
|
sess.send_event_raw(Event {
|
|
id: sub.id.clone(),
|
|
msg: EventMsg::Error(ErrorEvent {
|
|
message: err.to_string(),
|
|
codex_error_info: Some(CodexErrorInfo::Other),
|
|
}),
|
|
})
|
|
.await;
|
|
}
|
|
false
|
|
}
|
|
Op::RealtimeConversationAudio(params) => {
|
|
handle_realtime_conversation_audio(&sess, sub.id.clone(), params).await;
|
|
false
|
|
}
|
|
Op::RealtimeConversationText(params) => {
|
|
handle_realtime_conversation_text(&sess, sub.id.clone(), params).await;
|
|
false
|
|
}
|
|
Op::RealtimeConversationClose => {
|
|
handle_realtime_conversation_close(&sess, sub.id.clone()).await;
|
|
false
|
|
}
|
|
Op::RealtimeConversationListVoices => {
|
|
realtime_conversation_list_voices(&sess, sub.id.clone()).await;
|
|
false
|
|
}
|
|
Op::OverrideTurnContext {
|
|
cwd,
|
|
approval_policy,
|
|
approvals_reviewer,
|
|
sandbox_policy,
|
|
permission_profile,
|
|
windows_sandbox_level,
|
|
model,
|
|
effort,
|
|
summary,
|
|
service_tier,
|
|
collaboration_mode,
|
|
personality,
|
|
} => {
|
|
let collaboration_mode = if let Some(collab_mode) = collaboration_mode {
|
|
collab_mode
|
|
} else {
|
|
let state = sess.state.lock().await;
|
|
state.session_configuration.collaboration_mode.with_updates(
|
|
model.clone(),
|
|
effort,
|
|
/*developer_instructions*/ None,
|
|
)
|
|
};
|
|
let clear_active_permission_profile =
|
|
permission_profile.is_none() && sandbox_policy.is_some();
|
|
let permission_profile = permission_profile_with_legacy_fallback(
|
|
&sess,
|
|
sandbox_policy.as_ref(),
|
|
permission_profile,
|
|
cwd.as_deref(),
|
|
)
|
|
.await;
|
|
override_turn_context(
|
|
&sess,
|
|
sub.id.clone(),
|
|
SessionSettingsUpdate {
|
|
cwd,
|
|
approval_policy,
|
|
approvals_reviewer,
|
|
permission_profile,
|
|
clear_active_permission_profile,
|
|
windows_sandbox_level,
|
|
collaboration_mode: Some(collaboration_mode),
|
|
reasoning_summary: summary,
|
|
service_tier,
|
|
personality,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await;
|
|
false
|
|
}
|
|
Op::UserInput { .. }
|
|
| Op::UserInputWithTurnContext { .. }
|
|
| Op::UserTurn { .. } => {
|
|
user_input_or_turn(&sess, sub.id.clone(), sub.op).await;
|
|
false
|
|
}
|
|
Op::InterAgentCommunication { communication } => {
|
|
inter_agent_communication(&sess, sub.id.clone(), communication).await;
|
|
false
|
|
}
|
|
Op::ExecApproval {
|
|
id: approval_id,
|
|
turn_id,
|
|
decision,
|
|
} => {
|
|
exec_approval(&sess, approval_id, turn_id, decision).await;
|
|
false
|
|
}
|
|
Op::PatchApproval { id, decision } => {
|
|
patch_approval(&sess, id, decision).await;
|
|
false
|
|
}
|
|
Op::UserInputAnswer { id, response } => {
|
|
request_user_input_response(&sess, id, response).await;
|
|
false
|
|
}
|
|
Op::RequestPermissionsResponse { id, response } => {
|
|
request_permissions_response(&sess, id, response).await;
|
|
false
|
|
}
|
|
Op::DynamicToolResponse { id, response } => {
|
|
dynamic_tool_response(&sess, id, response).await;
|
|
false
|
|
}
|
|
Op::AddToHistory { text } => {
|
|
add_to_history(&sess, &config, text).await;
|
|
false
|
|
}
|
|
Op::GetHistoryEntryRequest { offset, log_id } => {
|
|
get_history_entry_request(&sess, &config, sub.id.clone(), offset, log_id).await;
|
|
false
|
|
}
|
|
Op::ListMcpTools => {
|
|
list_mcp_tools(&sess, &config, sub.id.clone()).await;
|
|
false
|
|
}
|
|
Op::RefreshMcpServers { config } => {
|
|
refresh_mcp_servers(&sess, config).await;
|
|
false
|
|
}
|
|
Op::ReloadUserConfig => {
|
|
reload_user_config(&sess).await;
|
|
false
|
|
}
|
|
Op::ListSkills { cwds, force_reload } => {
|
|
list_skills(&sess, sub.id.clone(), cwds, force_reload).await;
|
|
false
|
|
}
|
|
Op::Undo => {
|
|
undo(&sess, sub.id.clone()).await;
|
|
false
|
|
}
|
|
Op::Compact => {
|
|
compact(&sess, sub.id.clone()).await;
|
|
false
|
|
}
|
|
Op::ThreadRollback { num_turns } => {
|
|
thread_rollback(&sess, sub.id.clone(), num_turns).await;
|
|
false
|
|
}
|
|
Op::SetThreadName { name } => {
|
|
set_thread_name(&sess, sub.id.clone(), name).await;
|
|
false
|
|
}
|
|
Op::SetThreadMemoryMode { mode } => {
|
|
set_thread_memory_mode(&sess, sub.id.clone(), mode).await;
|
|
false
|
|
}
|
|
Op::RunUserShellCommand { command } => {
|
|
run_user_shell_command(&sess, sub.id.clone(), command).await;
|
|
false
|
|
}
|
|
Op::ResolveElicitation {
|
|
server_name,
|
|
request_id,
|
|
decision,
|
|
content,
|
|
meta,
|
|
} => {
|
|
resolve_elicitation(&sess, server_name, request_id, decision, content, meta)
|
|
.await;
|
|
false
|
|
}
|
|
Op::Shutdown => shutdown(&sess, sub.id.clone()).await,
|
|
Op::Review { review_request } => {
|
|
review(&sess, &config, sub.id.clone(), review_request).await;
|
|
false
|
|
}
|
|
Op::ApproveGuardianDeniedAction { event } => {
|
|
approve_guardian_denied_action(&sess, event).await;
|
|
false
|
|
}
|
|
_ => false, // Ignore unknown ops; enum is non_exhaustive to allow extensions.
|
|
}
|
|
}
|
|
.instrument(dispatch_span)
|
|
.await;
|
|
if should_exit {
|
|
break;
|
|
}
|
|
}
|
|
// If the submission loop exits because the channel closed without an
|
|
// explicit shutdown op, still run process teardown for child processes
|
|
// owned by this session.
|
|
sess.services
|
|
.unified_exec_manager
|
|
.terminate_all_processes()
|
|
.await;
|
|
let mcp_shutdown = {
|
|
let mut manager = sess.services.mcp_connection_manager.write().await;
|
|
manager.begin_shutdown()
|
|
};
|
|
mcp_shutdown.await;
|
|
// Also drain cached guardian state on this implicit shutdown path.
|
|
sess.guardian_review_session.shutdown().await;
|
|
debug!("Agent loop exited");
|
|
}
|
|
|
|
async fn approve_guardian_denied_action(sess: &Arc<Session>, event: GuardianAssessmentEvent) {
|
|
if event.status != GuardianAssessmentStatus::Denied {
|
|
warn!(
|
|
review_id = event.id.as_str(),
|
|
"ignoring approval for non-denied Guardian assessment"
|
|
);
|
|
return;
|
|
}
|
|
|
|
let approved_action = serde_json::json!({
|
|
"action": &event.action,
|
|
"outcome": "allowed",
|
|
});
|
|
let approved_action_json = match serde_json::to_string_pretty(&approved_action) {
|
|
Ok(approved_action_json) => approved_action_json,
|
|
Err(error) => {
|
|
warn!(%error, review_id = event.id.as_str(), "failed to serialize approved Guardian action");
|
|
return;
|
|
}
|
|
};
|
|
let approval_prefix = crate::guardian::AUTO_REVIEW_DENIED_ACTION_APPROVAL_DEVELOPER_PREFIX;
|
|
let text = format!(
|
|
r#"{approval_prefix}
|
|
|
|
Treat this as approval to perform that exact action in the same context in which it was originally requested.
|
|
Do not assume this also authorizes similar operations with different payloads.
|
|
|
|
Approved action:
|
|
{approved_action_json}"#,
|
|
);
|
|
let items = vec![ResponseInputItem::Message {
|
|
role: "developer".to_string(),
|
|
content: vec![ContentItem::InputText { text }],
|
|
phase: None,
|
|
}];
|
|
|
|
if let Err(items) = sess.inject_response_items(items).await {
|
|
sess.queue_response_items_for_next_turn(items).await;
|
|
}
|
|
}
|
|
|
|
pub(super) fn submission_dispatch_span(sub: &Submission) -> tracing::Span {
|
|
let op_name = sub.op.kind();
|
|
let span_name = format!("op.dispatch.{op_name}");
|
|
let dispatch_span = match &sub.op {
|
|
Op::RealtimeConversationAudio(_) => {
|
|
debug_span!(
|
|
"submission_dispatch",
|
|
otel.name = span_name.as_str(),
|
|
submission.id = sub.id.as_str(),
|
|
codex.op = op_name
|
|
)
|
|
}
|
|
_ => info_span!(
|
|
"submission_dispatch",
|
|
otel.name = span_name.as_str(),
|
|
submission.id = sub.id.as_str(),
|
|
codex.op = op_name
|
|
),
|
|
};
|
|
if let Some(trace) = sub.trace.as_ref()
|
|
&& !set_parent_from_w3c_trace_context(&dispatch_span, trace)
|
|
{
|
|
warn!(
|
|
submission.id = sub.id.as_str(),
|
|
"ignoring invalid submission trace carrier"
|
|
);
|
|
}
|
|
dispatch_span
|
|
}
|