mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
core: render remote environment cwd natively (#28152)
## Why Model-visible `<environment_context>` should match the environment of the executor, not of the app server. Stacked on #28146. ## What - Keep selected environment cwd values as `PathUri` while building environment context. - Render cwd text using the path convention represented by the URI, with the canonical URI as a fallback. - Preserve compatibility with legacy `TurnContextItem.cwd` values when reconstructing and diffing context. - Extend the Wine-backed remote Windows test to assert that the model sees `powershell` and `C:\windows`.
This commit is contained in:
@@ -458,7 +458,7 @@ impl CodexThread {
|
||||
self.codex
|
||||
.session
|
||||
.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
|
||||
.await;
|
||||
.await?;
|
||||
}
|
||||
self.codex
|
||||
.session
|
||||
|
||||
@@ -10,6 +10,7 @@ use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
use codex_protocol::protocol::TurnContextNetworkItem;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -28,12 +29,12 @@ pub(crate) struct EnvironmentContext {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct EnvironmentContextEnvironment {
|
||||
pub(crate) id: String,
|
||||
pub(crate) cwd: AbsolutePathBuf,
|
||||
pub(crate) cwd: PathUri,
|
||||
pub(crate) shell: String,
|
||||
}
|
||||
|
||||
impl EnvironmentContextEnvironment {
|
||||
fn legacy(cwd: AbsolutePathBuf, shell: String) -> Self {
|
||||
fn legacy(cwd: PathUri, shell: String) -> Self {
|
||||
Self {
|
||||
id: String::new(),
|
||||
cwd,
|
||||
@@ -44,18 +45,14 @@ impl EnvironmentContextEnvironment {
|
||||
fn from_turn_environments(environments: &[TurnEnvironment], shell: &Shell) -> Vec<Self> {
|
||||
environments
|
||||
.iter()
|
||||
.filter_map(|environment| {
|
||||
// TODO(anp): Migrate EnvironmentContextEnvironment to PathUri so foreign
|
||||
// environments remain visible in model context.
|
||||
Some(Self {
|
||||
id: environment.environment_id.clone(),
|
||||
cwd: environment.cwd().to_abs_path().ok()?,
|
||||
shell: environment
|
||||
.shell
|
||||
.as_ref()
|
||||
.map(|shell| shell.name().to_string())
|
||||
.unwrap_or_else(|| shell.name().to_string()),
|
||||
})
|
||||
.map(|environment| Self {
|
||||
id: environment.environment_id.clone(),
|
||||
cwd: environment.cwd().clone(),
|
||||
shell: environment
|
||||
.shell
|
||||
.as_ref()
|
||||
.map(|shell| shell.name().to_string())
|
||||
.unwrap_or_else(|| shell.name().to_string()),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -383,12 +380,12 @@ impl EnvironmentContext {
|
||||
pub(crate) fn diff_from_turn_context_item(
|
||||
before: &TurnContextItem,
|
||||
after: &EnvironmentContext,
|
||||
) -> Self {
|
||||
) -> std::io::Result<Self> {
|
||||
let before_network = Self::network_from_turn_context_item(before);
|
||||
let before_filesystem = Self::filesystem_from_turn_context_item(before);
|
||||
let before_filesystem = Self::filesystem_from_turn_context_item(before)?;
|
||||
let environments = match &after.environments {
|
||||
EnvironmentContextEnvironments::Single(environment) => {
|
||||
if before.cwd.as_path() != environment.cwd.as_path() {
|
||||
if before.cwd != environment.cwd {
|
||||
EnvironmentContextEnvironments::Single(EnvironmentContextEnvironment::legacy(
|
||||
environment.cwd.clone(),
|
||||
environment.shell.clone(),
|
||||
@@ -412,14 +409,14 @@ impl EnvironmentContext {
|
||||
} else {
|
||||
before_filesystem
|
||||
};
|
||||
EnvironmentContext::new_with_environments(
|
||||
Ok(EnvironmentContext::new_with_environments(
|
||||
environments,
|
||||
after.current_date.clone(),
|
||||
after.timezone.clone(),
|
||||
network,
|
||||
filesystem,
|
||||
/*subagents*/ None,
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn from_turn_context(turn_context: &TurnContext, shell: &Shell) -> Self {
|
||||
@@ -443,21 +440,18 @@ impl EnvironmentContext {
|
||||
pub(crate) fn from_turn_context_item(
|
||||
turn_context_item: &TurnContextItem,
|
||||
shell: String,
|
||||
) -> Self {
|
||||
let cwd = match AbsolutePathBuf::try_from(turn_context_item.cwd.clone()) {
|
||||
Ok(cwd) => cwd,
|
||||
Err(_) => AbsolutePathBuf::resolve_path_against_base(&turn_context_item.cwd, "/"),
|
||||
};
|
||||
Self::new_with_environments(
|
||||
) -> std::io::Result<Self> {
|
||||
Ok(Self::new_with_environments(
|
||||
EnvironmentContextEnvironments::from_vec(vec![EnvironmentContextEnvironment::legacy(
|
||||
cwd, shell,
|
||||
turn_context_item.cwd.clone(),
|
||||
shell,
|
||||
)]),
|
||||
turn_context_item.current_date.clone(),
|
||||
turn_context_item.timezone.clone(),
|
||||
Self::network_from_turn_context_item(turn_context_item),
|
||||
Self::filesystem_from_turn_context_item(turn_context_item),
|
||||
Self::filesystem_from_turn_context_item(turn_context_item)?,
|
||||
/*subagents*/ None,
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn with_subagents(mut self, subagents: String) -> Self {
|
||||
@@ -504,11 +498,11 @@ impl EnvironmentContext {
|
||||
|
||||
fn filesystem_from_turn_context_item(
|
||||
turn_context_item: &TurnContextItem,
|
||||
) -> Option<FileSystemContext> {
|
||||
Some(FileSystemContext::from_permission_profile(
|
||||
&turn_context_item.permission_profile(),
|
||||
) -> std::io::Result<Option<FileSystemContext>> {
|
||||
Ok(Some(FileSystemContext::from_permission_profile(
|
||||
&turn_context_item.permission_profile()?,
|
||||
&workspace_roots_from_turn_context_item(turn_context_item),
|
||||
))
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,7 +515,7 @@ fn workspace_roots_from_turn_context_item(
|
||||
|
||||
// Older rollout items did not persist workspace roots. Fall back to the
|
||||
// legacy cwd binding only when reconstructing that historical context.
|
||||
match AbsolutePathBuf::try_from(turn_context_item.cwd.clone()) {
|
||||
match turn_context_item.cwd.to_abs_path() {
|
||||
Ok(cwd) => vec![cwd],
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
@@ -547,20 +541,16 @@ impl ContextualUserFragment for EnvironmentContext {
|
||||
let mut lines = Vec::new();
|
||||
match &self.environments {
|
||||
EnvironmentContextEnvironments::Single(environment) => {
|
||||
lines.push(format!(
|
||||
" <cwd>{}</cwd>",
|
||||
environment.cwd.to_string_lossy()
|
||||
));
|
||||
let cwd = environment.cwd.inferred_native_path_string();
|
||||
lines.push(format!(" <cwd>{cwd}</cwd>"));
|
||||
lines.push(format!(" <shell>{}</shell>", environment.shell));
|
||||
}
|
||||
EnvironmentContextEnvironments::Multiple(environments) => {
|
||||
lines.push(" <environments>".to_string());
|
||||
for environment in environments {
|
||||
lines.push(format!(" <environment id=\"{}\">", environment.id));
|
||||
lines.push(format!(
|
||||
" <cwd>{}</cwd>",
|
||||
environment.cwd.to_string_lossy()
|
||||
));
|
||||
let cwd = environment.cwd.inferred_native_path_string();
|
||||
lines.push(format!(" <cwd>{cwd}</cwd>"));
|
||||
lines.push(format!(" <shell>{}</shell>", environment.shell));
|
||||
lines.push(" </environment>".to_string());
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ fn serialize_workspace_write_environment_context() {
|
||||
let context = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: cwd.abs(),
|
||||
cwd: PathUri::from_abs_path(&cwd.abs()),
|
||||
shell: fake_shell_name(),
|
||||
}],
|
||||
Some("2026-02-26".to_string()),
|
||||
@@ -58,6 +58,29 @@ fn serialize_workspace_write_environment_context() {
|
||||
assert_eq!(context.render(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_environment_context_with_foreign_windows_cwd() {
|
||||
let context = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "remote".to_string(),
|
||||
cwd: PathUri::parse("file:///C:/windows").expect("Windows cwd URI"),
|
||||
shell: "powershell".to_string(),
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
/*network*/ None,
|
||||
/*subagents*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
context.render(),
|
||||
r#"<environment_context>
|
||||
<cwd>C:\windows</cwd>
|
||||
<shell>powershell</shell>
|
||||
</environment_context>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_environment_context_with_network() {
|
||||
let network = NetworkContext::new(
|
||||
@@ -67,7 +90,7 @@ fn serialize_environment_context_with_network() {
|
||||
let context = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: test_path_buf("/repo").abs(),
|
||||
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||
shell: fake_shell_name(),
|
||||
}],
|
||||
Some("2026-02-26".to_string()),
|
||||
@@ -129,7 +152,7 @@ fn serialize_environment_context_with_full_filesystem_profile() {
|
||||
let mut context = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: test_path_buf("/repo").abs(),
|
||||
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||
shell: fake_shell_name(),
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
@@ -167,7 +190,7 @@ fn turn_context_item_filesystem_uses_workspace_roots_instead_of_cwd() {
|
||||
let repo_private = repo.join("private");
|
||||
let item = TurnContextItem {
|
||||
turn_id: None,
|
||||
cwd: test_path_buf("/not-the-workspace"),
|
||||
cwd: PathUri::from_abs_path(&test_abs_path("/not-the-workspace")),
|
||||
workspace_roots: Some(vec![repo.clone(), other_repo.clone()]),
|
||||
current_date: None,
|
||||
timezone: None,
|
||||
@@ -186,7 +209,9 @@ fn turn_context_item_filesystem_uses_workspace_roots_instead_of_cwd() {
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
};
|
||||
|
||||
let context = EnvironmentContext::from_turn_context_item(&item, fake_shell_name()).render();
|
||||
let context = EnvironmentContext::from_turn_context_item(&item, fake_shell_name())
|
||||
.expect("turn context should hydrate")
|
||||
.render();
|
||||
|
||||
assert!(
|
||||
context.contains(&format!(
|
||||
@@ -234,7 +259,7 @@ fn equals_except_shell_compares_cwd() {
|
||||
let context1 = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: test_abs_path("/repo"),
|
||||
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||
shell: fake_shell_name(),
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
@@ -245,7 +270,7 @@ fn equals_except_shell_compares_cwd() {
|
||||
let context2 = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: test_abs_path("/repo"),
|
||||
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||
shell: fake_shell_name(),
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
@@ -261,7 +286,7 @@ fn equals_except_shell_compares_cwd_differences() {
|
||||
let context1 = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: test_abs_path("/repo1"),
|
||||
cwd: PathUri::from_abs_path(&test_abs_path("/repo1")),
|
||||
shell: fake_shell_name(),
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
@@ -272,7 +297,7 @@ fn equals_except_shell_compares_cwd_differences() {
|
||||
let context2 = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: test_abs_path("/repo2"),
|
||||
cwd: PathUri::from_abs_path(&test_abs_path("/repo2")),
|
||||
shell: fake_shell_name(),
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
@@ -289,7 +314,7 @@ fn equals_except_shell_ignores_shell() {
|
||||
let context1 = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: test_abs_path("/repo"),
|
||||
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||
shell: "bash".to_string(),
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
@@ -300,7 +325,7 @@ fn equals_except_shell_ignores_shell() {
|
||||
let context2 = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "other".to_string(),
|
||||
cwd: test_abs_path("/repo"),
|
||||
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||
shell: "zsh".to_string(),
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
@@ -317,7 +342,7 @@ fn serialize_environment_context_with_subagents() {
|
||||
let context = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: test_path_buf("/repo").abs(),
|
||||
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||
shell: fake_shell_name(),
|
||||
}],
|
||||
Some("2026-02-26".to_string()),
|
||||
@@ -351,12 +376,12 @@ fn serialize_environment_context_with_multiple_selected_environments() {
|
||||
vec![
|
||||
EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: local_cwd.abs(),
|
||||
cwd: PathUri::from_abs_path(&local_cwd.abs()),
|
||||
shell: "bash".to_string(),
|
||||
},
|
||||
EnvironmentContextEnvironment {
|
||||
id: "remote".to_string(),
|
||||
cwd: remote_cwd.abs(),
|
||||
cwd: PathUri::from_abs_path(&remote_cwd.abs()),
|
||||
shell: "bash".to_string(),
|
||||
},
|
||||
],
|
||||
@@ -396,12 +421,12 @@ fn serialize_environment_context_prefers_environment_shell_when_present() {
|
||||
vec![
|
||||
EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: local_cwd.abs(),
|
||||
cwd: PathUri::from_abs_path(&local_cwd.abs()),
|
||||
shell: "powershell".to_string(),
|
||||
},
|
||||
EnvironmentContextEnvironment {
|
||||
id: "remote".to_string(),
|
||||
cwd: remote_cwd.abs(),
|
||||
cwd: PathUri::from_abs_path(&remote_cwd.abs()),
|
||||
shell: "cmd".to_string(),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -23,13 +23,13 @@ use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
use codex_utils_output_truncation::TruncationPolicy;
|
||||
use codex_utils_output_truncation::truncate_text;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use image::ImageBuffer;
|
||||
use image::ImageFormat;
|
||||
use image::Luma;
|
||||
use image::Rgba;
|
||||
use pretty_assertions::assert_eq;
|
||||
use regex_lite::Regex;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const EXEC_FORMAT_MAX_BYTES: usize = 10_000;
|
||||
const EXEC_FORMAT_MAX_TOKENS: usize = 2_500;
|
||||
@@ -127,7 +127,12 @@ fn developer_msg_with_fragments(texts: &[&str]) -> ResponseItem {
|
||||
fn reference_context_item() -> TurnContextItem {
|
||||
TurnContextItem {
|
||||
turn_id: Some("reference-turn".to_string()),
|
||||
cwd: PathBuf::from("/tmp/reference-cwd"),
|
||||
cwd: PathUri::from_path(
|
||||
std::env::current_dir()
|
||||
.expect("current directory")
|
||||
.join("reference-cwd"),
|
||||
)
|
||||
.expect("absolute reference cwd"),
|
||||
workspace_roots: None,
|
||||
current_date: Some("2026-03-23".to_string()),
|
||||
timezone: Some("America/Los_Angeles".to_string()),
|
||||
|
||||
@@ -22,40 +22,44 @@ fn build_environment_update_item(
|
||||
previous: Option<&TurnContextItem>,
|
||||
next: &TurnContext,
|
||||
shell: &Shell,
|
||||
) -> Option<ResponseItem> {
|
||||
) -> std::io::Result<Option<ResponseItem>> {
|
||||
if !next.config.include_environment_context {
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let prev = previous?;
|
||||
let prev_context = EnvironmentContext::from_turn_context_item(prev, shell.name().to_string());
|
||||
let Some(prev) = previous else {
|
||||
return Ok(None);
|
||||
};
|
||||
let prev_context = EnvironmentContext::from_turn_context_item(prev, shell.name().to_string())?;
|
||||
let next_context = EnvironmentContext::from_turn_context(next, shell);
|
||||
if prev_context.equals_except_shell(&next_context) {
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Some(ContextualUserFragment::into(
|
||||
EnvironmentContext::diff_from_turn_context_item(prev, &next_context),
|
||||
))
|
||||
Ok(Some(ContextualUserFragment::into(
|
||||
EnvironmentContext::diff_from_turn_context_item(prev, &next_context)?,
|
||||
)))
|
||||
}
|
||||
|
||||
fn build_permissions_update_item(
|
||||
previous: Option<&TurnContextItem>,
|
||||
next: &TurnContext,
|
||||
exec_policy: &Policy,
|
||||
) -> Option<String> {
|
||||
) -> std::io::Result<Option<String>> {
|
||||
if !next.config.include_permissions_instructions {
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let prev = previous?;
|
||||
if prev.permission_profile() == next.permission_profile()
|
||||
let Some(prev) = previous else {
|
||||
return Ok(None);
|
||||
};
|
||||
if prev.permission_profile()? == next.permission_profile()
|
||||
&& prev.approval_policy == next.approval_policy.value()
|
||||
{
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Some(
|
||||
Ok(Some(
|
||||
PermissionsInstructions::from_permission_profile(
|
||||
&next.permission_profile,
|
||||
next.approval_policy.value(),
|
||||
@@ -67,7 +71,7 @@ fn build_permissions_update_item(
|
||||
next.features.enabled(Feature::RequestPermissionsTool),
|
||||
)
|
||||
.render(),
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
fn build_collaboration_mode_update_item(
|
||||
@@ -214,17 +218,17 @@ pub(crate) fn build_settings_update_items(
|
||||
shell: &Shell,
|
||||
exec_policy: &Policy,
|
||||
personality_feature_enabled: bool,
|
||||
) -> Vec<ResponseItem> {
|
||||
) -> std::io::Result<Vec<ResponseItem>> {
|
||||
// TODO(ccunningham): build_settings_update_items still does not cover every
|
||||
// model-visible item emitted by build_initial_context. Persist the remaining
|
||||
// inputs or add explicit replay events so fork/resume can diff everything
|
||||
// deterministically.
|
||||
let contextual_user_message = build_environment_update_item(previous, next, shell);
|
||||
let contextual_user_message = build_environment_update_item(previous, next, shell)?;
|
||||
let developer_update_sections = [
|
||||
// Keep model-switch instructions first so model-specific guidance is read before
|
||||
// any other context diffs on this turn.
|
||||
build_model_instructions_update_item(previous_turn_settings, next),
|
||||
build_permissions_update_item(previous, next, exec_policy),
|
||||
build_permissions_update_item(previous, next, exec_policy)?,
|
||||
build_collaboration_mode_update_item(previous, next),
|
||||
build_realtime_update_item(previous, previous_turn_settings, next),
|
||||
build_personality_update_item(previous, next, personality_feature_enabled),
|
||||
@@ -240,5 +244,5 @@ pub(crate) fn build_settings_update_items(
|
||||
if let Some(contextual_user_message) = contextual_user_message {
|
||||
items.push(contextual_user_message);
|
||||
}
|
||||
items
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ pub(crate) async fn build_prompt_input_from_session(
|
||||
) -> CodexResult<Vec<ResponseItem>> {
|
||||
let turn_context = sess.new_default_turn().await;
|
||||
sess.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
|
||||
.await;
|
||||
.await?;
|
||||
|
||||
if !input.is_empty() {
|
||||
let response_item = sess.response_item_from_user_input(turn_context.as_ref(), input);
|
||||
|
||||
@@ -1621,7 +1621,7 @@ impl Session {
|
||||
&self,
|
||||
reference_context_item: Option<&TurnContextItem>,
|
||||
current_context: &TurnContext,
|
||||
) -> Vec<ResponseItem> {
|
||||
) -> CodexResult<Vec<ResponseItem>> {
|
||||
// TODO: Make context updates a pure diff of persisted previous/current TurnContextItem
|
||||
// state so replay/backtracking is deterministic. Runtime inputs that affect model-visible
|
||||
// context (shell, exec policy, feature gates, previous-turn bridge) should be persisted
|
||||
@@ -1632,13 +1632,15 @@ impl Session {
|
||||
};
|
||||
let shell = self.user_shell();
|
||||
let exec_policy = self.services.exec_policy.current();
|
||||
crate::context_manager::updates::build_settings_update_items(
|
||||
reference_context_item,
|
||||
previous_turn_settings.as_ref(),
|
||||
current_context,
|
||||
shell.as_ref(),
|
||||
exec_policy.as_ref(),
|
||||
self.features.enabled(Feature::Personality),
|
||||
Ok(
|
||||
crate::context_manager::updates::build_settings_update_items(
|
||||
reference_context_item,
|
||||
previous_turn_settings.as_ref(),
|
||||
current_context,
|
||||
shell.as_ref(),
|
||||
exec_policy.as_ref(),
|
||||
self.features.enabled(Feature::Personality),
|
||||
)?,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3186,7 +3188,7 @@ impl Session {
|
||||
pub(crate) async fn record_context_updates_and_set_reference_context_item(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
) {
|
||||
) -> CodexResult<()> {
|
||||
let reference_context_item = {
|
||||
let state = self.state.lock().await;
|
||||
state.reference_context_item()
|
||||
@@ -3197,7 +3199,7 @@ impl Session {
|
||||
} else {
|
||||
// Steady-state path: append only context diffs to minimize token overhead.
|
||||
self.build_settings_update_items(reference_context_item.as_ref(), turn_context)
|
||||
.await
|
||||
.await?
|
||||
};
|
||||
let turn_context_item = turn_context.to_turn_context_item();
|
||||
if !context_items.is_empty() {
|
||||
@@ -3213,6 +3215,7 @@ impl Session {
|
||||
// context items. This keeps later runtime diffing aligned with the current turn state.
|
||||
let mut state = self.state.lock().await;
|
||||
state.set_reference_context_item(Some(turn_context_item));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn update_token_usage_info(
|
||||
|
||||
@@ -9,6 +9,7 @@ use codex_protocol::protocol::CompactedItem;
|
||||
use codex_protocol::protocol::InitialHistory;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
use codex_protocol::protocol::ResumedHistory;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -88,7 +89,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ
|
||||
let previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
cwd: PathUri::from_abs_path(&turn_context.cwd),
|
||||
workspace_roots: None,
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -128,7 +129,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
|
||||
let mut previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
cwd: PathUri::from_abs_path(&turn_context.cwd),
|
||||
workspace_roots: None,
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -989,7 +990,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
let previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
cwd: PathUri::from_abs_path(&turn_context.cwd),
|
||||
workspace_roots: None,
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -1071,7 +1072,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
serde_json::to_value(Some(TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
cwd: PathUri::from_abs_path(&turn_context.cwd),
|
||||
workspace_roots: None,
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -1101,7 +1102,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
|
||||
let previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
cwd: PathUri::from_abs_path(&turn_context.cwd),
|
||||
workspace_roots: None,
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -1223,7 +1224,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
|
||||
let current_context_item = TurnContextItem {
|
||||
turn_id: Some(current_turn_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
cwd: PathUri::from_abs_path(&turn_context.cwd),
|
||||
workspace_roots: None,
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -1343,7 +1344,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
|
||||
let previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
cwd: PathUri::from_abs_path(&turn_context.cwd),
|
||||
workspace_roots: None,
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -1506,7 +1507,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
|
||||
let previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
cwd: PathUri::from_abs_path(&turn_context.cwd),
|
||||
workspace_roots: None,
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
|
||||
@@ -1874,7 +1874,8 @@ async fn resumed_history_injects_initial_context_on_first_context_update_only()
|
||||
|
||||
session
|
||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
||||
.await;
|
||||
.await
|
||||
.expect("context updates should hydrate");
|
||||
let initial_context = session.build_initial_context(&turn_context).await;
|
||||
expected.extend(initial_context);
|
||||
let history_after_seed = session.clone_history().await;
|
||||
@@ -1882,7 +1883,8 @@ async fn resumed_history_injects_initial_context_on_first_context_update_only()
|
||||
|
||||
session
|
||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
||||
.await;
|
||||
.await
|
||||
.expect("context updates should hydrate");
|
||||
let history_after_second_seed = session.clone_history().await;
|
||||
assert_eq!(
|
||||
history_after_seed.raw_items(),
|
||||
@@ -2673,7 +2675,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
|
||||
let previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
cwd: PathUri::from_abs_path(&turn_context.cwd),
|
||||
workspace_roots: None,
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -7290,7 +7292,8 @@ async fn build_settings_update_items_emits_environment_item_for_network_changes(
|
||||
let reference_context_item = previous_context.to_turn_context_item();
|
||||
let update_items = session
|
||||
.build_settings_update_items(Some(&reference_context_item), ¤t_context)
|
||||
.await;
|
||||
.await
|
||||
.expect("settings updates should hydrate");
|
||||
|
||||
let environment_update = user_input_texts(&update_items)
|
||||
.into_iter()
|
||||
@@ -7360,7 +7363,8 @@ async fn build_settings_update_items_emits_environment_item_for_time_changes() {
|
||||
let reference_context_item = previous_context.to_turn_context_item();
|
||||
let update_items = session
|
||||
.build_settings_update_items(Some(&reference_context_item), ¤t_context)
|
||||
.await;
|
||||
.await
|
||||
.expect("settings updates should hydrate");
|
||||
|
||||
let environment_update = user_input_texts(&update_items)
|
||||
.into_iter()
|
||||
@@ -7388,7 +7392,8 @@ async fn build_settings_update_items_omits_environment_item_when_disabled() {
|
||||
let reference_context_item = previous_context.to_turn_context_item();
|
||||
let update_items = session
|
||||
.build_settings_update_items(Some(&reference_context_item), ¤t_context)
|
||||
.await;
|
||||
.await
|
||||
.expect("settings updates should hydrate");
|
||||
|
||||
let user_texts = user_input_texts(&update_items);
|
||||
assert!(
|
||||
@@ -7416,7 +7421,8 @@ async fn build_settings_update_items_emits_realtime_start_when_session_becomes_l
|
||||
Some(&previous_context.to_turn_context_item()),
|
||||
¤t_context,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.expect("settings updates should hydrate");
|
||||
|
||||
let developer_texts = developer_input_texts(&update_items);
|
||||
assert!(
|
||||
@@ -7444,7 +7450,8 @@ async fn build_settings_update_items_emits_realtime_end_when_session_stops_being
|
||||
Some(&previous_context.to_turn_context_item()),
|
||||
¤t_context,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.expect("settings updates should hydrate");
|
||||
|
||||
let developer_texts = developer_input_texts(&update_items);
|
||||
assert!(
|
||||
@@ -7478,7 +7485,8 @@ async fn build_settings_update_items_uses_previous_turn_settings_for_realtime_en
|
||||
.await;
|
||||
let update_items = session
|
||||
.build_settings_update_items(Some(&previous_context_item), ¤t_context)
|
||||
.await;
|
||||
.await
|
||||
.expect("settings updates should hydrate");
|
||||
|
||||
let developer_texts = developer_input_texts(&update_items);
|
||||
assert!(
|
||||
@@ -8130,6 +8138,21 @@ async fn turn_context_item_uses_turn_context_comp_hash_snapshot() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_context_item_stores_primary_environment_cwd_uri() {
|
||||
let (_session, mut turn_context) = make_session_and_context().await;
|
||||
let environment = turn_context.environments.turn_environments[0].clone();
|
||||
let cwd = PathUri::parse("file:///C:/windows").expect("Windows cwd URI");
|
||||
turn_context.environments.turn_environments[0] = TurnEnvironment::new(
|
||||
"remote".to_string(),
|
||||
environment.environment,
|
||||
cwd.clone(),
|
||||
environment.shell,
|
||||
);
|
||||
|
||||
assert_eq!(turn_context.to_turn_context_item().cwd, cwd);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_context_item_omits_legacy_equivalent_file_system_sandbox_policy() {
|
||||
let (_session, turn_context) = make_session_and_context().await;
|
||||
@@ -8171,7 +8194,8 @@ async fn record_context_updates_and_set_reference_context_item_injects_full_cont
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
session
|
||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
||||
.await;
|
||||
.await
|
||||
.expect("context updates should hydrate");
|
||||
let history = session.clone_history().await;
|
||||
let initial_context = session.build_initial_context(&turn_context).await;
|
||||
assert_eq!(history.raw_items().to_vec(), initial_context);
|
||||
@@ -8202,7 +8226,8 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co
|
||||
.await;
|
||||
session
|
||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
||||
.await;
|
||||
.await
|
||||
.expect("context updates should hydrate");
|
||||
{
|
||||
let mut state = session.state.lock().await;
|
||||
state.set_reference_context_item(/*item*/ None);
|
||||
@@ -8216,7 +8241,8 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co
|
||||
|
||||
session
|
||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
||||
.await;
|
||||
.await
|
||||
.expect("context updates should hydrate");
|
||||
|
||||
let history = session.clone_history().await;
|
||||
let mut expected_history = vec![compacted_summary];
|
||||
@@ -8246,12 +8272,14 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline
|
||||
|
||||
let update_items = session
|
||||
.build_settings_update_items(Some(&previous_context_item), &turn_context)
|
||||
.await;
|
||||
.await
|
||||
.expect("settings updates should hydrate");
|
||||
assert_eq!(update_items, Vec::new());
|
||||
|
||||
session
|
||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
||||
.await;
|
||||
.await
|
||||
.expect("context updates should hydrate");
|
||||
|
||||
assert_eq!(
|
||||
session.clone_history().await.raw_items().to_vec(),
|
||||
@@ -8298,7 +8326,8 @@ async fn record_context_updates_and_set_reference_context_item_persists_split_fi
|
||||
|
||||
session
|
||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
||||
.await;
|
||||
.await
|
||||
.expect("context updates should hydrate");
|
||||
session.ensure_rollout_materialized().await;
|
||||
session.flush_rollout().await.expect("rollout should flush");
|
||||
|
||||
@@ -8382,7 +8411,8 @@ async fn record_context_updates_and_set_reference_context_item_persists_full_rei
|
||||
.await;
|
||||
session
|
||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
||||
.await;
|
||||
.await
|
||||
.expect("context updates should hydrate");
|
||||
session.ensure_rollout_materialized().await;
|
||||
session.flush_rollout().await.expect("rollout should flush");
|
||||
|
||||
|
||||
@@ -159,8 +159,16 @@ pub(crate) async fn run_turn(
|
||||
return None;
|
||||
}
|
||||
|
||||
sess.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
|
||||
.await;
|
||||
if let Err(err) = sess
|
||||
.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
|
||||
.await
|
||||
{
|
||||
let error = err.to_codex_protocol_error();
|
||||
sess.emit_turn_error_lifecycle(turn_context.as_ref(), error.clone())
|
||||
.await;
|
||||
error!(%err, "failed to hydrate persisted turn context");
|
||||
return None;
|
||||
}
|
||||
|
||||
let (injection_items, explicitly_enabled_connectors) =
|
||||
build_skills_and_plugins(&sess, turn_context.as_ref(), &input, &cancellation_token).await?;
|
||||
|
||||
@@ -396,10 +396,17 @@ impl TurnContext {
|
||||
|
||||
pub(crate) fn to_turn_context_item(&self) -> TurnContextItem {
|
||||
let workspace_roots = self.config.effective_workspace_roots();
|
||||
let cwd = self
|
||||
.environments
|
||||
.primary()
|
||||
.map(|environment| environment.cwd().clone())
|
||||
.unwrap_or_else(|| {
|
||||
#[allow(deprecated)]
|
||||
PathUri::from_abs_path(&self.cwd)
|
||||
});
|
||||
TurnContextItem {
|
||||
turn_id: Some(self.sub_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: self.cwd.to_path_buf(),
|
||||
cwd,
|
||||
workspace_roots: (!workspace_roots.is_empty()).then_some(workspace_roots),
|
||||
current_date: self.current_date.clone(),
|
||||
timezone: self.timezone.clone(),
|
||||
|
||||
Reference in New Issue
Block a user