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:
Adam Perry @ OpenAI
2026-06-16 16:17:47 -07:00
committed by GitHub
parent a34da3b295
commit 4c79527e31
25 changed files with 457 additions and 185 deletions
+13 -10
View File
@@ -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(),
+46 -16
View File
@@ -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), &current_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), &current_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), &current_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()),
&current_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()),
&current_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), &current_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");
+10 -2
View File
@@ -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?;
+9 -2
View File
@@ -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(),