Refresh global instructions at model-request boundaries (#44675)

## Why

Running root threads retained their startup global instructions, so edits to global `AGENTS.md` files did not take effect during an active session.

## What changed

- Reload global instructions when capturing model-request context, including after tools within the same turn. Apply changes without repeating unchanged instructions or rediscovering repository instructions when the environment and trust level are unchanged.
- Preserve the last successful global instructions on read failures, suppress recurring warnings until recovery, and clear instructions when their source is removed or blank.
- Serialize refreshes and allow cancellation without blocking subsequent requests.
- Give new subagents the parent's applied instruction snapshot and update Guardian reviewer reuse to account for refreshed instructions.

## Testing

Add regression coverage for live edits and removal, read failures and recovery, warning suppression, cancellation, subagent inheritance, and Guardian reviewer reuse. Update resume, fork, and compaction tests to verify refreshed instructions.

GitOrigin-RevId: f7e9b399740fa4f45e482c070f6e901ee7cf85cd
This commit is contained in:
vkg-oai authored and copyberry committed 2026-09-10 23:46:33 +00:00
1 parent 28f43b0417
commit 935ac7710d
23 files changed
+1143 -249

No files matched your search

+29 -18
View File
@@ -15,6 +15,7 @@ use crate::agent::agent_status_from_event;
use crate::agent::status::is_final;
use crate::agent_communication::AgentCommunicationContext;
use crate::agent_communication::AgentCommunicationKind;
use crate::agents_md_manager::SessionInstructions;
use crate::attestation::AttestationProvider;
use crate::compact;
use crate::compact::CompactedHistoryMetadata;
@@ -68,7 +69,6 @@ use codex_exec_server::EnvironmentManager;
use codex_execpolicy::prefix_rule_migration;
use codex_extension_api::ConversationHistorySnapshot;
use codex_extension_api::ExtensionDataInit;
use codex_extension_api::LoadedUserInstructions;
use codex_extension_api::PromptSlot;
use codex_extension_api::TurnContextContributionInput;
use codex_features::FEATURES;
@@ -426,7 +426,7 @@ pub(crate) enum ForkPersistence {
pub(crate) struct SessionSpawnArgs {
pub(crate) config: Config,
pub(crate) allow_provider_model_fallback: bool,
pub(crate) user_instructions: LoadedUserInstructions,
pub(crate) instructions: SessionInstructions,
pub(crate) installation_id: String,
pub(crate) auth_manager: Arc<AuthManager>,
pub(crate) models_manager: SharedModelsManager,
@@ -529,9 +529,9 @@ impl Session {
async fn spawn_internal(args: SessionSpawnArgs) -> CodexResult<(Arc<Self>, SessionIo)> {
let SessionSpawnArgs {
mut config,
config,
allow_provider_model_fallback,
user_instructions,
instructions,
installation_id,
auth_manager,
models_manager,
@@ -574,14 +574,6 @@ impl Session {
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
let (tx_event, rx_event) = async_channel::unbounded();
let LoadedUserInstructions {
instructions: user_instructions,
warnings: user_instruction_provider_warnings,
} = user_instructions;
// TODO(anp) pull startup_warnings out of Config
config
.startup_warnings
.extend(user_instruction_provider_warnings);
let isolation = thread_extension_init
.get::<codex_extension_api::SessionIsolation>()
.map(|policy| *policy)
@@ -822,7 +814,7 @@ impl Session {
session_configuration,
&environment_selections,
config.clone(),
user_instructions,
instructions,
installation_id,
auth_manager.clone(),
models_manager.clone(),
@@ -857,7 +849,10 @@ impl Session {
.await
.map_err(|e| {
error!("Failed to create session: {e:#}");
map_session_init_error(&e, &config.codex_home)
match e.downcast::<CodexErr>() {
Ok(error) => error,
Err(error) => map_session_init_error(&error, &config.codex_home),
}
})?;
if let Some(message) = initial_service_tier_warning {
session
@@ -1928,8 +1923,21 @@ impl Session {
.clone()
}
pub(crate) async fn user_instructions(&self) -> Option<codex_extension_api::Instructions> {
self.services.agents_md_manager.user_instructions()
pub(crate) async fn inherited_instructions(&self) -> SessionInstructions {
self.services
.agents_md_manager
.inherited_instructions()
.await
}
pub(crate) async fn emit_instruction_warnings(&self, warnings: Vec<String>) {
for message in warnings {
self.send_event_raw(Event {
id: INITIAL_SUBMIT_ID.to_owned(),
msg: EventMsg::Warning(WarningEvent { message }),
})
.await;
}
}
pub(crate) async fn provider(&self) -> ModelProviderInfo {
@@ -3712,11 +3720,14 @@ impl Session {
let session_telemetry = settings.telemetry(&turn_context.session_telemetry);
// Keep selections fixed for the turn while allowing their startup work to finish.
let environments = turn_context.environments.refresh_readiness();
self.services
let (loaded_agents_md, warnings) = self
.services
.agents_md_manager
.refresh(&turn_context.config, &environments)
.or_cancel(cancellation_token)
.await?;
let loaded_agents_md = self.services.agents_md_manager.get_loaded().await;
self.emit_instruction_warnings(warnings).await;
let loaded_agents_md = loaded_agents_md?;
let selected_capability_roots = self
.resolve_selected_capability_roots_for_step(&environments)
.await;
+11 -3
View File
@@ -6,6 +6,7 @@ use super::step_settings::StepSettingsConstraints;
use super::step_settings::StepSettingsUpdate;
use super::*;
use crate::agents_md_manager::AgentsMdManager;
use crate::agents_md_manager::SessionInstructions;
use crate::config::ConstraintError;
use crate::context::GuardianContextMode;
use crate::environment_selection::ThreadEnvironments;
@@ -667,7 +668,7 @@ impl Session {
mut session_configuration: SessionConfiguration,
environment_selections: &[TurnEnvironmentSelection],
config: Arc<Config>,
user_instructions: Option<codex_extension_api::Instructions>,
instructions: SessionInstructions,
installation_id: String,
auth_manager: Arc<AuthManager>,
models_manager: SharedModelsManager,
@@ -1300,7 +1301,7 @@ impl Session {
&session_configuration.inferred_environment_config(),
);
let resolved_environments = turn_environments.snapshot().await;
let agents_md_manager = Arc::new(AgentsMdManager::new(user_instructions));
let agents_md_manager = Arc::new(AgentsMdManager::new(instructions));
let plugin_skill_warmup = warm_plugins_and_skills_for_session_init(
Arc::clone(&config),
Arc::clone(&plugins_manager),
@@ -1318,13 +1319,20 @@ impl Session {
"session_init.thread_name_lookup",
otel.name = "session_init.thread_name_lookup",
));
let (agents_md_result, plugin_skill_errors, thread_name) = tokio::join!(
let (instruction_refresh, plugin_skill_errors, thread_name) = tokio::join!(
agents_md_manager.refresh(config.as_ref(), &resolved_environments),
plugin_skill_warmup,
thread_name_lookup,
);
let (agents_md_result, instruction_warnings) = instruction_refresh;
// TODO(anp): Present AGENTS.md discovery errors more clearly to the user.
agents_md_result?;
post_session_configured_events.extend(
instruction_warnings.into_iter().map(|message| Event {
id: INITIAL_SUBMIT_ID.to_owned(),
msg: EventMsg::Warning(WarningEvent { message }),
}),
);
for err in &plugin_skill_errors {
error!(
"failed to load skill {}: {}",
@@ -1,4 +1,6 @@
use super::*;
use crate::agents_md_manager::AgentsMdManager;
use crate::agents_md_manager::SessionInstructions;
use crate::guardian::BUNDLED_GUARDIAN_POLICY;
use crate::session::handlers::submission_loop;
use crate::session::step_context::StepContext;
@@ -15,6 +17,10 @@ use codex_config::ConfigRequirementsToml;
use codex_config::ConfigRequirementsWithSources;
use codex_config::RequirementSource;
use codex_config::Sourced;
use codex_extension_api::Instructions;
use codex_extension_api::LoadUserInstructionsFuture;
use codex_extension_api::LoadedUserInstructions;
use codex_extension_api::UserInstructionsProvider;
use codex_http_client::HttpClientFactory;
use codex_login::AuthManager;
use codex_models_manager::ModelsManagerConfig;
@@ -41,6 +47,8 @@ use codex_protocol::protocol::TurnAbortReason;
use pretty_assertions::assert_eq;
use std::collections::BTreeSet;
use std::sync::Mutex as StdMutex;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::time::Duration;
use test_case::test_case;
use tokio::sync::Notify;
@@ -52,6 +60,95 @@ use tokio_util::sync::CancellationToken;
const MODEL_A: &str = "step-activation-a";
const MODEL_B: &str = "step-activation-b";
#[tokio::test]
async fn instruction_refresh_serializes_reads_and_releases_on_cancellation() {
struct GatedInstructionsProvider {
instructions: StdMutex<Option<Instructions>>,
block_next_read: AtomicBool,
read_started: Notify,
}
impl UserInstructionsProvider for GatedInstructionsProvider {
fn load_user_instructions(&self) -> LoadUserInstructionsFuture<'_> {
let instructions = self
.instructions
.lock()
.expect("instruction snapshot")
.clone();
let block = self.block_next_read.swap(/*val*/ false, Ordering::SeqCst);
Box::pin(async move {
if block {
self.read_started.notify_one();
std::future::pending::<()>().await;
}
LoadedUserInstructions {
instructions,
warnings: Vec::new(),
}
})
}
}
let (mut session, turn) = make_session_and_context().await;
let turn = Arc::new(turn);
let source = turn.config.codex_home.join("AGENTS.md");
let instructions = |text: &str| {
Some(Instructions {
text: text.to_string(),
source: source.clone(),
})
};
let provider = Arc::new(GatedInstructionsProvider {
instructions: StdMutex::new(instructions("initial")),
block_next_read: AtomicBool::new(/*v*/ false),
read_started: Notify::new(),
});
session.services.agents_md_manager = Arc::new(AgentsMdManager::new(SessionInstructions {
user_provider: Some(provider.clone()),
..Default::default()
}));
let session = Arc::new(session);
session
.services
.agents_md_manager
.refresh(&turn.config, &turn.environments)
.await
.0
.expect("install initial provider");
*provider.instructions.lock().expect("instruction snapshot") = instructions("cancelled");
provider
.block_next_read
.store(/*val*/ true, Ordering::SeqCst);
let cancellation = CancellationToken::new();
let first_capture = session.capture_step_context(Arc::clone(&turn), &cancellation);
tokio::pin!(first_capture);
tokio::select! {
_ = &mut first_capture => panic!("capture must wait for provider"),
() = provider.read_started.notified() => {}
}
*provider.instructions.lock().expect("instruction snapshot") = instructions("latest");
let next_cancellation = CancellationToken::new();
let next_capture = session.capture_step_context(turn, &next_cancellation);
tokio::pin!(next_capture);
assert!(futures::poll!(&mut next_capture).is_pending());
assert_eq!(
session.inherited_instructions().await.user,
instructions("initial")
);
cancellation.cancel();
assert!(first_capture.await.is_err());
next_capture
.await
.expect("cancelled read releases the refresh guard");
assert_eq!(
session.inherited_instructions().await.user,
instructions("latest")
);
}
fn activation_models() -> Vec<ModelInfo> {
let model = bundled_models_response()
.expect("bundled models")
+6 -5
View File
@@ -6,6 +6,7 @@ pub(crate) use super::step_settings::tests::update_selected_settings_for_test;
use super::turn_context::TurnEnvironment;
use super::*;
use crate::agents_md_manager::AgentsMdManager;
use crate::agents_md_manager::SessionInstructions;
use crate::compact::InitialContextInjection;
use crate::config::ConfigBuilder;
use crate::config::ConfigOverrides;
@@ -6431,7 +6432,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
session_configuration,
/*environment_selections*/ &[],
Arc::clone(&config),
/*user_instructions*/ None,
SessionInstructions::default(),
"11111111-1111-4111-8111-111111111111".to_string(),
auth_manager,
models_manager,
@@ -6673,7 +6674,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
tool_approvals: Mutex::new(ApprovalStore::default()),
runtime_handle: tokio::runtime::Handle::current(),
skills_service,
agents_md_manager: Arc::new(AgentsMdManager::new(/*user_instructions*/ None)),
agents_md_manager: Arc::new(AgentsMdManager::new(SessionInstructions::default())),
plugins_manager,
mcp_manager,
extensions: Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
@@ -6912,7 +6913,7 @@ async fn make_session_with_config_and_rx(
session_configuration,
&default_environments,
Arc::clone(&config),
/*user_instructions*/ None,
SessionInstructions::default(),
"11111111-1111-4111-8111-111111111111".to_string(),
auth_manager,
models_manager,
@@ -7041,7 +7042,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
session_configuration,
&default_environments,
Arc::clone(&config),
/*user_instructions*/ None,
SessionInstructions::default(),
"11111111-1111-4111-8111-111111111111".to_string(),
auth_manager,
models_manager,
@@ -8847,7 +8848,7 @@ where
tool_approvals: Mutex::new(ApprovalStore::default()),
runtime_handle: tokio::runtime::Handle::current(),
skills_service,
agents_md_manager: Arc::new(AgentsMdManager::new(/*user_instructions*/ None)),
agents_md_manager: Arc::new(AgentsMdManager::new(SessionInstructions::default())),
plugins_manager,
mcp_manager,
extensions: Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
@@ -1262,7 +1262,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
let (session, io) = Session::spawn(SessionSpawnArgs {
config,
allow_provider_model_fallback: false,
user_instructions: Default::default(),
instructions: Default::default(),
installation_id: "11111111-1111-4111-8111-111111111111".to_string(),
auth_manager,
models_manager,