Merge remote-tracking branch 'origin/main' into jif/use-last-used

This commit is contained in:
jif-oai
2026-02-25 16:26:59 +00:00
10 changed files with 112 additions and 91 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -2095,6 +2095,7 @@ dependencies = [
"codex-utils-absolute-path",
"codex-utils-string",
"eventsource-stream",
"gethostname",
"http 1.4.0",
"opentelemetry",
"opentelemetry-appender-tracing",

View File

@@ -178,6 +178,7 @@ env_logger = "0.11.9"
eventsource-stream = "0.2.3"
futures = { version = "0.3", default-features = false }
globset = "0.4"
gethostname = "1.1.0"
http = "1.3.1"
icu_decimal = "2.1"
icu_locale_core = "2.1"

View File

@@ -41,12 +41,6 @@
"minimum": 1.0,
"type": "integer"
},
"max_spawn_depth": {
"description": "Maximum depth for thread-spawned subagents.",
"format": "uint",
"minimum": 1.0,
"type": "integer"
},
"max_threads": {
"description": "Maximum number of agent threads that can be open concurrently. When unset, no limit is enforced.",
"format": "uint",
@@ -2063,7 +2057,7 @@
"$ref": "#/definitions/AbsolutePathBuf"
}
],
"description": "Directory where Codex stores the SQLite state DB. Defaults to `$CODEX_SQLITE_HOME` when set. Otherwise uses a temp dir under WorkspaceWrite sandboxing and `$CODEX_HOME` for other modes."
"description": "Directory where Codex stores the SQLite state DB. Defaults to `$CODEX_SQLITE_HOME` when set. Otherwise uses `$CODEX_HOME`."
},
"suppress_unstable_features_warning": {
"description": "Suppress warnings about unstable (under development) features.",

View File

@@ -1,4 +1,3 @@
use crate::config::DEFAULT_AGENT_MAX_SPAWN_DEPTH;
use crate::error::CodexErr;
use crate::error::Result;
use codex_protocol::ThreadId;
@@ -43,10 +42,6 @@ pub(crate) fn next_thread_spawn_depth(session_source: &SessionSource) -> i32 {
session_depth(session_source).saturating_add(1)
}
pub(crate) fn max_thread_spawn_depth(max_depth: Option<usize>) -> i32 {
let max_depth = max_depth.or(DEFAULT_AGENT_MAX_SPAWN_DEPTH).unwrap_or(1);
i32::try_from(max_depth).unwrap_or(i32::MAX)
}
pub(crate) fn exceeds_thread_spawn_depth_limit(depth: i32, max_depth: i32) -> bool {
depth > max_depth
}

View File

@@ -6,6 +6,5 @@ pub(crate) mod status;
pub(crate) use codex_protocol::protocol::AgentStatus;
pub(crate) use control::AgentControl;
pub(crate) use guards::exceeds_thread_spawn_depth_limit;
pub(crate) use guards::max_thread_spawn_depth;
pub(crate) use guards::next_thread_spawn_depth;
pub(crate) use status::agent_status_from_event;

View File

@@ -115,22 +115,11 @@ pub use codex_git::GhostSnapshotConfig;
/// the context window.
pub(crate) const PROJECT_DOC_MAX_BYTES: usize = 32 * 1024; // 32 KiB
pub(crate) const DEFAULT_AGENT_MAX_THREADS: Option<usize> = Some(6);
pub(crate) const DEFAULT_AGENT_MAX_SPAWN_DEPTH: Option<usize> = Some(2);
pub(crate) const DEFAULT_AGENT_MAX_DEPTH: i32 = 1;
pub(crate) const DEFAULT_AGENT_JOB_MAX_RUNTIME_SECONDS: Option<u64> = None;
pub const CONFIG_TOML_FILE: &str = "config.toml";
fn default_sqlite_home(sandbox_policy: &SandboxPolicy, codex_home: &Path) -> PathBuf {
if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) {
let mut path = std::env::temp_dir();
path.push("codex-sqlite");
path
} else {
codex_home.to_path_buf()
}
}
fn resolve_sqlite_home_env(resolved_cwd: &Path) -> Option<PathBuf> {
let raw = std::env::var(codex_state::SQLITE_HOME_ENV).ok()?;
let trimmed = raw.trim();
@@ -357,8 +346,6 @@ pub struct Config {
/// Maximum number of agent threads that can be open concurrently.
pub agent_max_threads: Option<usize>,
/// Maximum depth for thread-spawned subagents.
pub agent_max_spawn_depth: Option<usize>,
/// Maximum runtime in seconds for agent job workers before they are failed.
pub agent_job_max_runtime_seconds: Option<u64>,
@@ -1149,8 +1136,7 @@ pub struct ConfigToml {
pub history: Option<History>,
/// Directory where Codex stores the SQLite state DB.
/// Defaults to `$CODEX_SQLITE_HOME` when set. Otherwise uses a temp dir
/// under WorkspaceWrite sandboxing and `$CODEX_HOME` for other modes.
/// Defaults to `$CODEX_SQLITE_HOME` when set. Otherwise uses `$CODEX_HOME`.
pub sqlite_home: Option<AbsolutePathBuf>,
/// Directory where Codex writes log files, for example `codex-tui.log`.
@@ -1340,9 +1326,6 @@ pub struct AgentsToml {
/// When unset, no limit is enforced.
#[schemars(range(min = 1))]
pub max_threads: Option<usize>,
/// Maximum depth for thread-spawned subagents.
#[schemars(range(min = 1))]
pub max_spawn_depth: Option<usize>,
/// Maximum nesting depth allowed for spawned agent threads.
/// Root sessions start at depth 0.
#[schemars(range(min = 1))]
@@ -1865,25 +1848,6 @@ impl Config {
})
.transpose()?
.unwrap_or_default();
let agent_max_spawn_depth = cfg
.agents
.as_ref()
.and_then(|agents| agents.max_spawn_depth)
.or(DEFAULT_AGENT_MAX_SPAWN_DEPTH);
if agent_max_spawn_depth == Some(0) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"agents.max_spawn_depth must be at least 1",
));
}
if let Some(max_spawn_depth) = agent_max_spawn_depth
&& max_spawn_depth > i32::MAX as usize
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"agents.max_spawn_depth must fit within a 32-bit signed integer",
));
}
let agent_job_max_runtime_seconds = cfg
.agents
.as_ref()
@@ -2032,7 +1996,7 @@ impl Config {
.as_ref()
.map(AbsolutePathBuf::to_path_buf)
.or_else(|| resolve_sqlite_home_env(&resolved_cwd))
.unwrap_or_else(|| default_sqlite_home(&sandbox_policy, &codex_home));
.unwrap_or_else(|| codex_home.to_path_buf());
// Ensure that every field of ConfigRequirements is applied to the final
// Config.
@@ -2149,7 +2113,6 @@ impl Config {
agent_max_depth,
agent_roles,
memories: cfg.memories.unwrap_or_default().into(),
agent_max_spawn_depth,
agent_job_max_runtime_seconds,
codex_home,
sqlite_home,
@@ -2986,6 +2949,23 @@ trust_level = "trusted"
Ok(())
}
#[test]
fn sqlite_home_defaults_to_codex_home_for_workspace_write() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let config = Config::load_from_base_config_with_overrides(
ConfigToml::default(),
ConfigOverrides {
sandbox_mode: Some(SandboxMode::WorkspaceWrite),
..Default::default()
},
codex_home.path().to_path_buf(),
)?;
assert_eq!(config.sqlite_home, codex_home.path().to_path_buf());
Ok(())
}
#[test]
fn config_defaults_to_file_cli_auth_store_mode() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
@@ -4490,7 +4470,6 @@ model = "gpt-5.1-codex"
let cfg = ConfigToml {
agents: Some(AgentsToml {
max_threads: None,
max_spawn_depth: None,
max_depth: None,
job_max_runtime_seconds: None,
roles: BTreeMap::from([(
@@ -4766,7 +4745,6 @@ model_verbosity = "high"
agent_max_depth: DEFAULT_AGENT_MAX_DEPTH,
agent_roles: BTreeMap::new(),
memories: MemoriesConfig::default(),
agent_max_spawn_depth: DEFAULT_AGENT_MAX_SPAWN_DEPTH,
agent_job_max_runtime_seconds: DEFAULT_AGENT_JOB_MAX_RUNTIME_SECONDS,
codex_home: fixture.codex_home(),
sqlite_home: fixture.codex_home(),
@@ -4893,7 +4871,6 @@ model_verbosity = "high"
agent_max_depth: DEFAULT_AGENT_MAX_DEPTH,
agent_roles: BTreeMap::new(),
memories: MemoriesConfig::default(),
agent_max_spawn_depth: DEFAULT_AGENT_MAX_SPAWN_DEPTH,
agent_job_max_runtime_seconds: DEFAULT_AGENT_JOB_MAX_RUNTIME_SECONDS,
codex_home: fixture.codex_home(),
sqlite_home: fixture.codex_home(),
@@ -5018,7 +4995,6 @@ model_verbosity = "high"
agent_max_depth: DEFAULT_AGENT_MAX_DEPTH,
agent_roles: BTreeMap::new(),
memories: MemoriesConfig::default(),
agent_max_spawn_depth: DEFAULT_AGENT_MAX_SPAWN_DEPTH,
agent_job_max_runtime_seconds: DEFAULT_AGENT_JOB_MAX_RUNTIME_SECONDS,
codex_home: fixture.codex_home(),
sqlite_home: fixture.codex_home(),
@@ -5129,7 +5105,6 @@ model_verbosity = "high"
agent_max_depth: DEFAULT_AGENT_MAX_DEPTH,
agent_roles: BTreeMap::new(),
memories: MemoriesConfig::default(),
agent_max_spawn_depth: DEFAULT_AGENT_MAX_SPAWN_DEPTH,
agent_job_max_runtime_seconds: DEFAULT_AGENT_JOB_MAX_RUNTIME_SECONDS,
codex_home: fixture.codex_home(),
sqlite_home: fixture.codex_home(),

View File

@@ -1,5 +1,4 @@
use crate::agent::exceeds_thread_spawn_depth_limit;
use crate::agent::max_thread_spawn_depth;
use crate::agent::next_thread_spawn_depth;
use crate::agent::status::is_final;
use crate::codex::Session;
@@ -531,7 +530,7 @@ async fn build_runner_options(
) -> Result<JobRunnerOptions, FunctionCallError> {
let session_source = turn.session_source.clone();
let child_depth = next_thread_spawn_depth(&session_source);
let max_depth = max_thread_spawn_depth(turn.config.agent_max_spawn_depth);
let max_depth = turn.config.agent_max_depth;
if exceeds_thread_spawn_depth_limit(child_depth, max_depth) {
return Err(FunctionCallError::RespondToModel(
"agent depth limit reached; this session cannot spawn more subagents".to_string(),
@@ -540,7 +539,7 @@ async fn build_runner_options(
let max_concurrency =
normalize_concurrency(requested_concurrency, turn.config.agent_max_threads);
let base_instructions = session.get_base_instructions().await;
let spawn_config = build_agent_spawn_config(&base_instructions, turn.as_ref(), child_depth)?;
let spawn_config = build_agent_spawn_config(&base_instructions, turn.as_ref())?;
Ok(JobRunnerOptions {
max_concurrency,
spawn_config,

View File

@@ -1,6 +1,5 @@
use crate::agent::AgentStatus;
use crate::agent::exceeds_thread_spawn_depth_limit;
use crate::agent::max_thread_spawn_depth;
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::config::Config;
@@ -96,7 +95,6 @@ mod spawn {
use crate::agent::role::apply_role_to_config;
use crate::agent::exceeds_thread_spawn_depth_limit;
use crate::agent::max_thread_spawn_depth;
use crate::agent::next_thread_spawn_depth;
use std::sync::Arc;
@@ -129,7 +127,7 @@ mod spawn {
let prompt = input_preview(&input_items);
let session_source = turn.session_source.clone();
let child_depth = next_thread_spawn_depth(&session_source);
let max_depth = max_thread_spawn_depth(turn.config.agent_max_spawn_depth);
let max_depth = turn.config.agent_max_depth;
if exceeds_thread_spawn_depth_limit(child_depth, max_depth) {
return Err(FunctionCallError::RespondToModel(
"Agent depth limit reached. Solve the task yourself.".to_string(),
@@ -146,11 +144,8 @@ mod spawn {
.into(),
)
.await;
let mut config = build_agent_spawn_config(
&session.get_base_instructions().await,
turn.as_ref(),
child_depth,
)?;
let mut config =
build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?;
apply_role_to_config(&mut config, role_name)
.await
.map_err(FunctionCallError::RespondToModel)?;
@@ -346,7 +341,7 @@ mod resume_agent {
.await
.unwrap_or((None, None));
let child_depth = next_thread_spawn_depth(&turn.session_source);
let max_depth = max_thread_spawn_depth(turn.config.agent_max_spawn_depth);
let max_depth = turn.config.agent_max_depth;
if exceeds_thread_spawn_depth_limit(child_depth, max_depth) {
return Err(FunctionCallError::RespondToModel(
"Agent depth limit reached. Solve the task yourself.".to_string(),
@@ -896,9 +891,8 @@ fn input_preview(items: &[UserInput]) -> String {
pub(crate) fn build_agent_spawn_config(
base_instructions: &BaseInstructions,
turn: &TurnContext,
child_depth: i32,
) -> Result<Config, FunctionCallError> {
let mut config = build_agent_shared_config(turn, child_depth)?;
let mut config = build_agent_shared_config(turn)?;
config.base_instructions = Some(base_instructions.text.clone());
Ok(config)
}
@@ -907,16 +901,14 @@ fn build_agent_resume_config(
turn: &TurnContext,
child_depth: i32,
) -> Result<Config, FunctionCallError> {
let mut config = build_agent_shared_config(turn, child_depth)?;
let mut config = build_agent_shared_config(turn)?;
apply_spawn_agent_overrides(&mut config, child_depth);
// For resume, keep base instructions sourced from rollout/session metadata.
config.base_instructions = None;
Ok(config)
}
fn build_agent_shared_config(
turn: &TurnContext,
child_depth: i32,
) -> Result<Config, FunctionCallError> {
fn build_agent_shared_config(turn: &TurnContext) -> Result<Config, FunctionCallError> {
let base_config = turn.config.clone();
let mut config = (*base_config).clone();
config.model = Some(turn.model_info.slug.clone());
@@ -926,7 +918,6 @@ fn build_agent_shared_config(
config.developer_instructions = turn.developer_instructions.clone();
config.compact_prompt = turn.compact_prompt.clone();
apply_spawn_agent_runtime_overrides(&mut config, turn)?;
apply_spawn_agent_overrides(&mut config, child_depth);
Ok(config)
}
@@ -956,8 +947,7 @@ fn apply_spawn_agent_runtime_overrides(
}
fn apply_spawn_agent_overrides(config: &mut Config, child_depth: i32) {
let max_depth = max_thread_spawn_depth(config.agent_max_spawn_depth);
if exceeds_thread_spawn_depth_limit(child_depth + 1, max_depth) {
if child_depth >= config.agent_max_depth {
config.features.disable(Feature::Collab);
}
}
@@ -968,7 +958,6 @@ mod tests {
use crate::AuthManager;
use crate::CodexAuth;
use crate::ThreadManager;
use crate::agent::max_thread_spawn_depth;
use crate::built_in_model_providers;
use crate::codex::make_session_and_context;
use crate::config::DEFAULT_AGENT_MAX_DEPTH;
@@ -1273,7 +1262,7 @@ mod tests {
let manager = thread_manager();
session.services.agent_control = manager.agent_control();
let max_depth = max_thread_spawn_depth(turn.config.agent_max_spawn_depth);
let max_depth = turn.config.agent_max_depth;
turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id: session.conversation_id,
depth: max_depth,
@@ -1704,7 +1693,7 @@ mod tests {
let manager = thread_manager();
session.services.agent_control = manager.agent_control();
let max_depth = max_thread_spawn_depth(turn.config.agent_max_spawn_depth);
let max_depth = turn.config.agent_max_depth;
turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id: session.conversation_id,
depth: max_depth,
@@ -2051,7 +2040,7 @@ mod tests {
.set(AskForApproval::OnRequest)
.expect("approval policy set");
let config = build_agent_spawn_config(&base_instructions, &turn, 0).expect("spawn config");
let config = build_agent_spawn_config(&base_instructions, &turn).expect("spawn config");
let mut expected = (*turn.config).clone();
expected.base_instructions = Some(base_instructions.text);
expected.model = Some(turn.model_info.slug.clone());
@@ -2087,7 +2076,7 @@ mod tests {
text: "base".to_string(),
};
let config = build_agent_spawn_config(&base_instructions, &turn, 0).expect("spawn config");
let config = build_agent_spawn_config(&base_instructions, &turn).expect("spawn config");
assert_eq!(config.user_instructions, base_config.user_instructions);
}

View File

@@ -26,6 +26,7 @@ codex-utils-string = { workspace = true }
codex-api = { workspace = true }
codex-protocol = { workspace = true }
eventsource-stream = { workspace = true }
gethostname = { workspace = true }
opentelemetry = { workspace = true, features = ["logs", "metrics", "trace"] }
opentelemetry-appender-tracing = { workspace = true }
opentelemetry-otlp = { workspace = true, features = [

View File

@@ -3,6 +3,7 @@ use crate::config::OtelHttpProtocol;
use crate::config::OtelSettings;
use crate::metrics::MetricsClient;
use crate::metrics::MetricsConfig;
use gethostname::gethostname;
use opentelemetry::Context;
use opentelemetry::KeyValue;
use opentelemetry::context::ContextGuard;
@@ -40,6 +41,7 @@ use tracing_subscriber::Layer;
use tracing_subscriber::registry::LookupSpan;
const ENV_ATTRIBUTE: &str = "env";
const HOST_NAME_ATTRIBUTE: &str = "host.name";
const TRACEPARENT_ENV_VAR: &str = "TRACEPARENT";
const TRACESTATE_ENV_VAR: &str = "TRACESTATE";
static TRACEPARENT_CONTEXT: OnceLock<Option<Context>> = OnceLock::new();
@@ -223,16 +225,37 @@ fn extract_traceparent_context(traceparent: String, tracestate: Option<String>)
fn make_resource(settings: &OtelSettings) -> Resource {
Resource::builder()
.with_service_name(settings.service_name.clone())
.with_attributes(vec![
KeyValue::new(
semconv::attribute::SERVICE_VERSION,
settings.service_version.clone(),
),
KeyValue::new(ENV_ATTRIBUTE, settings.environment.clone()),
])
.with_attributes(resource_attributes(
settings,
detected_host_name().as_deref(),
))
.build()
}
fn resource_attributes(settings: &OtelSettings, host_name: Option<&str>) -> Vec<KeyValue> {
let mut attributes = vec![
KeyValue::new(
semconv::attribute::SERVICE_VERSION,
settings.service_version.clone(),
),
KeyValue::new(ENV_ATTRIBUTE, settings.environment.clone()),
];
if let Some(host_name) = host_name.and_then(normalize_host_name) {
attributes.push(KeyValue::new(HOST_NAME_ATTRIBUTE, host_name));
}
attributes
}
fn detected_host_name() -> Option<String> {
let host_name = gethostname();
normalize_host_name(host_name.to_string_lossy().as_ref())
}
fn normalize_host_name(host_name: &str) -> Option<String> {
let host_name = host_name.trim();
(!host_name.is_empty()).then(|| host_name.to_owned())
}
fn build_logger(
resource: &Resource,
exporter: &OtelExporter,
@@ -377,6 +400,8 @@ mod tests {
use opentelemetry::trace::SpanId;
use opentelemetry::trace::TraceContextExt;
use opentelemetry::trace::TraceId;
use pretty_assertions::assert_eq;
use std::path::PathBuf;
#[test]
fn parses_valid_traceparent() {
@@ -398,4 +423,46 @@ mod tests {
fn invalid_traceparent_returns_none() {
assert!(extract_traceparent_context("not-a-traceparent".to_string(), None).is_none());
}
#[test]
fn resource_attributes_include_host_name_when_present() {
let attrs = resource_attributes(&test_otel_settings(), Some("opentelemetry-test"));
let host_name = attrs
.iter()
.find(|kv| kv.key.as_str() == HOST_NAME_ATTRIBUTE)
.map(|kv| kv.value.as_str().to_string());
assert_eq!(host_name, Some("opentelemetry-test".to_string()));
}
#[test]
fn resource_attributes_omit_host_name_when_missing_or_empty() {
let missing = resource_attributes(&test_otel_settings(), None);
let empty = resource_attributes(&test_otel_settings(), Some(" "));
assert!(
!missing
.iter()
.any(|kv| kv.key.as_str() == HOST_NAME_ATTRIBUTE)
);
assert!(
!empty
.iter()
.any(|kv| kv.key.as_str() == HOST_NAME_ATTRIBUTE)
);
}
fn test_otel_settings() -> OtelSettings {
OtelSettings {
environment: "test".to_string(),
service_name: "codex-test".to_string(),
service_version: "0.0.0".to_string(),
codex_home: PathBuf::from("."),
exporter: OtelExporter::None,
trace_exporter: OtelExporter::None,
metrics_exporter: OtelExporter::None,
runtime_metrics: false,
}
}
}