Compare commits

..

16 Commits

Author SHA1 Message Date
Ahmed Ibrahim
9212c96815 codex: preserve null developer instructions (#16964) 2026-04-06 21:15:15 -07:00
Ahmed Ibrahim
a23c12a7fd ci: revert bazel log tail parsing change (#16964) 2026-04-06 21:05:22 -07:00
Ahmed Ibrahim
585450b16b codex: fix personality instruction assertions (#16964) 2026-04-06 20:36:58 -07:00
Ahmed Ibrahim
3047ae3fb2 ci: print failed bazel test log tails (#16964) 2026-04-06 20:28:37 -07:00
Ahmed Ibrahim
f148caa9a1 codex: preserve forked null base instructions (#16964) 2026-04-06 20:19:07 -07:00
Ahmed Ibrahim
891566471f codex: narrow null instruction coverage (#16964) 2026-04-06 20:06:44 -07:00
Ahmed Ibrahim
60bf8ff815 codex: fix instruction test clippy (#16964) 2026-04-06 19:53:10 -07:00
Ahmed Ibrahim
eedec118b7 codex: fix app-server test import (#16964) 2026-04-06 19:46:59 -07:00
Ahmed Ibrahim
aafcfec146 codex: preserve empty developer instructions (#16964) 2026-04-06 19:44:52 -07:00
Ahmed Ibrahim
8ecfca3186 codex: address null instruction review feedback (#16964) 2026-04-06 19:44:52 -07:00
Ahmed Ibrahim
81252aa0c5 codex: fix rollout metadata test (#16964) 2026-04-06 19:44:52 -07:00
Ahmed Ibrahim
e7e8ae8450 codex: address PR review feedback (#16964) 2026-04-06 19:44:52 -07:00
Ahmed Ibrahim
ae434054e4 Preserve null instruction overrides 2026-04-06 19:44:52 -07:00
Ahmed Ibrahim
7a9927a73c Add fork null instruction coverage 2026-04-06 19:44:52 -07:00
Ahmed Ibrahim
f50a52b895 codex: fix CI failure on PR #16964 2026-04-06 19:44:52 -07:00
Ahmed Ibrahim
11b179bdee Honor null thread instructions 2026-04-06 19:44:51 -07:00
39 changed files with 299 additions and 559 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -2794,7 +2794,6 @@ dependencies = [
"codex-cloud-requirements",
"codex-config",
"codex-core",
"codex-exec-server",
"codex-features",
"codex-feedback",
"codex-file-search",

View File

@@ -84,6 +84,7 @@ pub fn create_fake_rollout_with_source(
agent_role: None,
model_provider: model_provider.map(str::to_string),
base_instructions: None,
developer_instructions: None,
dynamic_tools: None,
memory_mode: None,
};
@@ -167,6 +168,7 @@ pub fn create_fake_rollout_with_text_elements(
agent_role: None,
model_provider: model_provider.map(str::to_string),
base_instructions: None,
developer_instructions: None,
dynamic_tools: None,
memory_mode: None,
};

View File

@@ -200,6 +200,10 @@ async fn thread_fork_honors_explicit_null_thread_instructions() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let config_path = codex_home.path().join("config.toml");
let mut config_toml = std::fs::read_to_string(&config_path)?;
config_toml.push_str("\ndeveloper_instructions = \"Config developer instructions sentinel\"\n");
std::fs::write(config_path, config_toml)?;
let conversation_id = create_fake_rollout(
codex_home.path(),
@@ -323,6 +327,13 @@ async fn thread_fork_honors_explicit_null_thread_instructions() -> Result<()> {
"unexpected instructions field in payload: {payload:?}"
);
let developer_texts = request.message_input_texts("developer");
assert_eq!(
developer_texts
.iter()
.any(|text| { text.contains("Config developer instructions sentinel") }),
expect_instructions,
"unexpected config developer instruction presence: {developer_texts:?}"
);
assert!(
developer_texts.iter().all(|text| !text.is_empty()),
"did not expect empty developer instruction messages: {developer_texts:?}"

View File

@@ -377,6 +377,7 @@ stream_max_retries = 0
agent_role: None,
model_provider: Some("mock_provider".to_string()),
base_instructions: None,
developer_instructions: None,
dynamic_tools: None,
memory_mode: None,
};

View File

@@ -166,6 +166,10 @@ async fn turn_start_honors_explicit_null_thread_instructions() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri(), "never", &BTreeMap::new())?;
let config_path = codex_home.path().join("config.toml");
let mut config_toml = std::fs::read_to_string(&config_path)?;
config_toml.push_str("\ndeveloper_instructions = \"Config developer instructions sentinel\"\n");
std::fs::write(config_path, config_toml)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
@@ -240,6 +244,13 @@ async fn turn_start_honors_explicit_null_thread_instructions() -> Result<()> {
"unexpected instructions field in payload: {payload:?}"
);
let developer_texts = request.message_input_texts("developer");
assert_eq!(
developer_texts
.iter()
.any(|text| { text.contains("Config developer instructions sentinel") }),
expect_instructions,
"unexpected config developer instruction presence: {developer_texts:?}"
);
assert!(
developer_texts.iter().all(|text| !text.is_empty()),
"did not expect empty developer instruction messages: {developer_texts:?}"

View File

@@ -536,11 +536,7 @@ impl Codex {
config.startup_warnings.push(message);
}
let environment = environment_manager
.current()
.await
.map_err(|err| CodexErr::Fatal(format!("failed to create environment: {err}")))?;
let user_instructions = get_user_instructions(&config, environment.as_deref()).await;
let user_instructions = get_user_instructions(&config).await;
let exec_policy = if crate::guardian::is_guardian_reviewer_source(&session_source) {
// Guardian review should rely on the built-in shell safety checks,
@@ -618,6 +614,14 @@ impl Codex {
dynamic_tools
};
let developer_instructions_override = config
.developer_instructions_override
.clone()
.or_else(|| conversation_history.get_developer_instructions());
let developer_instructions = developer_instructions_override
.clone()
.unwrap_or_else(|| config.developer_instructions.clone());
// TODO (aibrahim): Consolidate config.model and config.model_reasoning_effort into config.collaboration_mode
// to avoid extracting these fields separately and constructing CollaborationMode here.
let collaboration_mode = CollaborationMode {
@@ -633,7 +637,8 @@ impl Codex {
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
service_tier: config.service_tier,
developer_instructions: config.developer_instructions.clone(),
developer_instructions,
developer_instructions_override,
user_instructions,
personality: config.personality,
base_instructions,
@@ -672,12 +677,12 @@ impl Codex {
agent_status_tx.clone(),
conversation_history,
session_source_clone,
environment_manager,
skills_manager,
plugins_manager,
mcp_manager.clone(),
skills_watcher,
agent_control,
environment,
)
.await
.map_err(|e| {
@@ -1103,6 +1108,10 @@ pub(crate) struct SessionConfiguration {
/// Developer instructions that supplement the base instructions.
developer_instructions: Option<String>,
/// Explicit developer instructions override, preserving `null` as distinct
/// from a missing override.
developer_instructions_override: Option<Option<String>>,
/// Model instructions that are appended to the base instructions.
user_instructions: Option<String>,
@@ -1526,12 +1535,12 @@ impl Session {
agent_status: watch::Sender<AgentStatus>,
initial_history: InitialHistory,
session_source: SessionSource,
environment_manager: Arc<EnvironmentManager>,
skills_manager: Arc<SkillsManager>,
plugins_manager: Arc<PluginsManager>,
mcp_manager: Arc<McpManager>,
skills_watcher: Arc<SkillsWatcher>,
agent_control: AgentControl,
environment: Option<Arc<Environment>>,
) -> anyhow::Result<Arc<Self>> {
debug!(
"Configuring session: model={}; provider={:?}",
@@ -1553,6 +1562,9 @@ impl Session {
.base_instructions
.clone()
.map(|text| BaseInstructions { text }),
session_configuration
.developer_instructions_override
.clone(),
session_configuration.dynamic_tools.clone(),
if session_configuration.persist_extended_history {
EventPersistenceMode::Extended
@@ -1972,7 +1984,7 @@ impl Session {
code_mode_service: crate::tools::code_mode::CodeModeService::new(
config.js_repl_node_path.clone(),
),
environment,
environment: environment_manager.current().await?,
};
services
.model_client

View File

@@ -1853,6 +1853,7 @@ async fn set_rate_limits_retains_previous_credits() {
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
developer_instructions_override: config.developer_instructions_override.clone(),
user_instructions: config.user_instructions.clone(),
service_tier: None,
personality: config.personality,
@@ -1955,6 +1956,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
developer_instructions_override: config.developer_instructions_override.clone(),
user_instructions: config.user_instructions.clone(),
service_tier: None,
personality: config.personality,
@@ -2227,6 +2229,7 @@ async fn attach_rollout_recorder(session: &Arc<Session>) -> PathBuf {
/*forked_from_id*/ None,
SessionSource::Exec,
Some(BaseInstructions::default()),
/*developer_instructions*/ None,
Vec::new(),
EventPersistenceMode::Limited,
),
@@ -2304,6 +2307,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
developer_instructions_override: config.developer_instructions_override.clone(),
user_instructions: config.user_instructions.clone(),
service_tier: None,
personality: config.personality,
@@ -2570,6 +2574,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
developer_instructions_override: config.developer_instructions_override.clone(),
user_instructions: config.user_instructions.clone(),
service_tier: None,
personality: config.personality,
@@ -2616,16 +2621,14 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
agent_status_tx,
InitialHistory::New,
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
skills_manager,
plugins_manager,
mcp_manager,
Arc::new(SkillsWatcher::noop()),
AgentControl::default(),
Some(Arc::new(
codex_exec_server::Environment::create(/*exec_server_url*/ None)
.await
.expect("create environment"),
)),
)
.await;
@@ -2673,6 +2676,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
developer_instructions_override: config.developer_instructions_override.clone(),
user_instructions: config.user_instructions.clone(),
service_tier: None,
personality: config.personality,
@@ -3513,6 +3517,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
developer_instructions_override: config.developer_instructions_override.clone(),
user_instructions: config.user_instructions.clone(),
service_tier: None,
personality: config.personality,
@@ -4089,13 +4094,18 @@ async fn handle_output_item_done_records_image_save_history_message() {
let image_output_dir = image_output_path
.parent()
.expect("generated image path should have a parent");
let image_message: ResponseItem = DeveloperInstructions::new(format!(
"Generated images are saved to {} as {} by default.\nIf you need to use a generated image at another path, copy it and leave the original in place unless the user explicitly asks you to delete it.",
let save_message: ResponseItem = DeveloperInstructions::new(format!(
"Generated images are saved to {} as {} by default.",
image_output_dir.display(),
image_output_path.display(),
))
.into();
assert_eq!(history.raw_items(), &[image_message, item]);
let copy_message: ResponseItem = DeveloperInstructions::new(
"If you need to use a generated image at another path, copy it and leave the original in place unless the user explicitly asks you to delete it."
.to_string(),
)
.into();
assert_eq!(history.raw_items(), &[save_message, copy_message, item]);
assert_eq!(
std::fs::read(&expected_saved_path).expect("saved file"),
b"foo"
@@ -4264,6 +4274,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline
/*forked_from_id*/ None,
SessionSource::Exec,
Some(BaseInstructions::default()),
/*developer_instructions*/ None,
Vec::new(),
EventPersistenceMode::Limited,
),
@@ -4361,6 +4372,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_full_rei
/*forked_from_id*/ None,
SessionSource::Exec,
Some(BaseInstructions::default()),
/*developer_instructions*/ None,
Vec::new(),
EventPersistenceMode::Limited,
),

View File

@@ -4506,6 +4506,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
experimental_realtime_ws_startup_context: None,
base_instructions: None,
developer_instructions: None,
developer_instructions_override: None,
guardian_developer_instructions: None,
include_permissions_instructions: true,
include_apps_instructions: true,
@@ -4651,6 +4652,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
experimental_realtime_ws_startup_context: None,
base_instructions: None,
developer_instructions: None,
developer_instructions_override: None,
guardian_developer_instructions: None,
include_permissions_instructions: true,
include_apps_instructions: true,
@@ -4794,6 +4796,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
experimental_realtime_ws_startup_context: None,
base_instructions: None,
developer_instructions: None,
developer_instructions_override: None,
guardian_developer_instructions: None,
include_permissions_instructions: true,
include_apps_instructions: true,
@@ -4923,6 +4926,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
experimental_realtime_ws_startup_context: None,
base_instructions: None,
developer_instructions: None,
developer_instructions_override: None,
guardian_developer_instructions: None,
include_permissions_instructions: true,
include_apps_instructions: true,

View File

@@ -248,6 +248,10 @@ pub struct Config {
/// Developer instructions override injected as a separate message.
pub developer_instructions: Option<String>,
/// Explicit developer instructions override, preserving `null` as distinct
/// from a missing override.
pub developer_instructions_override: Option<Option<String>>,
/// Guardian-specific developer instructions override from requirements.toml.
pub guardian_developer_instructions: Option<String>,
@@ -1761,6 +1765,7 @@ impl Config {
let file_base_instructions =
Self::try_read_non_empty_file(model_instructions_path, "model instructions file")?;
let base_instructions = base_instructions.or_else(|| file_base_instructions.map(Some));
let developer_instructions_override = developer_instructions.clone();
let developer_instructions =
developer_instructions.unwrap_or_else(|| cfg.developer_instructions.clone());
let include_permissions_instructions = config_profile
@@ -1945,6 +1950,7 @@ impl Config {
base_instructions,
personality,
developer_instructions,
developer_instructions_override,
compact_prompt,
commit_attribution,
include_permissions_instructions,

View File

@@ -43,6 +43,7 @@ async fn write_session_with_user_event(codex_home: &Path) -> io::Result<()> {
agent_role: None,
model_provider: None,
base_instructions: None,
developer_instructions: None,
dynamic_tools: None,
memory_mode: None,
},

View File

@@ -21,12 +21,10 @@ use crate::config_loader::default_project_root_markers;
use crate::config_loader::merge_toml_values;
use crate::config_loader::project_root_markers_from_config;
use codex_app_server_protocol::ConfigLayerSource;
use codex_exec_server::Environment;
use codex_exec_server::ExecutorFileSystem;
use codex_features::Feature;
use codex_utils_absolute_path::AbsolutePathBuf;
use dunce::canonicalize as normalize_path;
use std::io;
use std::path::PathBuf;
use tokio::io::AsyncReadExt;
use toml::Value as TomlValue;
use tracing::error;
@@ -78,19 +76,8 @@ fn render_js_repl_instructions(config: &Config) -> Option<String> {
/// Combines `Config::instructions` and `AGENTS.md` (if present) into a single
/// string of instructions.
pub(crate) async fn get_user_instructions(
config: &Config,
environment: Option<&Environment>,
) -> Option<String> {
let fs = environment?.get_filesystem();
get_user_instructions_with_fs(config, fs.as_ref()).await
}
pub(crate) async fn get_user_instructions_with_fs(
config: &Config,
fs: &dyn ExecutorFileSystem,
) -> Option<String> {
let project_docs = read_project_docs_with_fs(config, fs).await;
pub(crate) async fn get_user_instructions(config: &Config) -> Option<String> {
let project_docs = read_project_docs(config).await;
let mut output = String::new();
@@ -138,25 +125,14 @@ pub(crate) async fn get_user_instructions_with_fs(
/// concatenation of all discovered docs. If no documentation file is found the
/// function returns `Ok(None)`. Unexpected I/O failures bubble up as `Err` so
/// callers can decide how to handle them.
pub async fn read_project_docs(
config: &Config,
environment: &Environment,
) -> io::Result<Option<String>> {
let fs = environment.get_filesystem();
read_project_docs_with_fs(config, fs.as_ref()).await
}
async fn read_project_docs_with_fs(
config: &Config,
fs: &dyn ExecutorFileSystem,
) -> io::Result<Option<String>> {
pub async fn read_project_docs(config: &Config) -> std::io::Result<Option<String>> {
let max_total = config.project_doc_max_bytes;
if max_total == 0 {
return Ok(None);
}
let paths = discover_project_doc_paths(config, fs).await?;
let paths = discover_project_doc_paths(config)?;
if paths.is_empty() {
return Ok(None);
}
@@ -169,22 +145,16 @@ async fn read_project_docs_with_fs(
break;
}
match fs.get_metadata(&p).await {
Ok(metadata) if !metadata.is_file => continue,
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
Err(err) => return Err(err),
}
let mut data = match fs.read_file(&p).await {
Ok(data) => data,
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
Err(err) => return Err(err),
let file = match tokio::fs::File::open(&p).await {
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(e) => return Err(e),
};
let size = data.len() as u64;
if size > remaining {
data.truncate(remaining as usize);
}
let size = file.metadata().await?.len();
let mut reader = tokio::io::BufReader::new(file).take(remaining);
let mut data: Vec<u8> = Vec::new();
reader.read_to_end(&mut data).await?;
if size > remaining {
tracing::warn!(
@@ -213,17 +183,10 @@ async fn read_project_docs_with_fs(
/// contents. The list is ordered from project root to the current working
/// directory (inclusive). Symlinks are allowed. When `project_doc_max_bytes`
/// is zero, returns an empty list.
pub async fn discover_project_doc_paths(
config: &Config,
fs: &dyn ExecutorFileSystem,
) -> io::Result<Vec<AbsolutePathBuf>> {
if config.project_doc_max_bytes == 0 {
return Ok(Vec::new());
}
let mut dir = config.cwd.clone();
pub fn discover_project_doc_paths(config: &Config) -> std::io::Result<Vec<PathBuf>> {
let mut dir = config.cwd.to_path_buf();
if let Ok(canon) = normalize_path(&dir) {
dir = AbsolutePathBuf::try_from(canon)?;
dir = canon;
}
let mut merged = TomlValue::Table(toml::map::Map::new());
@@ -248,14 +211,14 @@ pub async fn discover_project_doc_paths(
if !project_root_markers.is_empty() {
for ancestor in dir.ancestors() {
for marker in &project_root_markers {
let marker_path = AbsolutePathBuf::try_from(ancestor.join(marker))?;
let marker_exists = match fs.get_metadata(&marker_path).await {
let marker_path = ancestor.join(marker);
let marker_exists = match std::fs::metadata(&marker_path) {
Ok(_) => true,
Err(err) if err.kind() == io::ErrorKind::NotFound => false,
Err(err) => return Err(err),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => false,
Err(e) => return Err(e),
};
if marker_exists {
project_root = Some(AbsolutePathBuf::try_from(ancestor.to_path_buf())?);
project_root = Some(ancestor.to_path_buf());
break;
}
}
@@ -265,11 +228,11 @@ pub async fn discover_project_doc_paths(
}
}
let search_dirs: Vec<AbsolutePathBuf> = if let Some(root) = project_root {
let search_dirs: Vec<PathBuf> = if let Some(root) = project_root {
let mut dirs = Vec::new();
let mut cursor = dir.clone();
let mut cursor = dir.as_path();
loop {
dirs.push(cursor.clone());
dirs.push(cursor.to_path_buf());
if cursor == root {
break;
}
@@ -284,25 +247,29 @@ pub async fn discover_project_doc_paths(
vec![dir]
};
let mut found: Vec<AbsolutePathBuf> = Vec::new();
let mut found: Vec<PathBuf> = Vec::new();
let candidate_filenames = candidate_filenames(config);
for d in search_dirs {
for name in &candidate_filenames {
let candidate = d.join(name)?;
match fs.get_metadata(&candidate).await {
Ok(md) if md.is_file => {
found.push(candidate);
break;
let candidate = d.join(name);
match std::fs::symlink_metadata(&candidate) {
Ok(md) => {
let ft = md.file_type();
// Allow regular files and symlinks; opening will later fail for dangling links.
if ft.is_file() || ft.is_symlink() {
found.push(candidate);
break;
}
}
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
Err(err) => return Err(err),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(e) => return Err(e),
}
}
}
Ok(found)
}
fn candidate_filenames<'a>(config: &'a Config) -> Vec<&'a str> {
let mut names: Vec<&'a str> =
Vec::with_capacity(2 + config.project_doc_fallback_filenames.len());

View File

@@ -1,23 +1,12 @@
use super::*;
use crate::config::ConfigBuilder;
use codex_exec_server::LOCAL_FS;
use codex_features::Feature;
use codex_utils_absolute_path::AbsolutePathBuf;
use core_test_support::PathBufExt;
use core_test_support::TempDirExt;
use pretty_assertions::assert_eq;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
async fn get_user_instructions(config: &Config) -> Option<String> {
super::get_user_instructions_with_fs(config, LOCAL_FS.as_ref()).await
}
async fn discover_project_doc_paths(config: &Config) -> std::io::Result<Vec<AbsolutePathBuf>> {
super::discover_project_doc_paths(config, LOCAL_FS.as_ref()).await
}
/// Helper that returns a `Config` pointing at `root` and using `limit` as
/// the maximum number of bytes to embed from AGENTS.md. The caller can
/// optionally specify a custom `instructions` string when `None` the
@@ -96,16 +85,6 @@ async fn no_doc_file_returns_none() {
assert!(res.is_none(), "Expected None when AGENTS.md is absent");
}
#[tokio::test]
async fn no_environment_returns_none() {
let tmp = tempfile::tempdir().expect("tempdir");
let config = make_config(&tmp, /*limit*/ 4096, Some("user instructions")).await;
let res = super::get_user_instructions(&config, /*environment*/ None).await;
assert_eq!(res, None);
}
/// Small file within the byte-limit is returned unmodified.
#[tokio::test]
async fn doc_smaller_than_limit_is_returned() {
@@ -182,18 +161,6 @@ async fn zero_byte_limit_disables_docs() {
);
}
#[tokio::test]
async fn zero_byte_limit_disables_discovery() {
let tmp = tempfile::tempdir().expect("tempdir");
fs::write(tmp.path().join("AGENTS.md"), "something").unwrap();
let discovery =
discover_project_doc_paths(&make_config(&tmp, /*limit*/ 0, /*instructions*/ None).await)
.await
.expect("discover paths");
assert_eq!(discovery, Vec::<AbsolutePathBuf>::new());
}
#[tokio::test]
async fn js_repl_instructions_are_appended_when_enabled() {
let tmp = tempfile::tempdir().expect("tempdir");
@@ -326,18 +293,11 @@ async fn project_root_markers_are_honored_for_agents_discovery() {
.await;
cfg.cwd = nested.abs();
let discovery = discover_project_doc_paths(&cfg)
.await
.expect("discover paths");
let expected_parent = AbsolutePathBuf::try_from(
dunce::canonicalize(root.path().join("AGENTS.md")).expect("canonical parent doc path"),
)
.expect("absolute parent doc path");
let expected_child = AbsolutePathBuf::try_from(
dunce::canonicalize(cfg.cwd.join("AGENTS.md").expect("absolute child doc path"))
.expect("canonical child doc path"),
)
.expect("absolute child doc path");
let discovery = discover_project_doc_paths(&cfg).expect("discover paths");
let expected_parent =
dunce::canonicalize(root.path().join("AGENTS.md")).expect("canonical parent doc path");
let expected_child =
dunce::canonicalize(cfg.cwd.as_path().join("AGENTS.md")).expect("canonical child doc path");
assert_eq!(discovery.len(), 2);
assert_eq!(discovery[0], expected_parent);
assert_eq!(discovery[1], expected_child);
@@ -361,9 +321,7 @@ async fn agents_local_md_preferred() {
assert_eq!(res, "local");
let discovery = discover_project_doc_paths(&cfg)
.await
.expect("discover paths");
let discovery = discover_project_doc_paths(&cfg).expect("discover paths");
assert_eq!(discovery.len(), 1);
assert_eq!(
discovery[0].file_name().unwrap().to_string_lossy(),
@@ -413,9 +371,7 @@ async fn agents_md_preferred_over_fallbacks() {
assert_eq!(res, "primary");
let discovery = discover_project_doc_paths(&cfg)
.await
.expect("discover paths");
let discovery = discover_project_doc_paths(&cfg).expect("discover paths");
assert_eq!(discovery.len(), 1);
assert!(
discovery[0]
@@ -426,73 +382,6 @@ async fn agents_md_preferred_over_fallbacks() {
);
}
#[tokio::test]
async fn agents_md_directory_is_ignored() {
let tmp = tempfile::tempdir().expect("tempdir");
fs::create_dir(tmp.path().join("AGENTS.md")).unwrap();
let cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await;
let res = get_user_instructions(&cfg).await;
assert_eq!(res, None);
let discovery = discover_project_doc_paths(&cfg)
.await
.expect("discover paths");
assert_eq!(discovery, Vec::<AbsolutePathBuf>::new());
}
#[cfg(unix)]
#[tokio::test]
async fn agents_md_special_file_is_ignored() {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("AGENTS.md");
let c_path = CString::new(path.as_os_str().as_bytes()).expect("path without nul");
// SAFETY: `c_path` is a valid, nul-terminated path and `mkfifo` does not
// retain the pointer after the call.
let rc = unsafe { libc::mkfifo(c_path.as_ptr(), 0o644) };
assert_eq!(rc, 0);
let cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await;
let res = get_user_instructions(&cfg).await;
assert_eq!(res, None);
let discovery = discover_project_doc_paths(&cfg)
.await
.expect("discover paths");
assert_eq!(discovery, Vec::<AbsolutePathBuf>::new());
}
#[tokio::test]
async fn override_directory_falls_back_to_agents_md_file() {
let tmp = tempfile::tempdir().expect("tempdir");
fs::create_dir(tmp.path().join(LOCAL_PROJECT_DOC_FILENAME)).unwrap();
fs::write(tmp.path().join(DEFAULT_PROJECT_DOC_FILENAME), "primary").unwrap();
let cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await;
let res = get_user_instructions(&cfg)
.await
.expect("AGENTS.md should be used when override is a directory");
assert_eq!(res, "primary");
let discovery = discover_project_doc_paths(&cfg)
.await
.expect("discover paths");
assert_eq!(discovery.len(), 1);
assert_eq!(
discovery[0]
.file_name()
.expect("file name")
.to_string_lossy(),
DEFAULT_PROJECT_DOC_FILENAME
);
}
#[tokio::test]
async fn skills_are_not_appended_to_project_doc() {
let tmp = tempfile::tempdir().expect("tempdir");

View File

@@ -379,12 +379,17 @@ pub(crate) async fn handle_non_tool_response_item(
.parent()
.unwrap_or(turn_context.config.codex_home.as_path());
let message: ResponseItem = DeveloperInstructions::new(format!(
"Generated images are saved to {} as {} by default.\nIf you need to use a generated image at another path, copy it and leave the original in place unless the user explicitly asks you to delete it.",
"Generated images are saved to {} as {} by default.",
image_output_dir.display(),
image_output_path.display(),
))
.into();
sess.record_conversation_items(turn_context, &[message])
let copy_message: ResponseItem = DeveloperInstructions::new(
"If you need to use a generated image at another path, copy it and leave the original in place unless the user explicitly asks you to delete it."
.to_string(),
)
.into();
sess.record_conversation_items(turn_context, &[message, copy_message])
.await;
}
Err(err) => {

View File

@@ -1,4 +1,3 @@
use std::future::Future;
use std::mem::swap;
use std::path::Path;
use std::path::PathBuf;
@@ -35,12 +34,12 @@ use codex_protocol::protocol::SessionConfiguredEvent;
use codex_protocol::protocol::SessionSource;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use futures::future::BoxFuture;
use serde_json::Value;
use tempfile::TempDir;
use wiremock::MockServer;
use crate::PathBufExt;
use crate::PathExt;
use crate::RemoteEnvConfig;
use crate::TempDirExt;
use crate::get_remote_test_env;
@@ -49,15 +48,13 @@ use crate::responses::WebSocketTestServer;
use crate::responses::output_value_to_text;
use crate::responses::start_mock_server;
use crate::streaming_sse::StreamingSseServer;
use crate::wait_for_event;
use crate::wait_for_event_match;
use crate::wait_for_event_with_timeout;
use wiremock::Match;
use wiremock::matchers::path_regex;
type ConfigMutator = dyn FnOnce(&mut Config) + Send;
type PreBuildHook = dyn FnOnce(&Path) + Send + 'static;
type WorkspaceSetup = dyn FnOnce(AbsolutePathBuf, Arc<dyn ExecutorFileSystem>) -> BoxFuture<'static, Result<()>>
+ Send;
const TEST_MODEL_WITH_EXPERIMENTAL_TOOLS: &str = "test-gpt-5.1-codex";
const REMOTE_EXEC_SERVER_START_TIMEOUT: Duration = Duration::from_secs(5);
const REMOTE_EXEC_SERVER_POLL_INTERVAL: Duration = Duration::from_millis(25);
@@ -100,28 +97,24 @@ impl RemoteExecServerProcess {
#[derive(Debug)]
pub struct TestEnv {
environment: codex_exec_server::Environment,
cwd: AbsolutePathBuf,
local_cwd_temp_dir: Option<Arc<TempDir>>,
cwd: PathBuf,
_local_cwd_temp_dir: Option<TempDir>,
_remote_exec_server_process: Option<RemoteExecServerProcess>,
}
impl TestEnv {
pub async fn local() -> Result<Self> {
let local_cwd_temp_dir = Arc::new(TempDir::new()?);
let cwd = local_cwd_temp_dir.abs();
let local_cwd_temp_dir = TempDir::new()?;
let cwd = local_cwd_temp_dir.path().to_path_buf();
let environment = codex_exec_server::Environment::create(/*exec_server_url*/ None).await?;
Ok(Self {
environment,
cwd,
local_cwd_temp_dir: Some(local_cwd_temp_dir),
_local_cwd_temp_dir: Some(local_cwd_temp_dir),
_remote_exec_server_process: None,
})
}
pub fn cwd(&self) -> &AbsolutePathBuf {
&self.cwd
}
pub fn environment(&self) -> &codex_exec_server::Environment {
&self.environment
}
@@ -129,10 +122,6 @@ impl TestEnv {
pub fn exec_server_url(&self) -> Option<&str> {
self.environment.exec_server_url()
}
fn local_cwd_temp_dir(&self) -> Option<Arc<TempDir>> {
self.local_cwd_temp_dir.clone()
}
}
pub async fn test_env() -> Result<TestEnv> {
@@ -145,13 +134,16 @@ pub async fn test_env() -> Result<TestEnv> {
let cwd = remote_aware_cwd_path();
environment
.get_filesystem()
.create_directory(&cwd, CreateDirectoryOptions { recursive: true })
.create_directory(
&absolute_path(&cwd)?,
CreateDirectoryOptions { recursive: true },
)
.await?;
remote_process.process.register_cleanup_path(cwd.as_path());
remote_process.process.register_cleanup_path(&cwd);
Ok(TestEnv {
environment,
cwd,
local_cwd_temp_dir: None,
_local_cwd_temp_dir: None,
_remote_exec_server_process: Some(remote_process.process),
})
}
@@ -209,12 +201,11 @@ echo $!"
})
}
fn remote_aware_cwd_path() -> AbsolutePathBuf {
fn remote_aware_cwd_path() -> PathBuf {
PathBuf::from(format!(
"/tmp/codex-core-test-cwd-{}",
remote_exec_server_instance_id()
))
.abs()
}
fn wait_for_remote_listen_url(container_name: &str, stdout_path: &str) -> Result<String> {
@@ -308,6 +299,10 @@ fn docker_command_capture_stdout<const N: usize>(args: [&str; N]) -> Result<Stri
String::from_utf8(output.stdout).context("docker stdout must be utf-8")
}
fn absolute_path(path: &Path) -> Result<AbsolutePathBuf> {
Ok(path.abs())
}
/// A collection of different ways the model can output an apply_patch call
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ApplyPatchModelOutput {
@@ -331,7 +326,6 @@ pub struct TestCodexBuilder {
config_mutators: Vec<Box<ConfigMutator>>,
auth: CodexAuth,
pre_build_hooks: Vec<Box<PreBuildHook>>,
workspace_setups: Vec<Box<WorkspaceSetup>>,
home: Option<Arc<TempDir>>,
user_shell_override: Option<Shell>,
}
@@ -365,16 +359,6 @@ impl TestCodexBuilder {
self
}
pub fn with_workspace_setup<F, Fut>(mut self, setup: F) -> Self
where
F: FnOnce(AbsolutePathBuf, Arc<dyn ExecutorFileSystem>) -> Fut + Send + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
self.workspace_setups
.push(Box::new(move |cwd, fs| Box::pin(setup(cwd, fs))));
self
}
pub fn with_home(mut self, home: Arc<TempDir>) -> Self {
self.home = Some(home);
self
@@ -398,24 +382,25 @@ impl TestCodexBuilder {
Some(home) => home,
None => Arc::new(TempDir::new()?),
};
let base_url = format!("{}/v1", server.uri());
let test_env = TestEnv::local().await?;
Box::pin(self.build_with_home_and_base_url(base_url, home, /*resume_from*/ None, test_env))
.await
Box::pin(self.build_with_home(server, home, /*resume_from*/ None)).await
}
pub async fn build_remote_aware(
&mut self,
server: &wiremock::MockServer,
) -> anyhow::Result<TestCodex> {
let test_env = test_env().await?;
let home = match self.home.clone() {
Some(home) => home,
None => Arc::new(TempDir::new()?),
};
let base_url = format!("{}/v1", server.uri());
let test_env = test_env().await?;
Box::pin(self.build_with_home_and_base_url(base_url, home, /*resume_from*/ None, test_env))
.await
let cwd = test_env.cwd.to_path_buf();
self.config_mutators.push(Box::new(move |config| {
config.cwd = cwd.abs();
}));
let (config, cwd) = self.prepare_config(base_url, &home).await?;
Box::pin(self.build_from_config(config, cwd, home, /*resume_from*/ None, test_env)).await
}
pub async fn build_with_streaming_server(
@@ -427,12 +412,10 @@ impl TestCodexBuilder {
Some(home) => home,
None => Arc::new(TempDir::new()?),
};
let test_env = TestEnv::local().await?;
Box::pin(self.build_with_home_and_base_url(
format!("{base_url}/v1"),
home,
/*resume_from*/ None,
test_env,
))
.await
}
@@ -452,9 +435,7 @@ impl TestCodexBuilder {
config.model_provider.supports_websockets = true;
config.experimental_realtime_ws_model = Some("realtime-test-model".to_string());
}));
let test_env = TestEnv::local().await?;
Box::pin(self.build_with_home_and_base_url(base_url, home, /*resume_from*/ None, test_env))
.await
Box::pin(self.build_with_home_and_base_url(base_url, home, /*resume_from*/ None)).await
}
pub async fn resume(
@@ -462,10 +443,19 @@ impl TestCodexBuilder {
server: &wiremock::MockServer,
home: Arc<TempDir>,
rollout_path: PathBuf,
) -> anyhow::Result<TestCodex> {
Box::pin(self.build_with_home(server, home, Some(rollout_path))).await
}
async fn build_with_home(
&mut self,
server: &wiremock::MockServer,
home: Arc<TempDir>,
resume_from: Option<PathBuf>,
) -> anyhow::Result<TestCodex> {
let base_url = format!("{}/v1", server.uri());
let test_env = TestEnv::local().await?;
Box::pin(self.build_with_home_and_base_url(base_url, home, Some(rollout_path), test_env))
let (config, cwd) = self.prepare_config(base_url, &home).await?;
Box::pin(self.build_from_config(config, cwd, home, resume_from, TestEnv::local().await?))
.await
}
@@ -474,30 +464,10 @@ impl TestCodexBuilder {
base_url: String,
home: Arc<TempDir>,
resume_from: Option<PathBuf>,
test_env: TestEnv,
) -> anyhow::Result<TestCodex> {
let (config, fallback_cwd) = self
.prepare_config(base_url, &home, test_env.cwd().clone())
.await?;
let environment_manager = Arc::new(codex_exec_server::EnvironmentManager::new(
test_env.exec_server_url().map(str::to_owned),
));
let file_system = test_env.environment().get_filesystem();
let mut workspace_setups = vec![];
swap(&mut self.workspace_setups, &mut workspace_setups);
for setup in workspace_setups {
setup(config.cwd.clone(), Arc::clone(&file_system)).await?;
}
let cwd = test_env.local_cwd_temp_dir().unwrap_or(fallback_cwd);
Box::pin(self.build_from_config(
config,
cwd,
home,
resume_from,
test_env,
environment_manager,
))
.await
let (config, cwd) = self.prepare_config(base_url, &home).await?;
Box::pin(self.build_from_config(config, cwd, home, resume_from, TestEnv::local().await?))
.await
}
async fn build_from_config(
@@ -507,9 +477,11 @@ impl TestCodexBuilder {
home: Arc<TempDir>,
resume_from: Option<PathBuf>,
test_env: TestEnv,
environment_manager: Arc<codex_exec_server::EnvironmentManager>,
) -> anyhow::Result<TestCodex> {
let auth = self.auth.clone();
let environment_manager = Arc::new(codex_exec_server::EnvironmentManager::new(
test_env.exec_server_url().map(str::to_owned),
));
let thread_manager = if config.model_catalog.is_some() {
ThreadManager::new(
&config,
@@ -581,7 +553,6 @@ impl TestCodexBuilder {
&mut self,
base_url: String,
home: &TempDir,
cwd_override: AbsolutePathBuf,
) -> anyhow::Result<(Config, Arc<TempDir>)> {
let model_provider = ModelProviderInfo {
base_url: Some(base_url),
@@ -592,7 +563,7 @@ impl TestCodexBuilder {
};
let cwd = Arc::new(TempDir::new()?);
let mut config = load_default_config_for_test(home).await;
config.cwd = cwd_override;
config.cwd = cwd.abs();
config.model_provider = model_provider;
for hook in self.pre_build_hooks.drain(..) {
hook(home.path());
@@ -767,14 +738,10 @@ impl TestCodex {
_ => None,
})
.await;
wait_for_event_with_timeout(
&self.codex,
|event| match event {
EventMsg::TurnComplete(event) => event.turn_id == turn_id,
_ => false,
},
Duration::from_secs(60),
)
wait_for_event(&self.codex, |event| match event {
EventMsg::TurnComplete(event) => event.turn_id == turn_id,
_ => false,
})
.await;
Ok(())
}
@@ -930,7 +897,6 @@ pub fn test_codex() -> TestCodexBuilder {
config_mutators: vec![],
auth: CodexAuth::from_api_key("dummy"),
pre_build_hooks: vec![],
workspace_setups: vec![],
home: None,
user_shell_override: None,
}

View File

@@ -17,7 +17,6 @@ impl TestCodexExecBuilder {
);
cmd.current_dir(self.cwd.path())
.env("CODEX_HOME", self.home.path())
.env("CODEX_SQLITE_HOME", self.home.path())
.env(CODEX_API_KEY_ENV_VAR, "dummy");
cmd
}

View File

@@ -1,130 +0,0 @@
use anyhow::Result;
use codex_exec_server::CreateDirectoryOptions;
use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_once;
use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server;
use core_test_support::test_codex::TestCodexBuilder;
use core_test_support::test_codex::test_codex;
async fn agents_instructions(mut builder: TestCodexBuilder) -> Result<String> {
let server = start_mock_server().await;
let resp_mock = mount_sse_once(
&server,
sse(vec![ev_response_created("resp1"), ev_completed("resp1")]),
)
.await;
let test = builder.build_remote_aware(&server).await?;
test.submit_turn("hello").await?;
let request = resp_mock.single_request();
request
.message_input_texts("user")
.into_iter()
.find(|text| text.starts_with("# AGENTS.md instructions for "))
.ok_or_else(|| anyhow::anyhow!("instructions message not found"))
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn agents_override_is_preferred_over_agents_md() -> Result<()> {
let instructions =
agents_instructions(test_codex().with_workspace_setup(|cwd, fs| async move {
let agents_md = cwd.join("AGENTS.md").expect("absolute AGENTS.md path");
let override_md = cwd
.join("AGENTS.override.md")
.expect("absolute AGENTS.override.md path");
fs.write_file(&agents_md, b"base doc".to_vec()).await?;
fs.write_file(&override_md, b"override doc".to_vec())
.await?;
Ok::<(), anyhow::Error>(())
}))
.await?;
assert!(
instructions.contains("override doc"),
"expected AGENTS.override.md contents: {instructions}"
);
assert!(
!instructions.contains("base doc"),
"expected AGENTS.md to be ignored when override exists: {instructions}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn configured_fallback_is_used_when_agents_candidate_is_directory() -> Result<()> {
let instructions = agents_instructions(
test_codex()
.with_config(|config| {
config.project_doc_fallback_filenames = vec!["WORKFLOW.md".to_string()];
})
.with_workspace_setup(|cwd, fs| async move {
let agents_dir = cwd.join("AGENTS.md").expect("absolute AGENTS.md path");
let fallback = cwd.join("WORKFLOW.md").expect("absolute WORKFLOW.md path");
fs.create_directory(&agents_dir, CreateDirectoryOptions { recursive: true })
.await?;
fs.write_file(&fallback, b"fallback doc".to_vec()).await?;
Ok::<(), anyhow::Error>(())
}),
)
.await?;
assert!(
instructions.contains("fallback doc"),
"expected fallback doc contents: {instructions}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn agents_docs_are_concatenated_from_project_root_to_cwd() -> Result<()> {
let instructions = agents_instructions(
test_codex()
.with_config(|config| {
config.cwd = config
.cwd
.join("nested/workspace")
.expect("absolute nested workspace path");
})
.with_workspace_setup(|cwd, fs| async move {
let nested = cwd.clone();
let root = nested
.parent()
.and_then(|parent| parent.parent())
.expect("nested workspace should have a project root ancestor");
let root_agents = root
.join("AGENTS.md")
.expect("absolute root AGENTS.md path");
let git_marker = root.join(".git").expect("absolute .git path");
let nested_agents = nested
.join("AGENTS.md")
.expect("absolute nested AGENTS.md path");
fs.create_directory(&nested, CreateDirectoryOptions { recursive: true })
.await?;
fs.write_file(&root_agents, b"root doc".to_vec()).await?;
fs.write_file(&git_marker, b"gitdir: /tmp/mock-git-dir\n".to_vec())
.await?;
fs.write_file(&nested_agents, b"child doc".to_vec()).await?;
Ok::<(), anyhow::Error>(())
}),
)
.await?;
let root_pos = instructions
.find("root doc")
.expect("expected root doc in AGENTS instructions");
let child_pos = instructions
.find("child doc")
.expect("expected child doc in AGENTS instructions");
assert!(
root_pos < child_pos,
"expected root doc before child doc: {instructions}"
);
Ok(())
}

View File

@@ -18,22 +18,21 @@ async fn hierarchical_agents_appends_to_project_doc_in_user_instructions() {
)
.await;
let mut builder = test_codex()
.with_config(|config| {
let mut builder = test_codex().with_config(|config| {
config
.features
.enable(Feature::ChildAgentsMd)
.expect("test config should allow feature update");
std::fs::write(
config
.features
.enable(Feature::ChildAgentsMd)
.expect("test config should allow feature update");
})
.with_workspace_setup(|cwd, fs| async move {
let agents_md = cwd.join("AGENTS.md").expect("absolute AGENTS.md path");
fs.write_file(&agents_md, b"be nice".to_vec()).await?;
Ok::<(), anyhow::Error>(())
});
let test = builder
.build_remote_aware(&server)
.await
.expect("build test codex");
.cwd
.join("AGENTS.md")
.expect("absolute AGENTS.md path"),
"be nice",
)
.expect("write AGENTS.md");
});
let test = builder.build(&server).await.expect("build test codex");
test.submit_turn("hello").await.expect("submit turn");
@@ -74,10 +73,7 @@ async fn hierarchical_agents_emits_when_no_project_doc() {
.enable(Feature::ChildAgentsMd)
.expect("test config should allow feature update");
});
let test = builder
.build_remote_aware(&server)
.await
.expect("build test codex");
let test = builder.build(&server).await.expect("build test codex");
test.submit_turn("hello").await.expect("submit turn");

View File

@@ -74,7 +74,6 @@ pub static CODEX_ALIASES_TEMP_DIR: Option<TestCodexAliasesGuard> = {
mod abort_tasks;
mod agent_jobs;
mod agent_websocket;
mod agents_md;
mod apply_patch_cli;
#[cfg(not(target_os = "windows"))]
mod approvals;

View File

@@ -71,6 +71,7 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R
agent_role: None,
model_provider: None,
base_instructions: None,
developer_instructions: None,
dynamic_tools: None,
memory_mode: None,
},
@@ -116,6 +117,7 @@ async fn write_rollout_with_meta_only(dir: &Path, thread_id: ThreadId) -> io::Re
agent_role: None,
model_provider: None,
base_instructions: None,
developer_instructions: None,
dynamic_tools: None,
memory_mode: None,
},

View File

@@ -1,6 +1,6 @@
use anyhow::Result;
use codex_exec_server::RemoveOptions;
use core_test_support::PathBufExt;
use codex_utils_absolute_path::AbsolutePathBuf;
use core_test_support::get_remote_test_env;
use core_test_support::test_codex::test_env;
use pretty_assertions::assert_eq;
@@ -17,7 +17,8 @@ async fn remote_test_env_can_connect_and_use_filesystem() -> Result<()> {
let test_env = test_env().await?;
let file_system = test_env.environment().get_filesystem();
let file_path_abs = remote_test_file_path().abs();
let file_path = remote_test_file_path();
let file_path_abs = absolute_path(file_path.clone())?;
let payload = b"remote-test-env-ok".to_vec();
file_system
@@ -38,6 +39,12 @@ async fn remote_test_env_can_connect_and_use_filesystem() -> Result<()> {
Ok(())
}
fn absolute_path(path: PathBuf) -> Result<AbsolutePathBuf> {
AbsolutePathBuf::try_from(path.clone())
.map_err(|err| anyhow::anyhow!("invalid absolute path {}: {err}", path.display()))
}
fn remote_test_file_path() -> PathBuf {
let nanos = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(duration) => duration.as_nanos(),

View File

@@ -174,6 +174,7 @@ async fn find_locates_rollout_file_written_by_recorder() -> std::io::Result<()>
/*forked_from_id*/ None,
SessionSource::Exec,
Some(BaseInstructions::default()),
/*developer_instructions*/ None,
Vec::new(),
EventPersistenceMode::Limited,
),

View File

@@ -146,6 +146,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> {
agent_role: None,
model_provider: None,
base_instructions: None,
developer_instructions: None,
dynamic_tools: Some(dynamic_tools_for_hook),
memory_mode: None,
},

View File

@@ -41,7 +41,6 @@ pub use file_system::FileMetadata;
pub use file_system::FileSystemResult;
pub use file_system::ReadDirectoryEntry;
pub use file_system::RemoveOptions;
pub use local_file_system::LOCAL_FS;
pub use process::ExecBackend;
pub use process::ExecProcess;
pub use process::StartedExecProcess;

View File

@@ -3,8 +3,6 @@ use codex_utils_absolute_path::AbsolutePathBuf;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::LazyLock;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use tokio::io;
@@ -19,9 +17,6 @@ use crate::RemoveOptions;
const MAX_READ_FILE_BYTES: u64 = 512 * 1024 * 1024;
pub static LOCAL_FS: LazyLock<Arc<dyn ExecutorFileSystem>> =
LazyLock::new(|| -> Arc<dyn ExecutorFileSystem> { Arc::new(LocalFileSystem) });
#[derive(Clone, Default)]
pub(crate) struct LocalFileSystem;

View File

@@ -23,7 +23,6 @@ use crate::ReadDirectoryEntry;
use crate::RemoveOptions;
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
const NOT_FOUND_ERROR_CODE: i64 = -32004;
#[derive(Clone)]
pub(crate) struct RemoteFileSystem {
@@ -152,9 +151,6 @@ impl ExecutorFileSystem for RemoteFileSystem {
fn map_remote_error(error: ExecServerError) -> io::Error {
match error {
ExecServerError::Server { code, message } if code == NOT_FOUND_ERROR_CODE => {
io::Error::new(io::ErrorKind::NotFound, message)
}
ExecServerError::Server { code, message } if code == INVALID_REQUEST_ERROR_CODE => {
io::Error::new(io::ErrorKind::InvalidInput, message)
}

View File

@@ -356,14 +356,6 @@ pub(crate) fn invalid_params(message: String) -> JSONRPCErrorError {
}
}
pub(crate) fn not_found(message: String) -> JSONRPCErrorError {
JSONRPCErrorError {
code: -32004,
data: None,
message,
}
}
pub(crate) fn internal_error(message: String) -> JSONRPCErrorError {
JSONRPCErrorError {
code: -32603,

View File

@@ -26,7 +26,6 @@ use crate::RemoveOptions;
use crate::local_file_system::LocalFileSystem;
use crate::rpc::internal_error;
use crate::rpc::invalid_request;
use crate::rpc::not_found;
#[derive(Clone, Default)]
pub(crate) struct FileSystemHandler {
@@ -154,9 +153,7 @@ impl FileSystemHandler {
}
fn map_fs_error(err: io::Error) -> JSONRPCErrorError {
if err.kind() == io::ErrorKind::NotFound {
not_found(err.to_string())
} else if err.kind() == io::ErrorKind::InvalidInput {
if err.kind() == io::ErrorKind::InvalidInput {
invalid_request(err.to_string())
} else {
internal_error(err.to_string())

View File

@@ -2351,6 +2351,26 @@ impl InitialHistory {
}
}
pub fn get_developer_instructions(&self) -> Option<Option<String>> {
match self {
InitialHistory::New => None,
InitialHistory::Resumed(resumed) => {
resumed.history.iter().find_map(|item| match item {
RolloutItem::SessionMeta(meta_line) => {
meta_line.meta.developer_instructions.clone()
}
_ => None,
})
}
InitialHistory::Forked(items) => items.iter().find_map(|item| match item {
RolloutItem::SessionMeta(meta_line) => {
meta_line.meta.developer_instructions.clone()
}
_ => None,
}),
}
}
pub fn get_dynamic_tools(&self) -> Option<Vec<DynamicToolSpec>> {
match self {
InitialHistory::New => None,
@@ -2549,6 +2569,13 @@ pub struct SessionMeta {
skip_serializing_if = "Option::is_none"
)]
pub base_instructions: Option<Option<BaseInstructions>>,
#[serde(
default,
deserialize_with = "deserialize_double_option",
serialize_with = "serialize_double_option",
skip_serializing_if = "Option::is_none"
)]
pub developer_instructions: Option<Option<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -2570,6 +2597,7 @@ impl Default for SessionMeta {
agent_path: None,
model_provider: None,
base_instructions: None,
developer_instructions: None,
dynamic_tools: None,
memory_mode: None,
}

View File

@@ -56,6 +56,7 @@ async fn extract_metadata_from_rollout_uses_session_meta() {
agent_role: None,
model_provider: Some("openai".to_string()),
base_instructions: None,
developer_instructions: None,
dynamic_tools: None,
memory_mode: None,
};
@@ -107,6 +108,7 @@ async fn extract_metadata_from_rollout_returns_latest_memory_mode() {
agent_role: None,
model_provider: Some("openai".to_string()),
base_instructions: None,
developer_instructions: None,
dynamic_tools: None,
memory_mode: None,
};
@@ -369,6 +371,7 @@ fn write_rollout_in_sessions_with_cwd(
agent_role: None,
model_provider: Some("test-provider".to_string()),
base_instructions: None,
developer_instructions: None,
dynamic_tools: None,
memory_mode: None,
};

View File

@@ -82,6 +82,7 @@ pub enum RolloutRecorderParams {
forked_from_id: Option<ThreadId>,
source: SessionSource,
base_instructions: Option<BaseInstructions>,
developer_instructions: Option<Option<String>>,
dynamic_tools: Vec<DynamicToolSpec>,
event_persistence_mode: EventPersistenceMode,
},
@@ -111,6 +112,7 @@ impl RolloutRecorderParams {
forked_from_id: Option<ThreadId>,
source: SessionSource,
base_instructions: Option<BaseInstructions>,
developer_instructions: Option<Option<String>>,
dynamic_tools: Vec<DynamicToolSpec>,
event_persistence_mode: EventPersistenceMode,
) -> Self {
@@ -119,6 +121,7 @@ impl RolloutRecorderParams {
forked_from_id,
source,
base_instructions,
developer_instructions,
dynamic_tools,
event_persistence_mode,
}
@@ -380,6 +383,7 @@ impl RolloutRecorder {
forked_from_id,
source,
base_instructions,
developer_instructions,
dynamic_tools,
event_persistence_mode,
} => {
@@ -409,6 +413,7 @@ impl RolloutRecorder {
source,
model_provider: Some(config.model_provider_id().to_string()),
base_instructions: Some(base_instructions),
developer_instructions,
dynamic_tools: if dynamic_tools.is_empty() {
None
} else {

View File

@@ -74,6 +74,7 @@ async fn recorder_materializes_only_after_explicit_persist() -> std::io::Result<
/*forked_from_id*/ None,
SessionSource::Exec,
Some(BaseInstructions::default()),
/*developer_instructions*/ None,
Vec::new(),
EventPersistenceMode::Limited,
),
@@ -167,6 +168,7 @@ async fn metadata_irrelevant_events_touch_state_db_updated_at() -> std::io::Resu
/*forked_from_id*/ None,
SessionSource::Cli,
Some(BaseInstructions::default()),
/*developer_instructions*/ None,
Vec::new(),
EventPersistenceMode::Limited,
),

View File

@@ -1140,6 +1140,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> {
agent_role: None,
model_provider: Some("test-provider".into()),
base_instructions: None,
developer_instructions: None,
dynamic_tools: None,
memory_mode: None,
},

View File

@@ -257,6 +257,7 @@ mod tests {
agent_role: None,
model_provider: Some("openai".to_string()),
base_instructions: None,
developer_instructions: None,
dynamic_tools: None,
memory_mode: None,
},
@@ -384,6 +385,7 @@ mod tests {
agent_role: None,
model_provider: Some("openai".to_string()),
base_instructions: None,
developer_instructions: None,
dynamic_tools: None,
memory_mode: None,
},

View File

@@ -1030,6 +1030,7 @@ mod tests {
agent_role: None,
model_provider: None,
base_instructions: None,
developer_instructions: None,
dynamic_tools: None,
memory_mode: Some("polluted".to_string()),
},
@@ -1088,6 +1089,7 @@ mod tests {
agent_role: None,
model_provider: None,
base_instructions: None,
developer_instructions: None,
dynamic_tools: None,
memory_mode: None,
},

View File

@@ -33,7 +33,6 @@ codex-chatgpt = { workspace = true }
codex-cloud-requirements = { workspace = true }
codex-config = { workspace = true }
codex-core = { workspace = true }
codex-exec-server = { workspace = true }
codex-features = { workspace = true }
codex-feedback = { workspace = true }
codex-file-search = { workspace = true }

View File

@@ -7303,8 +7303,6 @@ impl ChatWidget {
.values()
.cloned()
.collect();
let config = self.config.clone();
let frame_requester = self.frame_requester.clone();
let (cell, handle) = crate::status::new_status_output_with_rate_limits_handle(
&self.config,
self.status_account_display.as_ref(),
@@ -7319,21 +7317,8 @@ impl ChatWidget {
self.model_display_name(),
collaboration_mode,
reasoning_effort_override,
"<none>".to_string(),
refreshing_rate_limits,
);
let agents_summary_handle = handle.clone();
tokio::spawn(async move {
let agents_summary = match crate::status::discover_agents_summary(&config).await {
Ok(summary) => summary,
Err(err) => {
tracing::warn!(error = %err, "failed to discover project docs for /status");
"<none>".to_string()
}
};
agents_summary_handle.finish_agents_summary_discovery(agents_summary);
frame_requester.schedule_frame();
});
if let Some(request_id) = request_id {
self.refreshing_status_outputs.push((request_id, handle));
}

View File

@@ -28,6 +28,7 @@ use super::format::line_display_width;
use super::format::push_label;
use super::format::truncate_line_to_width;
use super::helpers::compose_account_display;
use super::helpers::compose_agents_summary;
use super::helpers::compose_model_display;
use super::helpers::format_directory_display;
use super::helpers::format_tokens_compact;
@@ -67,20 +68,10 @@ struct StatusRateLimitState {
#[derive(Debug, Clone)]
pub(crate) struct StatusHistoryHandle {
agents_summary: Arc<RwLock<String>>,
rate_limit_state: Arc<RwLock<StatusRateLimitState>>,
}
impl StatusHistoryHandle {
pub(crate) fn finish_agents_summary_discovery(&self, agents_summary: String) {
#[expect(clippy::expect_used)]
let mut current = self
.agents_summary
.write()
.expect("status history agents summary state poisoned");
*current = agents_summary;
}
pub(crate) fn finish_rate_limit_refresh(
&self,
rate_limits: &[RateLimitSnapshotDisplay],
@@ -107,7 +98,7 @@ struct StatusHistoryCell {
model_details: Vec<String>,
directory: PathBuf,
permissions: String,
agents_summary: Arc<RwLock<String>>,
agents_summary: String,
collaboration_mode: Option<String>,
model_provider: Option<String>,
account: Option<StatusAccountDisplay>,
@@ -186,7 +177,6 @@ pub(crate) fn new_status_output_with_rate_limits(
model_name,
collaboration_mode,
reasoning_effort_override,
"<none>".to_string(),
refreshing_rate_limits,
)
.0
@@ -207,7 +197,6 @@ pub(crate) fn new_status_output_with_rate_limits_handle(
model_name: &str,
collaboration_mode: Option<&str>,
reasoning_effort_override: Option<Option<ReasoningEffort>>,
agents_summary: String,
refreshing_rate_limits: bool,
) -> (CompositeHistoryCell, StatusHistoryHandle) {
let command = PlainHistoryCell::new(vec!["/status".magenta().into()]);
@@ -225,7 +214,6 @@ pub(crate) fn new_status_output_with_rate_limits_handle(
model_name,
collaboration_mode,
reasoning_effort_override,
agents_summary,
refreshing_rate_limits,
);
@@ -251,7 +239,6 @@ impl StatusHistoryCell {
model_name: &str,
collaboration_mode: Option<&str>,
reasoning_effort_override: Option<Option<ReasoningEffort>>,
agents_summary: String,
refreshing_rate_limits: bool,
) -> (Self, StatusHistoryHandle) {
let mut config_entries = vec![
@@ -315,6 +302,7 @@ impl StatusHistoryCell {
} else {
format!("Custom ({sandbox}, {approval})")
};
let agents_summary = compose_agents_summary(config);
let model_provider = format_model_provider(config);
let account = compose_account_display(account_display);
let session_id = session_id.as_ref().map(std::string::ToString::to_string);
@@ -345,7 +333,6 @@ impl StatusHistoryCell {
rate_limits,
refreshing_rate_limits,
}));
let agents_summary = Arc::new(RwLock::new(agents_summary));
(
Self {
@@ -353,6 +340,7 @@ impl StatusHistoryCell {
model_details,
directory: config.cwd.to_path_buf(),
permissions,
agents_summary,
collaboration_mode: collaboration_mode.map(ToString::to_string),
model_provider,
account,
@@ -360,13 +348,9 @@ impl StatusHistoryCell {
session_id,
forked_from,
token_usage,
agents_summary: agents_summary.clone(),
rate_limit_state: rate_limit_state.clone(),
},
StatusHistoryHandle {
agents_summary,
rate_limit_state,
},
StatusHistoryHandle { rate_limit_state },
)
}
@@ -580,12 +564,6 @@ impl HistoryCell for StatusHistoryCell {
.rate_limit_state
.read()
.expect("status history rate-limit state poisoned");
#[expect(clippy::expect_used)]
let agents_summary = self
.agents_summary
.read()
.expect("status history agents summary state poisoned")
.clone();
if self.model_provider.is_some() {
push_label(&mut labels, &mut seen, "Model provider");
@@ -647,7 +625,7 @@ impl HistoryCell for StatusHistoryCell {
}
lines.push(formatter.line("Directory", vec![Span::from(directory_value)]));
lines.push(formatter.line("Permissions", vec![Span::from(self.permissions.clone())]));
lines.push(formatter.line("Agents.md", vec![Span::from(agents_summary)]));
lines.push(formatter.line("Agents.md", vec![Span::from(self.agents_summary.clone())]));
if let Some(account_value) = account_value {
lines.push(formatter.line("Account", vec![Span::from(account_value)]));

View File

@@ -5,10 +5,7 @@ use chrono::DateTime;
use chrono::Local;
use codex_core::config::Config;
use codex_core::project_doc::discover_project_doc_paths;
use codex_exec_server::LOCAL_FS;
use codex_protocol::account::PlanType;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::io;
use std::path::Path;
use unicode_width::UnicodeWidthStr;
@@ -36,52 +33,51 @@ pub(crate) fn compose_model_display(
(model_name.to_string(), details)
}
pub(crate) async fn discover_agents_summary(config: &Config) -> io::Result<String> {
let paths = discover_project_doc_paths(config, LOCAL_FS.as_ref()).await?;
Ok(compose_agents_summary(config, &paths))
}
pub(crate) fn compose_agents_summary(config: &Config, paths: &[AbsolutePathBuf]) -> String {
let mut rels: Vec<String> = Vec::new();
for p in paths {
let file_name = p
.file_name()
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_else(|| "<unknown>".to_string());
let display = if let Some(parent) = p.parent() {
if parent.as_path() == config.cwd.as_path() {
file_name.clone()
} else {
let mut cur = config.cwd.as_path();
let mut ups = 0usize;
let mut reached = false;
while let Some(c) = cur.parent() {
if cur == parent.as_path() {
reached = true;
break;
pub(crate) fn compose_agents_summary(config: &Config) -> String {
match discover_project_doc_paths(config) {
Ok(paths) => {
let mut rels: Vec<String> = Vec::new();
for p in paths {
let file_name = p
.file_name()
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_else(|| "<unknown>".to_string());
let display = if let Some(parent) = p.parent() {
if parent == config.cwd.as_path() {
file_name.clone()
} else {
let mut cur = config.cwd.as_path();
let mut ups = 0usize;
let mut reached = false;
while let Some(c) = cur.parent() {
if cur == parent {
reached = true;
break;
}
cur = c;
ups += 1;
}
if reached {
let up = format!("..{}", std::path::MAIN_SEPARATOR);
format!("{}{}", up.repeat(ups), file_name)
} else if let Ok(stripped) = p.strip_prefix(&config.cwd) {
normalize_agents_display_path(stripped)
} else {
normalize_agents_display_path(&p)
}
}
cur = c;
ups += 1;
}
if reached {
let up = format!("..{}", std::path::MAIN_SEPARATOR);
format!("{}{}", up.repeat(ups), file_name)
} else if let Ok(stripped) = p.strip_prefix(&config.cwd) {
normalize_agents_display_path(stripped)
} else {
normalize_agents_display_path(p)
}
normalize_agents_display_path(&p)
};
rels.push(display);
}
} else {
normalize_agents_display_path(p)
};
rels.push(display);
}
if rels.is_empty() {
"<none>".to_string()
} else {
rels.join(", ")
if rels.is_empty() {
"<none>".to_string()
} else {
rels.join(", ")
}
}
Err(_) => "<none>".to_string(),
}
}

View File

@@ -19,7 +19,6 @@ pub(crate) use card::new_status_output;
#[cfg(test)]
pub(crate) use card::new_status_output_with_rate_limits;
pub(crate) use card::new_status_output_with_rate_limits_handle;
pub(crate) use helpers::discover_agents_summary;
pub(crate) use helpers::format_directory_display;
pub(crate) use helpers::format_tokens_compact;
pub(crate) use helpers::plan_type_display_name;