[codex] core: restore absolute turn context cwd (#28629)

## Why

#28152 jumped the gun on moving the rollout format to store URIs, and
would likely break compat with some features that don't go through the
same types as the core logic.

## What

Make `TurnContextItem.cwd` an `AbsolutePathBuf` again, remove test added
for `PathUri` serialization in rollouts. Also drops a bunch of error
paths that are no longer needed.
This commit is contained in:
Adam Perry @ OpenAI
2026-06-16 19:05:26 -07:00
committed by GitHub
parent 69bc0645ac
commit 3d02c443bc
21 changed files with 142 additions and 282 deletions
+1 -1
View File
@@ -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
@@ -380,12 +380,12 @@ impl EnvironmentContext {
pub(crate) fn diff_from_turn_context_item(
before: &TurnContextItem,
after: &EnvironmentContext,
) -> std::io::Result<Self> {
) -> 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 != environment.cwd {
if PathUri::from_abs_path(&before.cwd) != environment.cwd {
EnvironmentContextEnvironments::Single(EnvironmentContextEnvironment::legacy(
environment.cwd.clone(),
environment.shell.clone(),
@@ -409,14 +409,14 @@ impl EnvironmentContext {
} else {
before_filesystem
};
Ok(EnvironmentContext::new_with_environments(
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 {
@@ -440,18 +440,18 @@ impl EnvironmentContext {
pub(crate) fn from_turn_context_item(
turn_context_item: &TurnContextItem,
shell: String,
) -> std::io::Result<Self> {
Ok(Self::new_with_environments(
) -> Self {
Self::new_with_environments(
EnvironmentContextEnvironments::from_vec(vec![EnvironmentContextEnvironment::legacy(
turn_context_item.cwd.clone(),
PathUri::from_abs_path(&turn_context_item.cwd),
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 {
@@ -498,11 +498,11 @@ impl EnvironmentContext {
fn filesystem_from_turn_context_item(
turn_context_item: &TurnContextItem,
) -> std::io::Result<Option<FileSystemContext>> {
Ok(Some(FileSystemContext::from_permission_profile(
&turn_context_item.permission_profile()?,
) -> Option<FileSystemContext> {
Some(FileSystemContext::from_permission_profile(
&turn_context_item.permission_profile(),
&workspace_roots_from_turn_context_item(turn_context_item),
)))
))
}
}
@@ -513,12 +513,7 @@ fn workspace_roots_from_turn_context_item(
return workspace_roots.clone();
}
// Older rollout items did not persist workspace roots. Fall back to the
// legacy cwd binding only when reconstructing that historical context.
match turn_context_item.cwd.to_abs_path() {
Ok(cwd) => vec![cwd],
Err(_) => Vec::new(),
}
vec![turn_context_item.cwd.clone()]
}
impl ContextualUserFragment for EnvironmentContext {
@@ -190,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: PathUri::from_abs_path(&test_abs_path("/not-the-workspace")),
cwd: test_abs_path("/not-the-workspace"),
workspace_roots: Some(vec![repo.clone(), other_repo.clone()]),
current_date: None,
timezone: None,
@@ -209,9 +209,7 @@ 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())
.expect("turn context should hydrate")
.render();
let context = EnvironmentContext::from_turn_context_item(&item, fake_shell_name()).render();
assert!(
context.contains(&format!(
@@ -21,9 +21,9 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::InterAgentCommunication;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::TurnContextItem;
use codex_utils_absolute_path::AbsolutePathBuf;
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;
@@ -127,7 +127,7 @@ fn developer_msg_with_fragments(texts: &[&str]) -> ResponseItem {
fn reference_context_item() -> TurnContextItem {
TurnContextItem {
turn_id: Some("reference-turn".to_string()),
cwd: PathUri::from_path(
cwd: AbsolutePathBuf::try_from(
std::env::current_dir()
.expect("current directory")
.join("reference-cwd"),
+19 -23
View File
@@ -22,44 +22,40 @@ fn build_environment_update_item(
previous: Option<&TurnContextItem>,
next: &TurnContext,
shell: &Shell,
) -> std::io::Result<Option<ResponseItem>> {
) -> Option<ResponseItem> {
if !next.config.include_environment_context {
return Ok(None);
return None;
}
let Some(prev) = previous else {
return Ok(None);
};
let prev_context = EnvironmentContext::from_turn_context_item(prev, shell.name().to_string())?;
let prev = previous?;
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 Ok(None);
return None;
}
Ok(Some(ContextualUserFragment::into(
EnvironmentContext::diff_from_turn_context_item(prev, &next_context)?,
)))
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,
) -> std::io::Result<Option<String>> {
) -> Option<String> {
if !next.config.include_permissions_instructions {
return Ok(None);
return None;
}
let Some(prev) = previous else {
return Ok(None);
};
if prev.permission_profile()? == next.permission_profile()
let prev = previous?;
if prev.permission_profile() == next.permission_profile()
&& prev.approval_policy == next.approval_policy.value()
{
return Ok(None);
return None;
}
Ok(Some(
Some(
PermissionsInstructions::from_permission_profile(
&next.permission_profile,
next.approval_policy.value(),
@@ -71,7 +67,7 @@ fn build_permissions_update_item(
next.features.enabled(Feature::RequestPermissionsTool),
)
.render(),
))
)
}
fn build_collaboration_mode_update_item(
@@ -218,17 +214,17 @@ pub(crate) fn build_settings_update_items(
shell: &Shell,
exec_policy: &Policy,
personality_feature_enabled: bool,
) -> std::io::Result<Vec<ResponseItem>> {
) -> 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),
@@ -244,5 +240,5 @@ pub(crate) fn build_settings_update_items(
if let Some(contextual_user_message) = contextual_user_message {
items.push(contextual_user_message);
}
Ok(items)
items
}
+1 -1
View File
@@ -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);
+10 -13
View File
@@ -1621,7 +1621,7 @@ impl Session {
&self,
reference_context_item: Option<&TurnContextItem>,
current_context: &TurnContext,
) -> CodexResult<Vec<ResponseItem>> {
) -> 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,15 +1632,13 @@ impl Session {
};
let shell = self.user_shell();
let exec_policy = self.services.exec_policy.current();
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),
)?,
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),
)
}
@@ -3188,7 +3186,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()
@@ -3199,7 +3197,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() {
@@ -3215,7 +3213,6 @@ 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,7 +9,6 @@ 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;
@@ -89,7 +88,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: PathUri::from_abs_path(&turn_context.cwd),
cwd: turn_context.cwd.clone(),
workspace_roots: None,
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
@@ -129,7 +128,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: PathUri::from_abs_path(&turn_context.cwd),
cwd: turn_context.cwd.clone(),
workspace_roots: None,
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
@@ -990,7 +989,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: PathUri::from_abs_path(&turn_context.cwd),
cwd: turn_context.cwd.clone(),
workspace_roots: None,
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
@@ -1072,7 +1071,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: PathUri::from_abs_path(&turn_context.cwd),
cwd: turn_context.cwd.clone(),
workspace_roots: None,
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
@@ -1102,7 +1101,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: PathUri::from_abs_path(&turn_context.cwd),
cwd: turn_context.cwd.clone(),
workspace_roots: None,
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
@@ -1224,7 +1223,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: PathUri::from_abs_path(&turn_context.cwd),
cwd: turn_context.cwd.clone(),
workspace_roots: None,
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
@@ -1344,7 +1343,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: PathUri::from_abs_path(&turn_context.cwd),
cwd: turn_context.cwd.clone(),
workspace_roots: None,
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
@@ -1507,7 +1506,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: PathUri::from_abs_path(&turn_context.cwd),
cwd: turn_context.cwd.clone(),
workspace_roots: None,
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
+21 -34
View File
@@ -1874,8 +1874,7 @@ async fn resumed_history_injects_initial_context_on_first_context_update_only()
session
.record_context_updates_and_set_reference_context_item(&turn_context)
.await
.expect("context updates should hydrate");
.await;
let initial_context = session.build_initial_context(&turn_context).await;
expected.extend(initial_context);
let history_after_seed = session.clone_history().await;
@@ -1883,8 +1882,7 @@ async fn resumed_history_injects_initial_context_on_first_context_update_only()
session
.record_context_updates_and_set_reference_context_item(&turn_context)
.await
.expect("context updates should hydrate");
.await;
let history_after_second_seed = session.clone_history().await;
assert_eq!(
history_after_seed.raw_items(),
@@ -2675,7 +2673,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: PathUri::from_abs_path(&turn_context.cwd),
cwd: turn_context.cwd.clone(),
workspace_roots: None,
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
@@ -7292,8 +7290,7 @@ 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
.expect("settings updates should hydrate");
.await;
let environment_update = user_input_texts(&update_items)
.into_iter()
@@ -7363,8 +7360,7 @@ 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
.expect("settings updates should hydrate");
.await;
let environment_update = user_input_texts(&update_items)
.into_iter()
@@ -7392,8 +7388,7 @@ 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
.expect("settings updates should hydrate");
.await;
let user_texts = user_input_texts(&update_items);
assert!(
@@ -7421,8 +7416,7 @@ async fn build_settings_update_items_emits_realtime_start_when_session_becomes_l
Some(&previous_context.to_turn_context_item()),
&current_context,
)
.await
.expect("settings updates should hydrate");
.await;
let developer_texts = developer_input_texts(&update_items);
assert!(
@@ -7450,8 +7444,7 @@ async fn build_settings_update_items_emits_realtime_end_when_session_stops_being
Some(&previous_context.to_turn_context_item()),
&current_context,
)
.await
.expect("settings updates should hydrate");
.await;
let developer_texts = developer_input_texts(&update_items);
assert!(
@@ -7485,8 +7478,7 @@ 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
.expect("settings updates should hydrate");
.await;
let developer_texts = developer_input_texts(&update_items);
assert!(
@@ -8139,18 +8131,20 @@ async fn turn_context_item_uses_turn_context_comp_hash_snapshot() {
}
#[tokio::test]
async fn turn_context_item_stores_primary_environment_cwd_uri() {
async fn turn_context_item_stores_local_cwd() {
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(),
cwd,
environment.shell,
);
assert_eq!(turn_context.to_turn_context_item().cwd, cwd);
#[allow(deprecated)]
let local_cwd = turn_context.cwd.clone();
assert_eq!(turn_context.to_turn_context_item().cwd, local_cwd);
}
#[tokio::test]
@@ -8194,8 +8188,7 @@ 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
.expect("context updates should hydrate");
.await;
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);
@@ -8226,8 +8219,7 @@ 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
.expect("context updates should hydrate");
.await;
{
let mut state = session.state.lock().await;
state.set_reference_context_item(/*item*/ None);
@@ -8241,8 +8233,7 @@ 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
.expect("context updates should hydrate");
.await;
let history = session.clone_history().await;
let mut expected_history = vec![compacted_summary];
@@ -8272,14 +8263,12 @@ 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
.expect("settings updates should hydrate");
.await;
assert_eq!(update_items, Vec::new());
session
.record_context_updates_and_set_reference_context_item(&turn_context)
.await
.expect("context updates should hydrate");
.await;
assert_eq!(
session.clone_history().await.raw_items().to_vec(),
@@ -8326,8 +8315,7 @@ 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
.expect("context updates should hydrate");
.await;
session.ensure_rollout_materialized().await;
session.flush_rollout().await.expect("rollout should flush");
@@ -8411,8 +8399,7 @@ 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
.expect("context updates should hydrate");
.await;
session.ensure_rollout_materialized().await;
session.flush_rollout().await.expect("rollout should flush");
+2 -10
View File
@@ -159,16 +159,8 @@ pub(crate) async fn run_turn(
return None;
}
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;
}
sess.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
.await;
let (injection_items, explicitly_enabled_connectors) =
build_skills_and_plugins(&sess, turn_context.as_ref(), &input, &cancellation_token).await?;
+2 -8
View File
@@ -396,14 +396,8 @@ 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)
});
#[allow(deprecated)]
let cwd = self.cwd.clone();
TurnContextItem {
turn_id: Some(self.sub_id.clone()),
cwd,