mirror of
https://github.com/openai/codex.git
synced 2026-03-03 05:03:20 +00:00
Compare commits
4 Commits
fix/notify
...
jif/agent-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc1a527e8a | ||
|
|
963ace01ab | ||
|
|
0558a2f2ae | ||
|
|
0fbebfdf3b |
@@ -207,13 +207,12 @@ tmp_path.replace(payload_path)
|
||||
let notify_script = notify_script
|
||||
.to_str()
|
||||
.expect("notify script path should be valid UTF-8");
|
||||
let notify_command = if cfg!(windows) { "python" } else { "python3" };
|
||||
create_config_toml_with_extra(
|
||||
codex_home.path(),
|
||||
&server.uri(),
|
||||
"never",
|
||||
&format!(
|
||||
"notify = [\"{notify_command}\", {}]",
|
||||
"notify = [\"python3\", {}]",
|
||||
toml_basic_string(notify_script)
|
||||
),
|
||||
)?;
|
||||
@@ -262,12 +261,7 @@ tmp_path.replace(payload_path)
|
||||
)
|
||||
.await??;
|
||||
|
||||
let notify_timeout = if cfg!(windows) {
|
||||
Duration::from_secs(15)
|
||||
} else {
|
||||
Duration::from_secs(5)
|
||||
};
|
||||
fs_wait::wait_for_path_exists(¬ify_file, notify_timeout).await?;
|
||||
fs_wait::wait_for_path_exists(¬ify_file, Duration::from_secs(5)).await?;
|
||||
let payload_raw = tokio::fs::read_to_string(¬ify_file).await?;
|
||||
let payload: Value = serde_json::from_str(&payload_raw)?;
|
||||
assert_eq!(payload["client"], "xcode");
|
||||
|
||||
@@ -616,19 +616,11 @@
|
||||
"additionalProperties": false,
|
||||
"description": "Memories settings loaded from config.toml.",
|
||||
"properties": {
|
||||
"consolidation_model": {
|
||||
"description": "Model used for memory consolidation.",
|
||||
"type": "string"
|
||||
},
|
||||
"extract_model": {
|
||||
"description": "Model used for thread summarisation.",
|
||||
"type": "string"
|
||||
},
|
||||
"generate_memories": {
|
||||
"description": "When `false`, newly created threads are stored with `memory_mode = \"disabled\"` in the state DB.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"max_raw_memories_for_consolidation": {
|
||||
"max_raw_memories_for_global": {
|
||||
"description": "Maximum number of recent raw memories retained for global consolidation.",
|
||||
"format": "uint",
|
||||
"minimum": 0.0,
|
||||
@@ -659,6 +651,14 @@
|
||||
"description": "When `true`, web searches and MCP tool calls mark the thread `memory_mode` as `\"polluted\"`.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"phase_1_model": {
|
||||
"description": "Model used for thread summarisation.",
|
||||
"type": "string"
|
||||
},
|
||||
"phase_2_model": {
|
||||
"description": "Model used for memory consolidation.",
|
||||
"type": "string"
|
||||
},
|
||||
"use_memories": {
|
||||
"description": "When `false`, skip injecting memory usage instructions into developer prompts.",
|
||||
"type": "boolean"
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::find_thread_path_by_id_str;
|
||||
use crate::rollout::RolloutRecorder;
|
||||
use crate::session_prefix::format_subagent_context_line;
|
||||
use crate::session_prefix::format_subagent_notification_message;
|
||||
use crate::shell_snapshot::ShellSnapshot;
|
||||
use crate::state_db;
|
||||
use crate::thread_manager::ThreadManagerState;
|
||||
use codex_protocol::ThreadId;
|
||||
@@ -84,9 +83,6 @@ impl AgentControl {
|
||||
) -> CodexResult<ThreadId> {
|
||||
let state = self.upgrade()?;
|
||||
let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?;
|
||||
let inherited_shell_snapshot = self
|
||||
.inherited_shell_snapshot_for_source(&state, session_source.as_ref())
|
||||
.await;
|
||||
let session_source = match session_source {
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
@@ -165,7 +161,6 @@ impl AgentControl {
|
||||
self.clone(),
|
||||
session_source,
|
||||
false,
|
||||
inherited_shell_snapshot,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
@@ -176,7 +171,6 @@ impl AgentControl {
|
||||
session_source,
|
||||
false,
|
||||
None,
|
||||
inherited_shell_snapshot,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
@@ -241,9 +235,6 @@ impl AgentControl {
|
||||
other => other,
|
||||
};
|
||||
let notification_source = session_source.clone();
|
||||
let inherited_shell_snapshot = self
|
||||
.inherited_shell_snapshot_for_source(&state, Some(&session_source))
|
||||
.await;
|
||||
let rollout_path =
|
||||
find_thread_path_by_id_str(config.codex_home.as_path(), &thread_id.to_string())
|
||||
.await?
|
||||
@@ -255,7 +246,6 @@ impl AgentControl {
|
||||
rollout_path,
|
||||
self.clone(),
|
||||
session_source,
|
||||
inherited_shell_snapshot,
|
||||
)
|
||||
.await?;
|
||||
reservation.commit(resumed_thread.thread_id);
|
||||
@@ -441,22 +431,6 @@ impl AgentControl {
|
||||
.upgrade()
|
||||
.ok_or_else(|| CodexErr::UnsupportedOperation("thread manager dropped".to_string()))
|
||||
}
|
||||
|
||||
async fn inherited_shell_snapshot_for_source(
|
||||
&self,
|
||||
state: &Arc<ThreadManagerState>,
|
||||
session_source: Option<&SessionSource>,
|
||||
) -> Option<Arc<ShellSnapshot>> {
|
||||
let Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id, ..
|
||||
})) = session_source
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let parent_thread = state.get_thread(*parent_thread_id).await.ok()?;
|
||||
parent_thread.codex.session.user_shell().shell_snapshot()
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
//! Applies agent-role configuration layers on top of an existing session config.
|
||||
//!
|
||||
//! Roles are selected at spawn time and are loaded with the same config machinery as
|
||||
//! `config.toml`. This module resolves built-in and user-defined role files, inserts the role as a
|
||||
//! high-precedence layer, and preserves the caller's current profile/provider unless the role
|
||||
//! explicitly takes ownership of model selection. It does not decide when to spawn a sub-agent or
|
||||
//! which role to use; the multi-agent tool handler owns that orchestration.
|
||||
|
||||
use crate::config::AgentRoleConfig;
|
||||
use crate::config::Config;
|
||||
use crate::config::ConfigOverrides;
|
||||
@@ -13,10 +21,18 @@ use std::path::Path;
|
||||
use std::sync::LazyLock;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
/// The role name used when a caller omits `agent_type`.
|
||||
pub const DEFAULT_ROLE_NAME: &str = "default";
|
||||
const AGENT_TYPE_UNAVAILABLE_ERROR: &str = "agent type is currently not available";
|
||||
|
||||
/// Applies a role config layer to a mutable config and preserves unspecified keys.
|
||||
/// Applies a named role layer to `config` while preserving caller-owned model selection.
|
||||
///
|
||||
/// The role layer is inserted at session-flag precedence so it can override persisted config, but
|
||||
/// the caller's current `profile` and `model_provider` remain sticky runtime choices unless the
|
||||
/// role explicitly sets `profile`, explicitly sets `model_provider`, or rewrites the active
|
||||
/// profile's `model_provider` in place. Rebuilding the config without those overrides would make a
|
||||
/// spawned agent silently fall back to the default provider, which is the bug this preservation
|
||||
/// logic avoids.
|
||||
pub(crate) async fn apply_role_to_config(
|
||||
config: &mut Config,
|
||||
role_name: Option<&str>,
|
||||
@@ -60,6 +76,25 @@ pub(crate) async fn apply_role_to_config(
|
||||
.map_err(|_| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?;
|
||||
let role_layer_toml = resolve_relative_paths_in_config_toml(role_config_toml, role_config_base)
|
||||
.map_err(|_| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?;
|
||||
let role_selects_provider = role_layer_toml.get("model_provider").is_some();
|
||||
let role_selects_profile = role_layer_toml.get("profile").is_some();
|
||||
let role_updates_active_profile_provider = config
|
||||
.active_profile
|
||||
.as_ref()
|
||||
.and_then(|active_profile| {
|
||||
role_layer_toml
|
||||
.get("profiles")
|
||||
.and_then(TomlValue::as_table)
|
||||
.and_then(|profiles| profiles.get(active_profile))
|
||||
.and_then(TomlValue::as_table)
|
||||
.map(|profile| profile.contains_key("model_provider"))
|
||||
})
|
||||
.unwrap_or(false);
|
||||
// A role that does not explicitly take ownership of model selection should inherit the
|
||||
// caller's current profile/provider choices across the config reload.
|
||||
let preserve_current_profile = !role_selects_provider && !role_selects_profile;
|
||||
let preserve_current_provider =
|
||||
preserve_current_profile && !role_updates_active_profile_provider;
|
||||
|
||||
let mut layers: Vec<ConfigLayerEntry> = config
|
||||
.config_layer_stack
|
||||
@@ -86,6 +121,10 @@ pub(crate) async fn apply_role_to_config(
|
||||
merged_config,
|
||||
ConfigOverrides {
|
||||
cwd: Some(config.cwd.clone()),
|
||||
model_provider: preserve_current_provider.then(|| config.model_provider_id.clone()),
|
||||
config_profile: preserve_current_profile
|
||||
.then(|| config.active_profile.clone())
|
||||
.flatten(),
|
||||
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
|
||||
main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(),
|
||||
js_repl_node_path: config.js_repl_node_path.clone(),
|
||||
@@ -225,6 +264,7 @@ Rules:
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::CONFIG_TOML_FILE;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::config_loader::ConfigLayerStackOrdering;
|
||||
use crate::plugins::PluginsManager;
|
||||
@@ -382,6 +422,227 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_preserves_active_profile_and_model_provider() {
|
||||
let home = TempDir::new().expect("create temp dir");
|
||||
tokio::fs::write(
|
||||
home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[model_providers.test-provider]
|
||||
name = "Test Provider"
|
||||
base_url = "https://example.com/v1"
|
||||
env_key = "TEST_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[profiles.test-profile]
|
||||
model_provider = "test-provider"
|
||||
"#,
|
||||
)
|
||||
.await
|
||||
.expect("write config.toml");
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
config_profile: Some("test-profile".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.fallback_cwd(Some(home.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("load config");
|
||||
let role_path = write_role_config(&home, "empty-role.toml", "").await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
assert_eq!(config.active_profile.as_deref(), Some("test-profile"));
|
||||
assert_eq!(config.model_provider_id, "test-provider");
|
||||
assert_eq!(config.model_provider.name, "Test Provider");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_uses_role_profile_instead_of_current_profile() {
|
||||
let home = TempDir::new().expect("create temp dir");
|
||||
tokio::fs::write(
|
||||
home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[model_providers.base-provider]
|
||||
name = "Base Provider"
|
||||
base_url = "https://base.example.com/v1"
|
||||
env_key = "BASE_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[model_providers.role-provider]
|
||||
name = "Role Provider"
|
||||
base_url = "https://role.example.com/v1"
|
||||
env_key = "ROLE_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[profiles.base-profile]
|
||||
model_provider = "base-provider"
|
||||
|
||||
[profiles.role-profile]
|
||||
model_provider = "role-provider"
|
||||
"#,
|
||||
)
|
||||
.await
|
||||
.expect("write config.toml");
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
config_profile: Some("base-profile".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.fallback_cwd(Some(home.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("load config");
|
||||
let role_path =
|
||||
write_role_config(&home, "profile-role.toml", "profile = \"role-profile\"").await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
assert_eq!(config.active_profile.as_deref(), Some("role-profile"));
|
||||
assert_eq!(config.model_provider_id, "role-provider");
|
||||
assert_eq!(config.model_provider.name, "Role Provider");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_uses_role_model_provider_instead_of_current_profile_provider() {
|
||||
let home = TempDir::new().expect("create temp dir");
|
||||
tokio::fs::write(
|
||||
home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[model_providers.base-provider]
|
||||
name = "Base Provider"
|
||||
base_url = "https://base.example.com/v1"
|
||||
env_key = "BASE_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[model_providers.role-provider]
|
||||
name = "Role Provider"
|
||||
base_url = "https://role.example.com/v1"
|
||||
env_key = "ROLE_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[profiles.base-profile]
|
||||
model_provider = "base-provider"
|
||||
"#,
|
||||
)
|
||||
.await
|
||||
.expect("write config.toml");
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
config_profile: Some("base-profile".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.fallback_cwd(Some(home.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("load config");
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"provider-role.toml",
|
||||
"model_provider = \"role-provider\"",
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
assert_eq!(config.active_profile, None);
|
||||
assert_eq!(config.model_provider_id, "role-provider");
|
||||
assert_eq!(config.model_provider.name, "Role Provider");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_uses_active_profile_model_provider_update() {
|
||||
let home = TempDir::new().expect("create temp dir");
|
||||
tokio::fs::write(
|
||||
home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[model_providers.base-provider]
|
||||
name = "Base Provider"
|
||||
base_url = "https://base.example.com/v1"
|
||||
env_key = "BASE_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[model_providers.role-provider]
|
||||
name = "Role Provider"
|
||||
base_url = "https://role.example.com/v1"
|
||||
env_key = "ROLE_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[profiles.base-profile]
|
||||
model_provider = "base-provider"
|
||||
model_reasoning_effort = "low"
|
||||
"#,
|
||||
)
|
||||
.await
|
||||
.expect("write config.toml");
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
config_profile: Some("base-profile".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.fallback_cwd(Some(home.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("load config");
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"profile-edit-role.toml",
|
||||
r#"[profiles.base-profile]
|
||||
model_provider = "role-provider"
|
||||
model_reasoning_effort = "high"
|
||||
"#,
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
assert_eq!(config.active_profile.as_deref(), Some("base-profile"));
|
||||
assert_eq!(config.model_provider_id, "role-provider");
|
||||
assert_eq!(config.model_provider.name, "Role Provider");
|
||||
assert_eq!(config.model_reasoning_effort, Some(ReasoningEffort::High));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(not(windows))]
|
||||
async fn apply_role_does_not_materialize_default_sandbox_workspace_write_fields() {
|
||||
|
||||
@@ -345,7 +345,6 @@ impl Codex {
|
||||
dynamic_tools: Vec<DynamicToolSpec>,
|
||||
persist_extended_history: bool,
|
||||
metrics_service_name: Option<String>,
|
||||
inherited_shell_snapshot: Option<Arc<ShellSnapshot>>,
|
||||
) -> CodexResult<CodexSpawnOk> {
|
||||
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||
let (tx_event, rx_event) = async_channel::unbounded();
|
||||
@@ -476,7 +475,6 @@ impl Codex {
|
||||
session_source,
|
||||
dynamic_tools,
|
||||
persist_extended_history,
|
||||
inherited_shell_snapshot,
|
||||
};
|
||||
|
||||
// Generate a unique ID for the lifetime of this Codex session.
|
||||
@@ -867,7 +865,6 @@ pub(crate) struct SessionConfiguration {
|
||||
session_source: SessionSource,
|
||||
dynamic_tools: Vec<DynamicToolSpec>,
|
||||
persist_extended_history: bool,
|
||||
inherited_shell_snapshot: Option<Arc<ShellSnapshot>>,
|
||||
}
|
||||
|
||||
impl SessionConfiguration {
|
||||
@@ -1386,19 +1383,13 @@ impl Session {
|
||||
};
|
||||
// Create the mutable state for the Session.
|
||||
let shell_snapshot_tx = if config.features.enabled(Feature::ShellSnapshot) {
|
||||
if let Some(snapshot) = session_configuration.inherited_shell_snapshot.clone() {
|
||||
let (tx, rx) = watch::channel(Some(snapshot));
|
||||
default_shell.shell_snapshot = rx;
|
||||
tx
|
||||
} else {
|
||||
ShellSnapshot::start_snapshotting(
|
||||
config.codex_home.clone(),
|
||||
conversation_id,
|
||||
session_configuration.cwd.clone(),
|
||||
&mut default_shell,
|
||||
otel_manager.clone(),
|
||||
)
|
||||
}
|
||||
ShellSnapshot::start_snapshotting(
|
||||
config.codex_home.clone(),
|
||||
conversation_id,
|
||||
session_configuration.cwd.clone(),
|
||||
&mut default_shell,
|
||||
otel_manager.clone(),
|
||||
)
|
||||
} else {
|
||||
let (tx, rx) = watch::channel(None);
|
||||
default_shell.shell_snapshot = rx;
|
||||
@@ -1987,7 +1978,6 @@ impl Session {
|
||||
previous_cwd: &Path,
|
||||
next_cwd: &Path,
|
||||
codex_home: &Path,
|
||||
session_source: &SessionSource,
|
||||
) {
|
||||
if previous_cwd == next_cwd {
|
||||
return;
|
||||
@@ -1997,13 +1987,6 @@ impl Session {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
session_source,
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. })
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
ShellSnapshot::refresh_snapshot(
|
||||
codex_home.to_path_buf(),
|
||||
self.conversation_id,
|
||||
@@ -2025,16 +2008,10 @@ impl Session {
|
||||
let previous_cwd = state.session_configuration.cwd.clone();
|
||||
let next_cwd = updated.cwd.clone();
|
||||
let codex_home = updated.codex_home.clone();
|
||||
let session_source = updated.session_source.clone();
|
||||
state.session_configuration = updated;
|
||||
drop(state);
|
||||
|
||||
self.maybe_refresh_shell_snapshot_for_cwd(
|
||||
&previous_cwd,
|
||||
&next_cwd,
|
||||
&codex_home,
|
||||
&session_source,
|
||||
);
|
||||
self.maybe_refresh_shell_snapshot_for_cwd(&previous_cwd, &next_cwd, &codex_home);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2050,13 +2027,7 @@ impl Session {
|
||||
sub_id: String,
|
||||
updates: SessionSettingsUpdate,
|
||||
) -> ConstraintResult<Arc<TurnContext>> {
|
||||
let (
|
||||
session_configuration,
|
||||
sandbox_policy_changed,
|
||||
previous_cwd,
|
||||
codex_home,
|
||||
session_source,
|
||||
) = {
|
||||
let (session_configuration, sandbox_policy_changed, previous_cwd, codex_home) = {
|
||||
let mut state = self.state.lock().await;
|
||||
match state.session_configuration.clone().apply(&updates) {
|
||||
Ok(next) => {
|
||||
@@ -2064,15 +2035,8 @@ impl Session {
|
||||
let sandbox_policy_changed =
|
||||
state.session_configuration.sandbox_policy != next.sandbox_policy;
|
||||
let codex_home = next.codex_home.clone();
|
||||
let session_source = next.session_source.clone();
|
||||
state.session_configuration = next.clone();
|
||||
(
|
||||
next,
|
||||
sandbox_policy_changed,
|
||||
previous_cwd,
|
||||
codex_home,
|
||||
session_source,
|
||||
)
|
||||
(next, sandbox_policy_changed, previous_cwd, codex_home)
|
||||
}
|
||||
Err(err) => {
|
||||
drop(state);
|
||||
@@ -2093,7 +2057,6 @@ impl Session {
|
||||
&previous_cwd,
|
||||
&session_configuration.cwd,
|
||||
&codex_home,
|
||||
&session_source,
|
||||
);
|
||||
|
||||
Ok(self
|
||||
@@ -7704,7 +7667,6 @@ mod tests {
|
||||
session_source: SessionSource::Exec,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
inherited_shell_snapshot: None,
|
||||
};
|
||||
|
||||
let mut state = SessionState::new(session_configuration);
|
||||
@@ -7798,7 +7760,6 @@ mod tests {
|
||||
session_source: SessionSource::Exec,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
inherited_shell_snapshot: None,
|
||||
};
|
||||
|
||||
let mut state = SessionState::new(session_configuration);
|
||||
@@ -8111,7 +8072,6 @@ mod tests {
|
||||
session_source: SessionSource::Exec,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
inherited_shell_snapshot: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8166,7 +8126,6 @@ mod tests {
|
||||
session_source: SessionSource::Exec,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
inherited_shell_snapshot: None,
|
||||
};
|
||||
|
||||
let (tx_event, _rx_event) = async_channel::unbounded();
|
||||
@@ -8257,7 +8216,6 @@ mod tests {
|
||||
session_source: SessionSource::Exec,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
inherited_shell_snapshot: None,
|
||||
};
|
||||
let per_turn_config = Session::build_per_turn_config(&session_configuration);
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests(
|
||||
@@ -8425,7 +8383,6 @@ mod tests {
|
||||
session_source: SessionSource::Exec,
|
||||
dynamic_tools,
|
||||
persist_extended_history: false,
|
||||
inherited_shell_snapshot: None,
|
||||
};
|
||||
let per_turn_config = Session::build_per_turn_config(&session_configuration);
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests(
|
||||
|
||||
@@ -62,7 +62,6 @@ pub(crate) async fn run_codex_thread_interactive(
|
||||
Vec::new(),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let codex = Arc::new(codex);
|
||||
|
||||
@@ -2508,13 +2508,13 @@ persistence = "none"
|
||||
no_memories_if_mcp_or_web_search = true
|
||||
generate_memories = false
|
||||
use_memories = false
|
||||
max_raw_memories_for_consolidation = 512
|
||||
max_raw_memories_for_global = 512
|
||||
max_unused_days = 21
|
||||
max_rollout_age_days = 42
|
||||
max_rollouts_per_startup = 9
|
||||
min_rollout_idle_hours = 24
|
||||
extract_model = "gpt-5-mini"
|
||||
consolidation_model = "gpt-5"
|
||||
phase_1_model = "gpt-5-mini"
|
||||
phase_2_model = "gpt-5"
|
||||
"#;
|
||||
let memories_cfg =
|
||||
toml::from_str::<ConfigToml>(memories).expect("TOML deserialization should succeed");
|
||||
@@ -2523,13 +2523,13 @@ consolidation_model = "gpt-5"
|
||||
no_memories_if_mcp_or_web_search: Some(true),
|
||||
generate_memories: Some(false),
|
||||
use_memories: Some(false),
|
||||
max_raw_memories_for_consolidation: Some(512),
|
||||
max_raw_memories_for_global: Some(512),
|
||||
max_unused_days: Some(21),
|
||||
max_rollout_age_days: Some(42),
|
||||
max_rollouts_per_startup: Some(9),
|
||||
min_rollout_idle_hours: Some(24),
|
||||
extract_model: Some("gpt-5-mini".to_string()),
|
||||
consolidation_model: Some("gpt-5".to_string()),
|
||||
phase_1_model: Some("gpt-5-mini".to_string()),
|
||||
phase_2_model: Some("gpt-5".to_string()),
|
||||
}),
|
||||
memories_cfg.memories
|
||||
);
|
||||
@@ -2546,13 +2546,13 @@ consolidation_model = "gpt-5"
|
||||
no_memories_if_mcp_or_web_search: true,
|
||||
generate_memories: false,
|
||||
use_memories: false,
|
||||
max_raw_memories_for_consolidation: 512,
|
||||
max_raw_memories_for_global: 512,
|
||||
max_unused_days: 21,
|
||||
max_rollout_age_days: 42,
|
||||
max_rollouts_per_startup: 9,
|
||||
min_rollout_idle_hours: 24,
|
||||
extract_model: Some("gpt-5-mini".to_string()),
|
||||
consolidation_model: Some("gpt-5".to_string()),
|
||||
phase_1_model: Some("gpt-5-mini".to_string()),
|
||||
phase_2_model: Some("gpt-5".to_string()),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ pub const DEFAULT_OTEL_ENVIRONMENT: &str = "dev";
|
||||
pub const DEFAULT_MEMORIES_MAX_ROLLOUTS_PER_STARTUP: usize = 16;
|
||||
pub const DEFAULT_MEMORIES_MAX_ROLLOUT_AGE_DAYS: i64 = 30;
|
||||
pub const DEFAULT_MEMORIES_MIN_ROLLOUT_IDLE_HOURS: i64 = 6;
|
||||
pub const DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_CONSOLIDATION: usize = 256;
|
||||
pub const DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_GLOBAL: usize = 256;
|
||||
pub const DEFAULT_MEMORIES_MAX_UNUSED_DAYS: i64 = 30;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)]
|
||||
@@ -378,7 +378,7 @@ pub struct MemoriesToml {
|
||||
/// When `false`, skip injecting memory usage instructions into developer prompts.
|
||||
pub use_memories: Option<bool>,
|
||||
/// Maximum number of recent raw memories retained for global consolidation.
|
||||
pub max_raw_memories_for_consolidation: Option<usize>,
|
||||
pub max_raw_memories_for_global: Option<usize>,
|
||||
/// Maximum number of days since a memory was last used before it becomes ineligible for phase 2 selection.
|
||||
pub max_unused_days: Option<i64>,
|
||||
/// Maximum age of the threads used for memories.
|
||||
@@ -388,9 +388,9 @@ pub struct MemoriesToml {
|
||||
/// Minimum idle time between last thread activity and memory creation (hours). > 12h recommended.
|
||||
pub min_rollout_idle_hours: Option<i64>,
|
||||
/// Model used for thread summarisation.
|
||||
pub extract_model: Option<String>,
|
||||
pub phase_1_model: Option<String>,
|
||||
/// Model used for memory consolidation.
|
||||
pub consolidation_model: Option<String>,
|
||||
pub phase_2_model: Option<String>,
|
||||
}
|
||||
|
||||
/// Effective memories settings after defaults are applied.
|
||||
@@ -399,13 +399,13 @@ pub struct MemoriesConfig {
|
||||
pub no_memories_if_mcp_or_web_search: bool,
|
||||
pub generate_memories: bool,
|
||||
pub use_memories: bool,
|
||||
pub max_raw_memories_for_consolidation: usize,
|
||||
pub max_raw_memories_for_global: usize,
|
||||
pub max_unused_days: i64,
|
||||
pub max_rollout_age_days: i64,
|
||||
pub max_rollouts_per_startup: usize,
|
||||
pub min_rollout_idle_hours: i64,
|
||||
pub extract_model: Option<String>,
|
||||
pub consolidation_model: Option<String>,
|
||||
pub phase_1_model: Option<String>,
|
||||
pub phase_2_model: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for MemoriesConfig {
|
||||
@@ -414,13 +414,13 @@ impl Default for MemoriesConfig {
|
||||
no_memories_if_mcp_or_web_search: false,
|
||||
generate_memories: true,
|
||||
use_memories: true,
|
||||
max_raw_memories_for_consolidation: DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_CONSOLIDATION,
|
||||
max_raw_memories_for_global: DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_GLOBAL,
|
||||
max_unused_days: DEFAULT_MEMORIES_MAX_UNUSED_DAYS,
|
||||
max_rollout_age_days: DEFAULT_MEMORIES_MAX_ROLLOUT_AGE_DAYS,
|
||||
max_rollouts_per_startup: DEFAULT_MEMORIES_MAX_ROLLOUTS_PER_STARTUP,
|
||||
min_rollout_idle_hours: DEFAULT_MEMORIES_MIN_ROLLOUT_IDLE_HOURS,
|
||||
extract_model: None,
|
||||
consolidation_model: None,
|
||||
phase_1_model: None,
|
||||
phase_2_model: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -434,9 +434,9 @@ impl From<MemoriesToml> for MemoriesConfig {
|
||||
.unwrap_or(defaults.no_memories_if_mcp_or_web_search),
|
||||
generate_memories: toml.generate_memories.unwrap_or(defaults.generate_memories),
|
||||
use_memories: toml.use_memories.unwrap_or(defaults.use_memories),
|
||||
max_raw_memories_for_consolidation: toml
|
||||
.max_raw_memories_for_consolidation
|
||||
.unwrap_or(defaults.max_raw_memories_for_consolidation)
|
||||
max_raw_memories_for_global: toml
|
||||
.max_raw_memories_for_global
|
||||
.unwrap_or(defaults.max_raw_memories_for_global)
|
||||
.min(4096),
|
||||
max_unused_days: toml
|
||||
.max_unused_days
|
||||
@@ -454,8 +454,8 @@ impl From<MemoriesToml> for MemoriesConfig {
|
||||
.min_rollout_idle_hours
|
||||
.unwrap_or(defaults.min_rollout_idle_hours)
|
||||
.clamp(1, 48),
|
||||
extract_model: toml.extract_model,
|
||||
consolidation_model: toml.consolidation_model,
|
||||
phase_1_model: toml.phase_1_model,
|
||||
phase_2_model: toml.phase_2_model,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ async fn claim_startup_jobs(
|
||||
async fn build_request_context(session: &Arc<Session>, config: &Config) -> RequestContext {
|
||||
let model_name = config
|
||||
.memories
|
||||
.extract_model
|
||||
.phase_1_model
|
||||
.clone()
|
||||
.unwrap_or(phase_one::MODEL.to_string());
|
||||
let model = session
|
||||
|
||||
@@ -52,7 +52,7 @@ pub(super) async fn run(session: &Arc<Session>, config: Arc<Config>) {
|
||||
return;
|
||||
};
|
||||
let root = memory_root(&config.codex_home);
|
||||
let max_raw_memories = config.memories.max_raw_memories_for_consolidation;
|
||||
let max_raw_memories = config.memories.max_raw_memories_for_global;
|
||||
let max_unused_days = config.memories.max_unused_days;
|
||||
|
||||
// 1. Claim the job.
|
||||
@@ -294,7 +294,7 @@ mod agent {
|
||||
agent_config.model = Some(
|
||||
config
|
||||
.memories
|
||||
.consolidation_model
|
||||
.phase_2_model
|
||||
.clone()
|
||||
.unwrap_or(phase_two::MODEL.to_string()),
|
||||
);
|
||||
|
||||
@@ -13,21 +13,21 @@ use crate::memories::rollout_summaries_dir;
|
||||
pub(super) async fn rebuild_raw_memories_file_from_memories(
|
||||
root: &Path,
|
||||
memories: &[Stage1Output],
|
||||
max_raw_memories_for_consolidation: usize,
|
||||
max_raw_memories_for_global: usize,
|
||||
) -> std::io::Result<()> {
|
||||
ensure_layout(root).await?;
|
||||
rebuild_raw_memories_file(root, memories, max_raw_memories_for_consolidation).await
|
||||
rebuild_raw_memories_file(root, memories, max_raw_memories_for_global).await
|
||||
}
|
||||
|
||||
/// Syncs canonical rollout summary files from DB-backed stage-1 output rows.
|
||||
pub(super) async fn sync_rollout_summaries_from_memories(
|
||||
root: &Path,
|
||||
memories: &[Stage1Output],
|
||||
max_raw_memories_for_consolidation: usize,
|
||||
max_raw_memories_for_global: usize,
|
||||
) -> std::io::Result<()> {
|
||||
ensure_layout(root).await?;
|
||||
|
||||
let retained = retained_memories(memories, max_raw_memories_for_consolidation);
|
||||
let retained = retained_memories(memories, max_raw_memories_for_global);
|
||||
let keep = retained
|
||||
.iter()
|
||||
.map(rollout_summary_file_stem)
|
||||
@@ -62,9 +62,9 @@ pub(super) async fn sync_rollout_summaries_from_memories(
|
||||
async fn rebuild_raw_memories_file(
|
||||
root: &Path,
|
||||
memories: &[Stage1Output],
|
||||
max_raw_memories_for_consolidation: usize,
|
||||
max_raw_memories_for_global: usize,
|
||||
) -> std::io::Result<()> {
|
||||
let retained = retained_memories(memories, max_raw_memories_for_consolidation);
|
||||
let retained = retained_memories(memories, max_raw_memories_for_global);
|
||||
let mut body = String::from("# Raw Memories\n\n");
|
||||
|
||||
if retained.is_empty() {
|
||||
@@ -155,9 +155,9 @@ async fn write_rollout_summary_for_thread(
|
||||
|
||||
fn retained_memories(
|
||||
memories: &[Stage1Output],
|
||||
max_raw_memories_for_consolidation: usize,
|
||||
max_raw_memories_for_global: usize,
|
||||
) -> &[Stage1Output] {
|
||||
&memories[..memories.len().min(max_raw_memories_for_consolidation)]
|
||||
&memories[..memories.len().min(max_raw_memories_for_global)]
|
||||
}
|
||||
|
||||
fn raw_memories_format_error(err: std::fmt::Error) -> std::io::Error {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::storage::rebuild_raw_memories_file_from_memories;
|
||||
use super::storage::sync_rollout_summaries_from_memories;
|
||||
use crate::config::types::DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_CONSOLIDATION;
|
||||
use crate::config::types::DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_GLOBAL;
|
||||
use crate::memories::ensure_layout;
|
||||
use crate::memories::memory_root;
|
||||
use crate::memories::raw_memories_file;
|
||||
@@ -95,14 +95,14 @@ async fn sync_rollout_summaries_and_raw_memories_file_keeps_latest_memories_only
|
||||
sync_rollout_summaries_from_memories(
|
||||
&root,
|
||||
&memories,
|
||||
DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_CONSOLIDATION,
|
||||
DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_GLOBAL,
|
||||
)
|
||||
.await
|
||||
.expect("sync rollout summaries");
|
||||
rebuild_raw_memories_file_from_memories(
|
||||
&root,
|
||||
&memories,
|
||||
DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_CONSOLIDATION,
|
||||
DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_GLOBAL,
|
||||
)
|
||||
.await
|
||||
.expect("rebuild raw memories");
|
||||
@@ -201,7 +201,7 @@ async fn sync_rollout_summaries_uses_timestamp_hash_and_sanitized_slug_filename(
|
||||
sync_rollout_summaries_from_memories(
|
||||
&root,
|
||||
&memories,
|
||||
DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_CONSOLIDATION,
|
||||
DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_GLOBAL,
|
||||
)
|
||||
.await
|
||||
.expect("sync rollout summaries");
|
||||
@@ -304,14 +304,14 @@ task_outcome: success
|
||||
sync_rollout_summaries_from_memories(
|
||||
&root,
|
||||
&memories,
|
||||
DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_CONSOLIDATION,
|
||||
DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_GLOBAL,
|
||||
)
|
||||
.await
|
||||
.expect("sync rollout summaries");
|
||||
rebuild_raw_memories_file_from_memories(
|
||||
&root,
|
||||
&memories,
|
||||
DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_CONSOLIDATION,
|
||||
DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_GLOBAL,
|
||||
)
|
||||
.await
|
||||
.expect("rebuild raw memories");
|
||||
|
||||
@@ -20,7 +20,6 @@ use crate::protocol::EventMsg;
|
||||
use crate::protocol::SessionConfiguredEvent;
|
||||
use crate::rollout::RolloutRecorder;
|
||||
use crate::rollout::truncation;
|
||||
use crate::shell_snapshot::ShellSnapshot;
|
||||
use crate::skills::SkillsManager;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::CollaborationModeMask;
|
||||
@@ -480,7 +479,6 @@ impl ThreadManagerState {
|
||||
self.session_source.clone(),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -492,7 +490,6 @@ impl ThreadManagerState {
|
||||
session_source: SessionSource,
|
||||
persist_extended_history: bool,
|
||||
metrics_service_name: Option<String>,
|
||||
inherited_shell_snapshot: Option<Arc<ShellSnapshot>>,
|
||||
) -> CodexResult<NewThread> {
|
||||
self.spawn_thread_with_source(
|
||||
config,
|
||||
@@ -503,7 +500,6 @@ impl ThreadManagerState {
|
||||
Vec::new(),
|
||||
persist_extended_history,
|
||||
metrics_service_name,
|
||||
inherited_shell_snapshot,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -514,7 +510,6 @@ impl ThreadManagerState {
|
||||
rollout_path: PathBuf,
|
||||
agent_control: AgentControl,
|
||||
session_source: SessionSource,
|
||||
inherited_shell_snapshot: Option<Arc<ShellSnapshot>>,
|
||||
) -> CodexResult<NewThread> {
|
||||
let initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?;
|
||||
self.spawn_thread_with_source(
|
||||
@@ -526,7 +521,6 @@ impl ThreadManagerState {
|
||||
Vec::new(),
|
||||
false,
|
||||
None,
|
||||
inherited_shell_snapshot,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -538,7 +532,6 @@ impl ThreadManagerState {
|
||||
agent_control: AgentControl,
|
||||
session_source: SessionSource,
|
||||
persist_extended_history: bool,
|
||||
inherited_shell_snapshot: Option<Arc<ShellSnapshot>>,
|
||||
) -> CodexResult<NewThread> {
|
||||
self.spawn_thread_with_source(
|
||||
config,
|
||||
@@ -549,7 +542,6 @@ impl ThreadManagerState {
|
||||
Vec::new(),
|
||||
persist_extended_history,
|
||||
None,
|
||||
inherited_shell_snapshot,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -575,7 +567,6 @@ impl ThreadManagerState {
|
||||
dynamic_tools,
|
||||
persist_extended_history,
|
||||
metrics_service_name,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -591,7 +582,6 @@ impl ThreadManagerState {
|
||||
dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
|
||||
persist_extended_history: bool,
|
||||
metrics_service_name: Option<String>,
|
||||
inherited_shell_snapshot: Option<Arc<ShellSnapshot>>,
|
||||
) -> CodexResult<NewThread> {
|
||||
let watch_registration = self
|
||||
.file_watcher
|
||||
@@ -612,7 +602,6 @@ impl ThreadManagerState {
|
||||
dynamic_tools,
|
||||
persist_extended_history,
|
||||
metrics_service_name,
|
||||
inherited_shell_snapshot,
|
||||
)
|
||||
.await?;
|
||||
self.finalize_thread_spawn(codex, thread_id, watch_registration)
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
//! Implements the collaboration tool surface for spawning and managing sub-agents.
|
||||
//!
|
||||
//! This handler translates model tool calls into `AgentControl` operations and keeps spawned
|
||||
//! agents aligned with the live turn that created them. Sub-agents start from the turn's effective
|
||||
//! config, inherit runtime-only state such as provider, approval policy, sandbox, and cwd, and
|
||||
//! then optionally layer role-specific config on top.
|
||||
|
||||
use crate::agent::AgentStatus;
|
||||
use crate::agent::exceeds_thread_spawn_depth_limit;
|
||||
use crate::codex::Session;
|
||||
@@ -35,6 +42,7 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Function-tool handler for the multi-agent collaboration API.
|
||||
pub struct MultiAgentHandler;
|
||||
|
||||
/// Minimum wait timeout to prevent tight polling loops from burning CPU.
|
||||
@@ -894,6 +902,13 @@ fn input_preview(items: &[UserInput]) -> String {
|
||||
parts.join("\n")
|
||||
}
|
||||
|
||||
/// Builds the base config snapshot for a newly spawned sub-agent.
|
||||
///
|
||||
/// The returned config starts from the parent's effective config and then refreshes the
|
||||
/// runtime-owned fields carried on `turn`, including model selection, reasoning settings,
|
||||
/// approval policy, sandbox, and cwd. Role-specific overrides are layered after this step;
|
||||
/// skipping this helper and cloning stale config state directly can send the child agent out with
|
||||
/// the wrong provider or runtime policy.
|
||||
pub(crate) fn build_agent_spawn_config(
|
||||
base_instructions: &BaseInstructions,
|
||||
turn: &TurnContext,
|
||||
@@ -928,6 +943,10 @@ fn build_agent_shared_config(turn: &TurnContext) -> Result<Config, FunctionCallE
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Copies runtime-only turn state onto a child config before it is handed to `AgentControl`.
|
||||
///
|
||||
/// These values are chosen by the live turn rather than persisted config, so leaving them stale
|
||||
/// can make a child agent disagree with its parent about approval policy, cwd, or sandboxing.
|
||||
fn apply_spawn_agent_runtime_overrides(
|
||||
config: &mut Config,
|
||||
turn: &TurnContext,
|
||||
@@ -1114,6 +1133,9 @@ mod tests {
|
||||
let manager = thread_manager();
|
||||
session.services.agent_control = manager.agent_control();
|
||||
let mut config = (*turn.config).clone();
|
||||
let provider = built_in_model_providers()["ollama"].clone();
|
||||
config.model_provider_id = "ollama".to_string();
|
||||
config.model_provider = provider.clone();
|
||||
config
|
||||
.permissions
|
||||
.approval_policy
|
||||
@@ -1122,6 +1144,7 @@ mod tests {
|
||||
turn.approval_policy
|
||||
.set(AskForApproval::OnRequest)
|
||||
.expect("approval policy should be set");
|
||||
turn.provider = provider;
|
||||
turn.config = Arc::new(config);
|
||||
|
||||
let invocation = invocation(
|
||||
@@ -1160,6 +1183,7 @@ mod tests {
|
||||
.config_snapshot()
|
||||
.await;
|
||||
assert_eq!(snapshot.approval_policy, AskForApproval::OnRequest);
|
||||
assert_eq!(snapshot.model_provider_id, "ollama");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -168,7 +168,7 @@ async fn web_search_pollution_moves_selected_thread_into_removed_phase2_inputs()
|
||||
let mut initial_builder = test_codex().with_home(home.clone()).with_config(|config| {
|
||||
config.features.enable(Feature::Sqlite);
|
||||
config.features.enable(Feature::MemoryTool);
|
||||
config.memories.max_raw_memories_for_consolidation = 1;
|
||||
config.memories.max_raw_memories_for_global = 1;
|
||||
config.memories.no_memories_if_mcp_or_web_search = true;
|
||||
});
|
||||
let initial = initial_builder.build(&server).await?;
|
||||
@@ -234,7 +234,7 @@ async fn web_search_pollution_moves_selected_thread_into_removed_phase2_inputs()
|
||||
let mut resumed_builder = test_codex().with_home(home.clone()).with_config(|config| {
|
||||
config.features.enable(Feature::Sqlite);
|
||||
config.features.enable(Feature::MemoryTool);
|
||||
config.memories.max_raw_memories_for_consolidation = 1;
|
||||
config.memories.max_raw_memories_for_global = 1;
|
||||
config.memories.no_memories_if_mcp_or_web_search = true;
|
||||
});
|
||||
let resumed = resumed_builder
|
||||
@@ -313,7 +313,7 @@ async fn build_test_codex(server: &wiremock::MockServer, home: Arc<TempDir>) ->
|
||||
let mut builder = test_codex().with_home(home).with_config(|config| {
|
||||
config.features.enable(Feature::Sqlite);
|
||||
config.features.enable(Feature::MemoryTool);
|
||||
config.memories.max_raw_memories_for_consolidation = 1;
|
||||
config.memories.max_raw_memories_for_global = 1;
|
||||
});
|
||||
builder.build(server).await
|
||||
}
|
||||
|
||||
@@ -1246,8 +1246,8 @@ impl App {
|
||||
.collect();
|
||||
|
||||
self.chat_widget.show_selection_view(SelectionViewParams {
|
||||
title: Some("Multi-agents".to_string()),
|
||||
subtitle: Some("Select an agent to watch".to_string()),
|
||||
title: Some("Agents".to_string()),
|
||||
subtitle: Some("Select a thread to focus".to_string()),
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
items,
|
||||
initial_selected_idx,
|
||||
|
||||
@@ -342,7 +342,7 @@ mod tests {
|
||||
CommandItem::UserPrompt(_) => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(cmds, vec!["model", "mention", "mcp", "multi-agents"]);
|
||||
assert_eq!(cmds, vec!["model", "mention", "mcp"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -129,7 +129,6 @@ pub(crate) enum CancellationEvent {
|
||||
NotHandled,
|
||||
}
|
||||
|
||||
use crate::bottom_pane::prompt_args::parse_slash_name;
|
||||
pub(crate) use chat_composer::ChatComposer;
|
||||
pub(crate) use chat_composer::ChatComposerConfig;
|
||||
pub(crate) use chat_composer::InputResult;
|
||||
@@ -399,20 +398,11 @@ impl BottomPane {
|
||||
self.request_redraw();
|
||||
InputResult::None
|
||||
} else {
|
||||
let is_agent_command = self
|
||||
.composer_text()
|
||||
.lines()
|
||||
.next()
|
||||
.and_then(parse_slash_name)
|
||||
.is_some_and(|(name, _, _)| name == "agent");
|
||||
|
||||
// If a task is running and a status line is visible, allow Esc to
|
||||
// send an interrupt even while the composer has focus.
|
||||
// When a popup is active, prefer dismissing it over interrupting the task.
|
||||
if key_event.code == KeyCode::Esc
|
||||
&& matches!(key_event.kind, KeyEventKind::Press | KeyEventKind::Repeat)
|
||||
&& self.is_task_running
|
||||
&& !is_agent_command
|
||||
&& !self.composer.popup_active()
|
||||
&& let Some(status) = &self.status
|
||||
{
|
||||
@@ -1603,90 +1593,6 @@ mod tests {
|
||||
assert_eq!(pane.composer_text(), "/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esc_with_agent_command_without_popup_does_not_interrupt_task() {
|
||||
let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut pane = BottomPane::new(BottomPaneParams {
|
||||
app_event_tx: tx,
|
||||
frame_requester: FrameRequester::test_dummy(),
|
||||
has_input_focus: true,
|
||||
enhanced_keys_supported: false,
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
pane.set_task_running(true);
|
||||
|
||||
// Repro: `/agent ` hides the popup (cursor past command name). Esc should
|
||||
// keep editing command text instead of interrupting the running task.
|
||||
pane.insert_str("/agent ");
|
||||
assert!(
|
||||
!pane.composer.popup_active(),
|
||||
"expected command popup to be hidden after entering `/agent `"
|
||||
);
|
||||
|
||||
pane.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
|
||||
while let Ok(ev) = rx.try_recv() {
|
||||
assert!(
|
||||
!matches!(ev, AppEvent::CodexOp(Op::Interrupt)),
|
||||
"expected Esc to not send Op::Interrupt while typing `/agent`"
|
||||
);
|
||||
}
|
||||
assert_eq!(pane.composer_text(), "/agent ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esc_release_after_dismissing_agent_picker_does_not_interrupt_task() {
|
||||
let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut pane = BottomPane::new(BottomPaneParams {
|
||||
app_event_tx: tx,
|
||||
frame_requester: FrameRequester::test_dummy(),
|
||||
has_input_focus: true,
|
||||
enhanced_keys_supported: false,
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
pane.set_task_running(true);
|
||||
pane.show_selection_view(SelectionViewParams {
|
||||
title: Some("Agents".to_string()),
|
||||
items: vec![SelectionItem {
|
||||
name: "Main".to_string(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
pane.handle_key_event(KeyEvent::new_with_kind(
|
||||
KeyCode::Esc,
|
||||
KeyModifiers::NONE,
|
||||
KeyEventKind::Press,
|
||||
));
|
||||
pane.handle_key_event(KeyEvent::new_with_kind(
|
||||
KeyCode::Esc,
|
||||
KeyModifiers::NONE,
|
||||
KeyEventKind::Release,
|
||||
));
|
||||
|
||||
while let Ok(ev) = rx.try_recv() {
|
||||
assert!(
|
||||
!matches!(ev, AppEvent::CodexOp(Op::Interrupt)),
|
||||
"expected Esc release after dismissing agent picker to not interrupt"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
pane.no_modal_or_popup_active(),
|
||||
"expected Esc press to dismiss the agent picker"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esc_interrupts_running_task_when_no_popup() {
|
||||
let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
|
||||
|
||||
@@ -3609,7 +3609,7 @@ impl ChatWidget {
|
||||
}
|
||||
self.open_collaboration_modes_popup();
|
||||
}
|
||||
SlashCommand::Agent | SlashCommand::MultiAgents => {
|
||||
SlashCommand::Agent => {
|
||||
self.app_event_tx.send(AppEvent::OpenAgentPicker);
|
||||
}
|
||||
SlashCommand::Approvals => {
|
||||
|
||||
@@ -53,7 +53,6 @@ pub enum SlashCommand {
|
||||
Realtime,
|
||||
Settings,
|
||||
TestApproval,
|
||||
MultiAgents,
|
||||
// Debugging commands.
|
||||
#[strum(serialize = "debug-m-drop")]
|
||||
MemoryDrop,
|
||||
@@ -94,7 +93,7 @@ impl SlashCommand {
|
||||
SlashCommand::Settings => "configure realtime microphone/speaker",
|
||||
SlashCommand::Plan => "switch to Plan mode",
|
||||
SlashCommand::Collab => "change collaboration mode (experimental)",
|
||||
SlashCommand::Agent | SlashCommand::MultiAgents => "switch the active agent thread",
|
||||
SlashCommand::Agent => "switch the active agent thread",
|
||||
SlashCommand::Approvals => "choose what Codex is allowed to do",
|
||||
SlashCommand::Permissions => "choose what Codex is allowed to do",
|
||||
SlashCommand::ElevateSandbox => "set up elevated agent sandbox",
|
||||
@@ -168,7 +167,7 @@ impl SlashCommand {
|
||||
SlashCommand::Realtime => true,
|
||||
SlashCommand::Settings => true,
|
||||
SlashCommand::Collab => true,
|
||||
SlashCommand::Agent | SlashCommand::MultiAgents => true,
|
||||
SlashCommand::Agent => true,
|
||||
SlashCommand::Statusline => false,
|
||||
SlashCommand::Theme => false,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user