mirror of
https://github.com/openai/codex.git
synced 2026-11-07 00:33:29 +00: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:
1 parent
a34da3b295
commit
4c79527e31
25 files changed
+457
-185
No files matched your search
@@ -458,7 +458,7 @@ impl CodexThread {
|
|||||||
self.codex
|
self.codex
|
||||||
.session
|
.session
|
||||||
.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
|
.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
|
||||||
.await;
|
.await?;
|
||||||
}
|
}
|
||||||
self.codex
|
self.codex
|
||||||
.session
|
.session
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use codex_protocol::permissions::FileSystemSpecialPath;
|
|||||||
use codex_protocol::protocol::TurnContextItem;
|
use codex_protocol::protocol::TurnContextItem;
|
||||||
use codex_protocol::protocol::TurnContextNetworkItem;
|
use codex_protocol::protocol::TurnContextNetworkItem;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
|
use codex_utils_path_uri::PathUri;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -28,12 +29,12 @@ pub(crate) struct EnvironmentContext {
|
|||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub(crate) struct EnvironmentContextEnvironment {
|
pub(crate) struct EnvironmentContextEnvironment {
|
||||||
pub(crate) id: String,
|
pub(crate) id: String,
|
||||||
pub(crate) cwd: AbsolutePathBuf,
|
pub(crate) cwd: PathUri,
|
||||||
pub(crate) shell: String,
|
pub(crate) shell: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EnvironmentContextEnvironment {
|
impl EnvironmentContextEnvironment {
|
||||||
fn legacy(cwd: AbsolutePathBuf, shell: String) -> Self {
|
fn legacy(cwd: PathUri, shell: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: String::new(),
|
id: String::new(),
|
||||||
cwd,
|
cwd,
|
||||||
@@ -44,18 +45,14 @@ impl EnvironmentContextEnvironment {
|
|||||||
fn from_turn_environments(environments: &[TurnEnvironment], shell: &Shell) -> Vec<Self> {
|
fn from_turn_environments(environments: &[TurnEnvironment], shell: &Shell) -> Vec<Self> {
|
||||||
environments
|
environments
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|environment| {
|
.map(|environment| Self {
|
||||||
// TODO(anp): Migrate EnvironmentContextEnvironment to PathUri so foreign
|
id: environment.environment_id.clone(),
|
||||||
// environments remain visible in model context.
|
cwd: environment.cwd().clone(),
|
||||||
Some(Self {
|
shell: environment
|
||||||
id: environment.environment_id.clone(),
|
.shell
|
||||||
cwd: environment.cwd().to_abs_path().ok()?,
|
.as_ref()
|
||||||
shell: environment
|
.map(|shell| shell.name().to_string())
|
||||||
.shell
|
.unwrap_or_else(|| shell.name().to_string()),
|
||||||
.as_ref()
|
|
||||||
.map(|shell| shell.name().to_string())
|
|
||||||
.unwrap_or_else(|| shell.name().to_string()),
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -383,12 +380,12 @@ impl EnvironmentContext {
|
|||||||
pub(crate) fn diff_from_turn_context_item(
|
pub(crate) fn diff_from_turn_context_item(
|
||||||
before: &TurnContextItem,
|
before: &TurnContextItem,
|
||||||
after: &EnvironmentContext,
|
after: &EnvironmentContext,
|
||||||
) -> Self {
|
) -> std::io::Result<Self> {
|
||||||
let before_network = Self::network_from_turn_context_item(before);
|
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 {
|
let environments = match &after.environments {
|
||||||
EnvironmentContextEnvironments::Single(environment) => {
|
EnvironmentContextEnvironments::Single(environment) => {
|
||||||
if before.cwd.as_path() != environment.cwd.as_path() {
|
if before.cwd != environment.cwd {
|
||||||
EnvironmentContextEnvironments::Single(EnvironmentContextEnvironment::legacy(
|
EnvironmentContextEnvironments::Single(EnvironmentContextEnvironment::legacy(
|
||||||
environment.cwd.clone(),
|
environment.cwd.clone(),
|
||||||
environment.shell.clone(),
|
environment.shell.clone(),
|
||||||
@@ -412,14 +409,14 @@ impl EnvironmentContext {
|
|||||||
} else {
|
} else {
|
||||||
before_filesystem
|
before_filesystem
|
||||||
};
|
};
|
||||||
EnvironmentContext::new_with_environments(
|
Ok(EnvironmentContext::new_with_environments(
|
||||||
environments,
|
environments,
|
||||||
after.current_date.clone(),
|
after.current_date.clone(),
|
||||||
after.timezone.clone(),
|
after.timezone.clone(),
|
||||||
network,
|
network,
|
||||||
filesystem,
|
filesystem,
|
||||||
/*subagents*/ None,
|
/*subagents*/ None,
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn from_turn_context(turn_context: &TurnContext, shell: &Shell) -> Self {
|
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(
|
pub(crate) fn from_turn_context_item(
|
||||||
turn_context_item: &TurnContextItem,
|
turn_context_item: &TurnContextItem,
|
||||||
shell: String,
|
shell: String,
|
||||||
) -> Self {
|
) -> std::io::Result<Self> {
|
||||||
let cwd = match AbsolutePathBuf::try_from(turn_context_item.cwd.clone()) {
|
Ok(Self::new_with_environments(
|
||||||
Ok(cwd) => cwd,
|
|
||||||
Err(_) => AbsolutePathBuf::resolve_path_against_base(&turn_context_item.cwd, "/"),
|
|
||||||
};
|
|
||||||
Self::new_with_environments(
|
|
||||||
EnvironmentContextEnvironments::from_vec(vec![EnvironmentContextEnvironment::legacy(
|
EnvironmentContextEnvironments::from_vec(vec![EnvironmentContextEnvironment::legacy(
|
||||||
cwd, shell,
|
turn_context_item.cwd.clone(),
|
||||||
|
shell,
|
||||||
)]),
|
)]),
|
||||||
turn_context_item.current_date.clone(),
|
turn_context_item.current_date.clone(),
|
||||||
turn_context_item.timezone.clone(),
|
turn_context_item.timezone.clone(),
|
||||||
Self::network_from_turn_context_item(turn_context_item),
|
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,
|
/*subagents*/ None,
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn with_subagents(mut self, subagents: String) -> Self {
|
pub(crate) fn with_subagents(mut self, subagents: String) -> Self {
|
||||||
@@ -504,11 +498,11 @@ impl EnvironmentContext {
|
|||||||
|
|
||||||
fn filesystem_from_turn_context_item(
|
fn filesystem_from_turn_context_item(
|
||||||
turn_context_item: &TurnContextItem,
|
turn_context_item: &TurnContextItem,
|
||||||
) -> Option<FileSystemContext> {
|
) -> std::io::Result<Option<FileSystemContext>> {
|
||||||
Some(FileSystemContext::from_permission_profile(
|
Ok(Some(FileSystemContext::from_permission_profile(
|
||||||
&turn_context_item.permission_profile(),
|
&turn_context_item.permission_profile()?,
|
||||||
&workspace_roots_from_turn_context_item(turn_context_item),
|
&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
|
// Older rollout items did not persist workspace roots. Fall back to the
|
||||||
// legacy cwd binding only when reconstructing that historical context.
|
// 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],
|
Ok(cwd) => vec![cwd],
|
||||||
Err(_) => Vec::new(),
|
Err(_) => Vec::new(),
|
||||||
}
|
}
|
||||||
@@ -547,20 +541,16 @@ impl ContextualUserFragment for EnvironmentContext {
|
|||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
match &self.environments {
|
match &self.environments {
|
||||||
EnvironmentContextEnvironments::Single(environment) => {
|
EnvironmentContextEnvironments::Single(environment) => {
|
||||||
lines.push(format!(
|
let cwd = environment.cwd.inferred_native_path_string();
|
||||||
" <cwd>{}</cwd>",
|
lines.push(format!(" <cwd>{cwd}</cwd>"));
|
||||||
environment.cwd.to_string_lossy()
|
|
||||||
));
|
|
||||||
lines.push(format!(" <shell>{}</shell>", environment.shell));
|
lines.push(format!(" <shell>{}</shell>", environment.shell));
|
||||||
}
|
}
|
||||||
EnvironmentContextEnvironments::Multiple(environments) => {
|
EnvironmentContextEnvironments::Multiple(environments) => {
|
||||||
lines.push(" <environments>".to_string());
|
lines.push(" <environments>".to_string());
|
||||||
for environment in environments {
|
for environment in environments {
|
||||||
lines.push(format!(" <environment id=\"{}\">", environment.id));
|
lines.push(format!(" <environment id=\"{}\">", environment.id));
|
||||||
lines.push(format!(
|
let cwd = environment.cwd.inferred_native_path_string();
|
||||||
" <cwd>{}</cwd>",
|
lines.push(format!(" <cwd>{cwd}</cwd>"));
|
||||||
environment.cwd.to_string_lossy()
|
|
||||||
));
|
|
||||||
lines.push(format!(" <shell>{}</shell>", environment.shell));
|
lines.push(format!(" <shell>{}</shell>", environment.shell));
|
||||||
lines.push(" </environment>".to_string());
|
lines.push(" </environment>".to_string());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ fn serialize_workspace_write_environment_context() {
|
|||||||
let context = EnvironmentContext::new(
|
let context = EnvironmentContext::new(
|
||||||
vec![EnvironmentContextEnvironment {
|
vec![EnvironmentContextEnvironment {
|
||||||
id: "local".to_string(),
|
id: "local".to_string(),
|
||||||
cwd: cwd.abs(),
|
cwd: PathUri::from_abs_path(&cwd.abs()),
|
||||||
shell: fake_shell_name(),
|
shell: fake_shell_name(),
|
||||||
}],
|
}],
|
||||||
Some("2026-02-26".to_string()),
|
Some("2026-02-26".to_string()),
|
||||||
@@ -58,6 +58,29 @@ fn serialize_workspace_write_environment_context() {
|
|||||||
assert_eq!(context.render(), expected);
|
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]
|
#[test]
|
||||||
fn serialize_environment_context_with_network() {
|
fn serialize_environment_context_with_network() {
|
||||||
let network = NetworkContext::new(
|
let network = NetworkContext::new(
|
||||||
@@ -67,7 +90,7 @@ fn serialize_environment_context_with_network() {
|
|||||||
let context = EnvironmentContext::new(
|
let context = EnvironmentContext::new(
|
||||||
vec![EnvironmentContextEnvironment {
|
vec![EnvironmentContextEnvironment {
|
||||||
id: "local".to_string(),
|
id: "local".to_string(),
|
||||||
cwd: test_path_buf("/repo").abs(),
|
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||||
shell: fake_shell_name(),
|
shell: fake_shell_name(),
|
||||||
}],
|
}],
|
||||||
Some("2026-02-26".to_string()),
|
Some("2026-02-26".to_string()),
|
||||||
@@ -129,7 +152,7 @@ fn serialize_environment_context_with_full_filesystem_profile() {
|
|||||||
let mut context = EnvironmentContext::new(
|
let mut context = EnvironmentContext::new(
|
||||||
vec![EnvironmentContextEnvironment {
|
vec![EnvironmentContextEnvironment {
|
||||||
id: "local".to_string(),
|
id: "local".to_string(),
|
||||||
cwd: test_path_buf("/repo").abs(),
|
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||||
shell: fake_shell_name(),
|
shell: fake_shell_name(),
|
||||||
}],
|
}],
|
||||||
/*current_date*/ None,
|
/*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 repo_private = repo.join("private");
|
||||||
let item = TurnContextItem {
|
let item = TurnContextItem {
|
||||||
turn_id: None,
|
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()]),
|
workspace_roots: Some(vec![repo.clone(), other_repo.clone()]),
|
||||||
current_date: None,
|
current_date: None,
|
||||||
timezone: 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,
|
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!(
|
assert!(
|
||||||
context.contains(&format!(
|
context.contains(&format!(
|
||||||
@@ -234,7 +259,7 @@ fn equals_except_shell_compares_cwd() {
|
|||||||
let context1 = EnvironmentContext::new(
|
let context1 = EnvironmentContext::new(
|
||||||
vec![EnvironmentContextEnvironment {
|
vec![EnvironmentContextEnvironment {
|
||||||
id: "local".to_string(),
|
id: "local".to_string(),
|
||||||
cwd: test_abs_path("/repo"),
|
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||||
shell: fake_shell_name(),
|
shell: fake_shell_name(),
|
||||||
}],
|
}],
|
||||||
/*current_date*/ None,
|
/*current_date*/ None,
|
||||||
@@ -245,7 +270,7 @@ fn equals_except_shell_compares_cwd() {
|
|||||||
let context2 = EnvironmentContext::new(
|
let context2 = EnvironmentContext::new(
|
||||||
vec![EnvironmentContextEnvironment {
|
vec![EnvironmentContextEnvironment {
|
||||||
id: "local".to_string(),
|
id: "local".to_string(),
|
||||||
cwd: test_abs_path("/repo"),
|
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||||
shell: fake_shell_name(),
|
shell: fake_shell_name(),
|
||||||
}],
|
}],
|
||||||
/*current_date*/ None,
|
/*current_date*/ None,
|
||||||
@@ -261,7 +286,7 @@ fn equals_except_shell_compares_cwd_differences() {
|
|||||||
let context1 = EnvironmentContext::new(
|
let context1 = EnvironmentContext::new(
|
||||||
vec![EnvironmentContextEnvironment {
|
vec![EnvironmentContextEnvironment {
|
||||||
id: "local".to_string(),
|
id: "local".to_string(),
|
||||||
cwd: test_abs_path("/repo1"),
|
cwd: PathUri::from_abs_path(&test_abs_path("/repo1")),
|
||||||
shell: fake_shell_name(),
|
shell: fake_shell_name(),
|
||||||
}],
|
}],
|
||||||
/*current_date*/ None,
|
/*current_date*/ None,
|
||||||
@@ -272,7 +297,7 @@ fn equals_except_shell_compares_cwd_differences() {
|
|||||||
let context2 = EnvironmentContext::new(
|
let context2 = EnvironmentContext::new(
|
||||||
vec![EnvironmentContextEnvironment {
|
vec![EnvironmentContextEnvironment {
|
||||||
id: "local".to_string(),
|
id: "local".to_string(),
|
||||||
cwd: test_abs_path("/repo2"),
|
cwd: PathUri::from_abs_path(&test_abs_path("/repo2")),
|
||||||
shell: fake_shell_name(),
|
shell: fake_shell_name(),
|
||||||
}],
|
}],
|
||||||
/*current_date*/ None,
|
/*current_date*/ None,
|
||||||
@@ -289,7 +314,7 @@ fn equals_except_shell_ignores_shell() {
|
|||||||
let context1 = EnvironmentContext::new(
|
let context1 = EnvironmentContext::new(
|
||||||
vec![EnvironmentContextEnvironment {
|
vec![EnvironmentContextEnvironment {
|
||||||
id: "local".to_string(),
|
id: "local".to_string(),
|
||||||
cwd: test_abs_path("/repo"),
|
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||||
shell: "bash".to_string(),
|
shell: "bash".to_string(),
|
||||||
}],
|
}],
|
||||||
/*current_date*/ None,
|
/*current_date*/ None,
|
||||||
@@ -300,7 +325,7 @@ fn equals_except_shell_ignores_shell() {
|
|||||||
let context2 = EnvironmentContext::new(
|
let context2 = EnvironmentContext::new(
|
||||||
vec![EnvironmentContextEnvironment {
|
vec![EnvironmentContextEnvironment {
|
||||||
id: "other".to_string(),
|
id: "other".to_string(),
|
||||||
cwd: test_abs_path("/repo"),
|
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||||
shell: "zsh".to_string(),
|
shell: "zsh".to_string(),
|
||||||
}],
|
}],
|
||||||
/*current_date*/ None,
|
/*current_date*/ None,
|
||||||
@@ -317,7 +342,7 @@ fn serialize_environment_context_with_subagents() {
|
|||||||
let context = EnvironmentContext::new(
|
let context = EnvironmentContext::new(
|
||||||
vec![EnvironmentContextEnvironment {
|
vec![EnvironmentContextEnvironment {
|
||||||
id: "local".to_string(),
|
id: "local".to_string(),
|
||||||
cwd: test_path_buf("/repo").abs(),
|
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||||
shell: fake_shell_name(),
|
shell: fake_shell_name(),
|
||||||
}],
|
}],
|
||||||
Some("2026-02-26".to_string()),
|
Some("2026-02-26".to_string()),
|
||||||
@@ -351,12 +376,12 @@ fn serialize_environment_context_with_multiple_selected_environments() {
|
|||||||
vec![
|
vec![
|
||||||
EnvironmentContextEnvironment {
|
EnvironmentContextEnvironment {
|
||||||
id: "local".to_string(),
|
id: "local".to_string(),
|
||||||
cwd: local_cwd.abs(),
|
cwd: PathUri::from_abs_path(&local_cwd.abs()),
|
||||||
shell: "bash".to_string(),
|
shell: "bash".to_string(),
|
||||||
},
|
},
|
||||||
EnvironmentContextEnvironment {
|
EnvironmentContextEnvironment {
|
||||||
id: "remote".to_string(),
|
id: "remote".to_string(),
|
||||||
cwd: remote_cwd.abs(),
|
cwd: PathUri::from_abs_path(&remote_cwd.abs()),
|
||||||
shell: "bash".to_string(),
|
shell: "bash".to_string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -396,12 +421,12 @@ fn serialize_environment_context_prefers_environment_shell_when_present() {
|
|||||||
vec![
|
vec![
|
||||||
EnvironmentContextEnvironment {
|
EnvironmentContextEnvironment {
|
||||||
id: "local".to_string(),
|
id: "local".to_string(),
|
||||||
cwd: local_cwd.abs(),
|
cwd: PathUri::from_abs_path(&local_cwd.abs()),
|
||||||
shell: "powershell".to_string(),
|
shell: "powershell".to_string(),
|
||||||
},
|
},
|
||||||
EnvironmentContextEnvironment {
|
EnvironmentContextEnvironment {
|
||||||
id: "remote".to_string(),
|
id: "remote".to_string(),
|
||||||
cwd: remote_cwd.abs(),
|
cwd: PathUri::from_abs_path(&remote_cwd.abs()),
|
||||||
shell: "cmd".to_string(),
|
shell: "cmd".to_string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ use codex_protocol::protocol::SandboxPolicy;
|
|||||||
use codex_protocol::protocol::TurnContextItem;
|
use codex_protocol::protocol::TurnContextItem;
|
||||||
use codex_utils_output_truncation::TruncationPolicy;
|
use codex_utils_output_truncation::TruncationPolicy;
|
||||||
use codex_utils_output_truncation::truncate_text;
|
use codex_utils_output_truncation::truncate_text;
|
||||||
|
use codex_utils_path_uri::PathUri;
|
||||||
use image::ImageBuffer;
|
use image::ImageBuffer;
|
||||||
use image::ImageFormat;
|
use image::ImageFormat;
|
||||||
use image::Luma;
|
use image::Luma;
|
||||||
use image::Rgba;
|
use image::Rgba;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use regex_lite::Regex;
|
use regex_lite::Regex;
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
const EXEC_FORMAT_MAX_BYTES: usize = 10_000;
|
const EXEC_FORMAT_MAX_BYTES: usize = 10_000;
|
||||||
const EXEC_FORMAT_MAX_TOKENS: usize = 2_500;
|
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 {
|
fn reference_context_item() -> TurnContextItem {
|
||||||
TurnContextItem {
|
TurnContextItem {
|
||||||
turn_id: Some("reference-turn".to_string()),
|
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,
|
workspace_roots: None,
|
||||||
current_date: Some("2026-03-23".to_string()),
|
current_date: Some("2026-03-23".to_string()),
|
||||||
timezone: Some("America/Los_Angeles".to_string()),
|
timezone: Some("America/Los_Angeles".to_string()),
|
||||||
|
|||||||
@@ -22,40 +22,44 @@ fn build_environment_update_item(
|
|||||||
previous: Option<&TurnContextItem>,
|
previous: Option<&TurnContextItem>,
|
||||||
next: &TurnContext,
|
next: &TurnContext,
|
||||||
shell: &Shell,
|
shell: &Shell,
|
||||||
) -> Option<ResponseItem> {
|
) -> std::io::Result<Option<ResponseItem>> {
|
||||||
if !next.config.include_environment_context {
|
if !next.config.include_environment_context {
|
||||||
return None;
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let prev = previous?;
|
let Some(prev) = previous else {
|
||||||
let prev_context = EnvironmentContext::from_turn_context_item(prev, shell.name().to_string());
|
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);
|
let next_context = EnvironmentContext::from_turn_context(next, shell);
|
||||||
if prev_context.equals_except_shell(&next_context) {
|
if prev_context.equals_except_shell(&next_context) {
|
||||||
return None;
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(ContextualUserFragment::into(
|
Ok(Some(ContextualUserFragment::into(
|
||||||
EnvironmentContext::diff_from_turn_context_item(prev, &next_context),
|
EnvironmentContext::diff_from_turn_context_item(prev, &next_context)?,
|
||||||
))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_permissions_update_item(
|
fn build_permissions_update_item(
|
||||||
previous: Option<&TurnContextItem>,
|
previous: Option<&TurnContextItem>,
|
||||||
next: &TurnContext,
|
next: &TurnContext,
|
||||||
exec_policy: &Policy,
|
exec_policy: &Policy,
|
||||||
) -> Option<String> {
|
) -> std::io::Result<Option<String>> {
|
||||||
if !next.config.include_permissions_instructions {
|
if !next.config.include_permissions_instructions {
|
||||||
return None;
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let prev = previous?;
|
let Some(prev) = previous else {
|
||||||
if prev.permission_profile() == next.permission_profile()
|
return Ok(None);
|
||||||
|
};
|
||||||
|
if prev.permission_profile()? == next.permission_profile()
|
||||||
&& prev.approval_policy == next.approval_policy.value()
|
&& prev.approval_policy == next.approval_policy.value()
|
||||||
{
|
{
|
||||||
return None;
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(
|
Ok(Some(
|
||||||
PermissionsInstructions::from_permission_profile(
|
PermissionsInstructions::from_permission_profile(
|
||||||
&next.permission_profile,
|
&next.permission_profile,
|
||||||
next.approval_policy.value(),
|
next.approval_policy.value(),
|
||||||
@@ -67,7 +71,7 @@ fn build_permissions_update_item(
|
|||||||
next.features.enabled(Feature::RequestPermissionsTool),
|
next.features.enabled(Feature::RequestPermissionsTool),
|
||||||
)
|
)
|
||||||
.render(),
|
.render(),
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_collaboration_mode_update_item(
|
fn build_collaboration_mode_update_item(
|
||||||
@@ -214,17 +218,17 @@ pub(crate) fn build_settings_update_items(
|
|||||||
shell: &Shell,
|
shell: &Shell,
|
||||||
exec_policy: &Policy,
|
exec_policy: &Policy,
|
||||||
personality_feature_enabled: bool,
|
personality_feature_enabled: bool,
|
||||||
) -> Vec<ResponseItem> {
|
) -> std::io::Result<Vec<ResponseItem>> {
|
||||||
// TODO(ccunningham): build_settings_update_items still does not cover every
|
// TODO(ccunningham): build_settings_update_items still does not cover every
|
||||||
// model-visible item emitted by build_initial_context. Persist the remaining
|
// model-visible item emitted by build_initial_context. Persist the remaining
|
||||||
// inputs or add explicit replay events so fork/resume can diff everything
|
// inputs or add explicit replay events so fork/resume can diff everything
|
||||||
// deterministically.
|
// 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 = [
|
let developer_update_sections = [
|
||||||
// Keep model-switch instructions first so model-specific guidance is read before
|
// Keep model-switch instructions first so model-specific guidance is read before
|
||||||
// any other context diffs on this turn.
|
// any other context diffs on this turn.
|
||||||
build_model_instructions_update_item(previous_turn_settings, next),
|
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_collaboration_mode_update_item(previous, next),
|
||||||
build_realtime_update_item(previous, previous_turn_settings, next),
|
build_realtime_update_item(previous, previous_turn_settings, next),
|
||||||
build_personality_update_item(previous, next, personality_feature_enabled),
|
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 {
|
if let Some(contextual_user_message) = contextual_user_message {
|
||||||
items.push(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>> {
|
) -> CodexResult<Vec<ResponseItem>> {
|
||||||
let turn_context = sess.new_default_turn().await;
|
let turn_context = sess.new_default_turn().await;
|
||||||
sess.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
|
sess.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
|
||||||
.await;
|
.await?;
|
||||||
|
|
||||||
if !input.is_empty() {
|
if !input.is_empty() {
|
||||||
let response_item = sess.response_item_from_user_input(turn_context.as_ref(), input);
|
let response_item = sess.response_item_from_user_input(turn_context.as_ref(), input);
|
||||||
|
|||||||
@@ -1621,7 +1621,7 @@ impl Session {
|
|||||||
&self,
|
&self,
|
||||||
reference_context_item: Option<&TurnContextItem>,
|
reference_context_item: Option<&TurnContextItem>,
|
||||||
current_context: &TurnContext,
|
current_context: &TurnContext,
|
||||||
) -> Vec<ResponseItem> {
|
) -> CodexResult<Vec<ResponseItem>> {
|
||||||
// TODO: Make context updates a pure diff of persisted previous/current TurnContextItem
|
// TODO: Make context updates a pure diff of persisted previous/current TurnContextItem
|
||||||
// state so replay/backtracking is deterministic. Runtime inputs that affect model-visible
|
// state so replay/backtracking is deterministic. Runtime inputs that affect model-visible
|
||||||
// context (shell, exec policy, feature gates, previous-turn bridge) should be persisted
|
// context (shell, exec policy, feature gates, previous-turn bridge) should be persisted
|
||||||
@@ -1632,13 +1632,15 @@ impl Session {
|
|||||||
};
|
};
|
||||||
let shell = self.user_shell();
|
let shell = self.user_shell();
|
||||||
let exec_policy = self.services.exec_policy.current();
|
let exec_policy = self.services.exec_policy.current();
|
||||||
crate::context_manager::updates::build_settings_update_items(
|
Ok(
|
||||||
reference_context_item,
|
crate::context_manager::updates::build_settings_update_items(
|
||||||
previous_turn_settings.as_ref(),
|
reference_context_item,
|
||||||
current_context,
|
previous_turn_settings.as_ref(),
|
||||||
shell.as_ref(),
|
current_context,
|
||||||
exec_policy.as_ref(),
|
shell.as_ref(),
|
||||||
self.features.enabled(Feature::Personality),
|
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(
|
pub(crate) async fn record_context_updates_and_set_reference_context_item(
|
||||||
&self,
|
&self,
|
||||||
turn_context: &TurnContext,
|
turn_context: &TurnContext,
|
||||||
) {
|
) -> CodexResult<()> {
|
||||||
let reference_context_item = {
|
let reference_context_item = {
|
||||||
let state = self.state.lock().await;
|
let state = self.state.lock().await;
|
||||||
state.reference_context_item()
|
state.reference_context_item()
|
||||||
@@ -3197,7 +3199,7 @@ impl Session {
|
|||||||
} else {
|
} else {
|
||||||
// Steady-state path: append only context diffs to minimize token overhead.
|
// Steady-state path: append only context diffs to minimize token overhead.
|
||||||
self.build_settings_update_items(reference_context_item.as_ref(), turn_context)
|
self.build_settings_update_items(reference_context_item.as_ref(), turn_context)
|
||||||
.await
|
.await?
|
||||||
};
|
};
|
||||||
let turn_context_item = turn_context.to_turn_context_item();
|
let turn_context_item = turn_context.to_turn_context_item();
|
||||||
if !context_items.is_empty() {
|
if !context_items.is_empty() {
|
||||||
@@ -3213,6 +3215,7 @@ impl Session {
|
|||||||
// context items. This keeps later runtime diffing aligned with the current turn state.
|
// context items. This keeps later runtime diffing aligned with the current turn state.
|
||||||
let mut state = self.state.lock().await;
|
let mut state = self.state.lock().await;
|
||||||
state.set_reference_context_item(Some(turn_context_item));
|
state.set_reference_context_item(Some(turn_context_item));
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn update_token_usage_info(
|
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::InitialHistory;
|
||||||
use codex_protocol::protocol::InterAgentCommunication;
|
use codex_protocol::protocol::InterAgentCommunication;
|
||||||
use codex_protocol::protocol::ResumedHistory;
|
use codex_protocol::protocol::ResumedHistory;
|
||||||
|
use codex_utils_path_uri::PathUri;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use std::path::PathBuf;
|
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 {
|
let previous_context_item = TurnContextItem {
|
||||||
turn_id: Some(turn_context.sub_id.clone()),
|
turn_id: Some(turn_context.sub_id.clone()),
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
cwd: turn_context.cwd.to_path_buf(),
|
cwd: PathUri::from_abs_path(&turn_context.cwd),
|
||||||
workspace_roots: None,
|
workspace_roots: None,
|
||||||
current_date: turn_context.current_date.clone(),
|
current_date: turn_context.current_date.clone(),
|
||||||
timezone: turn_context.timezone.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 {
|
let mut previous_context_item = TurnContextItem {
|
||||||
turn_id: Some(turn_context.sub_id.clone()),
|
turn_id: Some(turn_context.sub_id.clone()),
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
cwd: turn_context.cwd.to_path_buf(),
|
cwd: PathUri::from_abs_path(&turn_context.cwd),
|
||||||
workspace_roots: None,
|
workspace_roots: None,
|
||||||
current_date: turn_context.current_date.clone(),
|
current_date: turn_context.current_date.clone(),
|
||||||
timezone: turn_context.timezone.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 {
|
let previous_context_item = TurnContextItem {
|
||||||
turn_id: Some(turn_context.sub_id.clone()),
|
turn_id: Some(turn_context.sub_id.clone()),
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
cwd: turn_context.cwd.to_path_buf(),
|
cwd: PathUri::from_abs_path(&turn_context.cwd),
|
||||||
workspace_roots: None,
|
workspace_roots: None,
|
||||||
current_date: turn_context.current_date.clone(),
|
current_date: turn_context.current_date.clone(),
|
||||||
timezone: turn_context.timezone.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 {
|
serde_json::to_value(Some(TurnContextItem {
|
||||||
turn_id: Some(turn_context.sub_id.clone()),
|
turn_id: Some(turn_context.sub_id.clone()),
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
cwd: turn_context.cwd.to_path_buf(),
|
cwd: PathUri::from_abs_path(&turn_context.cwd),
|
||||||
workspace_roots: None,
|
workspace_roots: None,
|
||||||
current_date: turn_context.current_date.clone(),
|
current_date: turn_context.current_date.clone(),
|
||||||
timezone: turn_context.timezone.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 {
|
let previous_context_item = TurnContextItem {
|
||||||
turn_id: Some(turn_context.sub_id.clone()),
|
turn_id: Some(turn_context.sub_id.clone()),
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
cwd: turn_context.cwd.to_path_buf(),
|
cwd: PathUri::from_abs_path(&turn_context.cwd),
|
||||||
workspace_roots: None,
|
workspace_roots: None,
|
||||||
current_date: turn_context.current_date.clone(),
|
current_date: turn_context.current_date.clone(),
|
||||||
timezone: turn_context.timezone.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 {
|
let current_context_item = TurnContextItem {
|
||||||
turn_id: Some(current_turn_id.clone()),
|
turn_id: Some(current_turn_id.clone()),
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
cwd: turn_context.cwd.to_path_buf(),
|
cwd: PathUri::from_abs_path(&turn_context.cwd),
|
||||||
workspace_roots: None,
|
workspace_roots: None,
|
||||||
current_date: turn_context.current_date.clone(),
|
current_date: turn_context.current_date.clone(),
|
||||||
timezone: turn_context.timezone.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 {
|
let previous_context_item = TurnContextItem {
|
||||||
turn_id: Some(turn_context.sub_id.clone()),
|
turn_id: Some(turn_context.sub_id.clone()),
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
cwd: turn_context.cwd.to_path_buf(),
|
cwd: PathUri::from_abs_path(&turn_context.cwd),
|
||||||
workspace_roots: None,
|
workspace_roots: None,
|
||||||
current_date: turn_context.current_date.clone(),
|
current_date: turn_context.current_date.clone(),
|
||||||
timezone: turn_context.timezone.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 {
|
let previous_context_item = TurnContextItem {
|
||||||
turn_id: Some(turn_context.sub_id.clone()),
|
turn_id: Some(turn_context.sub_id.clone()),
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
cwd: turn_context.cwd.to_path_buf(),
|
cwd: PathUri::from_abs_path(&turn_context.cwd),
|
||||||
workspace_roots: None,
|
workspace_roots: None,
|
||||||
current_date: turn_context.current_date.clone(),
|
current_date: turn_context.current_date.clone(),
|
||||||
timezone: turn_context.timezone.clone(),
|
timezone: turn_context.timezone.clone(),
|
||||||
|
|||||||
@@ -1874,7 +1874,8 @@ async fn resumed_history_injects_initial_context_on_first_context_update_only()
|
|||||||
|
|
||||||
session
|
session
|
||||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
.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;
|
let initial_context = session.build_initial_context(&turn_context).await;
|
||||||
expected.extend(initial_context);
|
expected.extend(initial_context);
|
||||||
let history_after_seed = session.clone_history().await;
|
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
|
session
|
||||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
.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;
|
let history_after_second_seed = session.clone_history().await;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
history_after_seed.raw_items(),
|
history_after_seed.raw_items(),
|
||||||
@@ -2673,7 +2675,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
|
|||||||
let previous_context_item = TurnContextItem {
|
let previous_context_item = TurnContextItem {
|
||||||
turn_id: Some(turn_context.sub_id.clone()),
|
turn_id: Some(turn_context.sub_id.clone()),
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
cwd: turn_context.cwd.to_path_buf(),
|
cwd: PathUri::from_abs_path(&turn_context.cwd),
|
||||||
workspace_roots: None,
|
workspace_roots: None,
|
||||||
current_date: turn_context.current_date.clone(),
|
current_date: turn_context.current_date.clone(),
|
||||||
timezone: turn_context.timezone.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 reference_context_item = previous_context.to_turn_context_item();
|
||||||
let update_items = session
|
let update_items = session
|
||||||
.build_settings_update_items(Some(&reference_context_item), ¤t_context)
|
.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)
|
let environment_update = user_input_texts(&update_items)
|
||||||
.into_iter()
|
.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 reference_context_item = previous_context.to_turn_context_item();
|
||||||
let update_items = session
|
let update_items = session
|
||||||
.build_settings_update_items(Some(&reference_context_item), ¤t_context)
|
.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)
|
let environment_update = user_input_texts(&update_items)
|
||||||
.into_iter()
|
.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 reference_context_item = previous_context.to_turn_context_item();
|
||||||
let update_items = session
|
let update_items = session
|
||||||
.build_settings_update_items(Some(&reference_context_item), ¤t_context)
|
.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);
|
let user_texts = user_input_texts(&update_items);
|
||||||
assert!(
|
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()),
|
Some(&previous_context.to_turn_context_item()),
|
||||||
¤t_context,
|
¤t_context,
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
|
.expect("settings updates should hydrate");
|
||||||
|
|
||||||
let developer_texts = developer_input_texts(&update_items);
|
let developer_texts = developer_input_texts(&update_items);
|
||||||
assert!(
|
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()),
|
Some(&previous_context.to_turn_context_item()),
|
||||||
¤t_context,
|
¤t_context,
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
|
.expect("settings updates should hydrate");
|
||||||
|
|
||||||
let developer_texts = developer_input_texts(&update_items);
|
let developer_texts = developer_input_texts(&update_items);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -7478,7 +7485,8 @@ async fn build_settings_update_items_uses_previous_turn_settings_for_realtime_en
|
|||||||
.await;
|
.await;
|
||||||
let update_items = session
|
let update_items = session
|
||||||
.build_settings_update_items(Some(&previous_context_item), ¤t_context)
|
.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);
|
let developer_texts = developer_input_texts(&update_items);
|
||||||
assert!(
|
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]
|
#[tokio::test]
|
||||||
async fn turn_context_item_omits_legacy_equivalent_file_system_sandbox_policy() {
|
async fn turn_context_item_omits_legacy_equivalent_file_system_sandbox_policy() {
|
||||||
let (_session, turn_context) = make_session_and_context().await;
|
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;
|
let (session, turn_context) = make_session_and_context().await;
|
||||||
session
|
session
|
||||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
.record_context_updates_and_set_reference_context_item(&turn_context)
|
||||||
.await;
|
.await
|
||||||
|
.expect("context updates should hydrate");
|
||||||
let history = session.clone_history().await;
|
let history = session.clone_history().await;
|
||||||
let initial_context = session.build_initial_context(&turn_context).await;
|
let initial_context = session.build_initial_context(&turn_context).await;
|
||||||
assert_eq!(history.raw_items().to_vec(), initial_context);
|
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;
|
.await;
|
||||||
session
|
session
|
||||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
.record_context_updates_and_set_reference_context_item(&turn_context)
|
||||||
.await;
|
.await
|
||||||
|
.expect("context updates should hydrate");
|
||||||
{
|
{
|
||||||
let mut state = session.state.lock().await;
|
let mut state = session.state.lock().await;
|
||||||
state.set_reference_context_item(/*item*/ None);
|
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
|
session
|
||||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
.record_context_updates_and_set_reference_context_item(&turn_context)
|
||||||
.await;
|
.await
|
||||||
|
.expect("context updates should hydrate");
|
||||||
|
|
||||||
let history = session.clone_history().await;
|
let history = session.clone_history().await;
|
||||||
let mut expected_history = vec![compacted_summary];
|
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
|
let update_items = session
|
||||||
.build_settings_update_items(Some(&previous_context_item), &turn_context)
|
.build_settings_update_items(Some(&previous_context_item), &turn_context)
|
||||||
.await;
|
.await
|
||||||
|
.expect("settings updates should hydrate");
|
||||||
assert_eq!(update_items, Vec::new());
|
assert_eq!(update_items, Vec::new());
|
||||||
|
|
||||||
session
|
session
|
||||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
.record_context_updates_and_set_reference_context_item(&turn_context)
|
||||||
.await;
|
.await
|
||||||
|
.expect("context updates should hydrate");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
session.clone_history().await.raw_items().to_vec(),
|
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
|
session
|
||||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
.record_context_updates_and_set_reference_context_item(&turn_context)
|
||||||
.await;
|
.await
|
||||||
|
.expect("context updates should hydrate");
|
||||||
session.ensure_rollout_materialized().await;
|
session.ensure_rollout_materialized().await;
|
||||||
session.flush_rollout().await.expect("rollout should flush");
|
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;
|
.await;
|
||||||
session
|
session
|
||||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
.record_context_updates_and_set_reference_context_item(&turn_context)
|
||||||
.await;
|
.await
|
||||||
|
.expect("context updates should hydrate");
|
||||||
session.ensure_rollout_materialized().await;
|
session.ensure_rollout_materialized().await;
|
||||||
session.flush_rollout().await.expect("rollout should flush");
|
session.flush_rollout().await.expect("rollout should flush");
|
||||||
|
|
||||||
|
|||||||
@@ -159,8 +159,16 @@ pub(crate) async fn run_turn(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
sess.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
|
if let Err(err) = sess
|
||||||
.await;
|
.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) =
|
let (injection_items, explicitly_enabled_connectors) =
|
||||||
build_skills_and_plugins(&sess, turn_context.as_ref(), &input, &cancellation_token).await?;
|
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 {
|
pub(crate) fn to_turn_context_item(&self) -> TurnContextItem {
|
||||||
let workspace_roots = self.config.effective_workspace_roots();
|
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 {
|
TurnContextItem {
|
||||||
turn_id: Some(self.sub_id.clone()),
|
turn_id: Some(self.sub_id.clone()),
|
||||||
#[allow(deprecated)]
|
cwd,
|
||||||
cwd: self.cwd.to_path_buf(),
|
|
||||||
workspace_roots: (!workspace_roots.is_empty()).then_some(workspace_roots),
|
workspace_roots: (!workspace_roots.is_empty()).then_some(workspace_roots),
|
||||||
current_date: self.current_date.clone(),
|
current_date: self.current_date.clone(),
|
||||||
timezone: self.timezone.clone(),
|
timezone: self.timezone.clone(),
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ use core_test_support::wait_for_event;
|
|||||||
use codex_utils_path_uri::LegacyAppPathString;
|
use codex_utils_path_uri::LegacyAppPathString;
|
||||||
use codex_utils_path_uri::PathUri;
|
use codex_utils_path_uri::PathUri;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::Value;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
@@ -246,6 +247,51 @@ async fn app_server_starts_thread_with_windows_environment_native_cwd() -> Resul
|
|||||||
)
|
)
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
|
let requests = server
|
||||||
|
.received_requests()
|
||||||
|
.await
|
||||||
|
.context("failed to fetch received requests")?;
|
||||||
|
let first_request = requests
|
||||||
|
.iter()
|
||||||
|
.find(|request| request.url.path().ends_with("/responses"))
|
||||||
|
.context("turn should send a Responses request")?;
|
||||||
|
let body = first_request.body_json::<Value>()?;
|
||||||
|
let environment_context = body["input"]
|
||||||
|
.as_array()
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter(|item| item.get("role").and_then(Value::as_str) == Some("user"))
|
||||||
|
.filter_map(|item| item.get("content").and_then(Value::as_array))
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|content| content.get("text").and_then(Value::as_str))
|
||||||
|
.find(|text| text.starts_with("<environment_context>"))
|
||||||
|
.context("environment context should be model visible")?;
|
||||||
|
// The model should see the remote environment's shell, not the Linux app-server's
|
||||||
|
// host shell.
|
||||||
|
assert_eq!(
|
||||||
|
environment_context
|
||||||
|
.lines()
|
||||||
|
.find(|line| line.trim_start().starts_with("<shell>"))
|
||||||
|
.map(str::trim),
|
||||||
|
Some("<shell>powershell</shell>"),
|
||||||
|
);
|
||||||
|
// The model should see cwd using the remote environment's native path convention, not
|
||||||
|
// the Linux app-server's host path convention.
|
||||||
|
assert_eq!(
|
||||||
|
environment_context
|
||||||
|
.lines()
|
||||||
|
.find(|line| line.trim_start().starts_with("<cwd>"))
|
||||||
|
.map(str::trim),
|
||||||
|
Some(r"<cwd>C:\windows</cwd>"),
|
||||||
|
);
|
||||||
|
let host_workspace_roots = format!(
|
||||||
|
"<workspace_roots><root>{}</root></workspace_roots>",
|
||||||
|
codex_home.path().display()
|
||||||
|
);
|
||||||
|
// TODO(anp): Derive model-visible workspace roots from the selected remote environment
|
||||||
|
// and render them using its native path convention.
|
||||||
|
assert!(environment_context.contains(&host_workspace_roots));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use codex_protocol::protocol::TurnContextItem;
|
|||||||
use codex_protocol::protocol::TurnStartedEvent;
|
use codex_protocol::protocol::TurnStartedEvent;
|
||||||
use codex_protocol::protocol::UserMessageEvent;
|
use codex_protocol::protocol::UserMessageEvent;
|
||||||
use codex_protocol::protocol::WarningEvent;
|
use codex_protocol::protocol::WarningEvent;
|
||||||
|
use codex_utils_path_uri::PathUri;
|
||||||
use core::time::Duration;
|
use core::time::Duration;
|
||||||
use core_test_support::load_default_config_for_test;
|
use core_test_support::load_default_config_for_test;
|
||||||
use core_test_support::wait_for_event;
|
use core_test_support::wait_for_event;
|
||||||
@@ -27,7 +28,7 @@ fn resume_history(
|
|||||||
let turn_id = "resume-warning-seed-turn".to_string();
|
let turn_id = "resume-warning-seed-turn".to_string();
|
||||||
let turn_ctx = TurnContextItem {
|
let turn_ctx = TurnContextItem {
|
||||||
turn_id: Some(turn_id.clone()),
|
turn_id: Some(turn_id.clone()),
|
||||||
cwd: config.cwd.to_path_buf(),
|
cwd: PathUri::from_abs_path(&config.cwd),
|
||||||
workspace_roots: None,
|
workspace_roots: None,
|
||||||
current_date: None,
|
current_date: None,
|
||||||
timezone: None,
|
timezone: None,
|
||||||
|
|||||||
@@ -1423,7 +1423,11 @@ async fn parse_latest_turn_context_cwd(path: &Path) -> Option<PathBuf> {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if let RolloutItem::TurnContext(item) = rollout_line.item {
|
if let RolloutItem::TurnContext(item) = rollout_line.item {
|
||||||
return Some(item.cwd);
|
return item
|
||||||
|
.cwd
|
||||||
|
.to_abs_path()
|
||||||
|
.ok()
|
||||||
|
.map(AbsolutePathBuf::into_path_buf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -2988,7 +2988,7 @@ pub struct TurnContextNetworkItem {
|
|||||||
pub struct TurnContextItem {
|
pub struct TurnContextItem {
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub turn_id: Option<String>,
|
pub turn_id: Option<String>,
|
||||||
pub cwd: PathBuf,
|
pub cwd: PathUri,
|
||||||
/// Effective workspace roots used to materialize symbolic
|
/// Effective workspace roots used to materialize symbolic
|
||||||
/// `:workspace_roots` filesystem permissions in `permission_profile`.
|
/// `:workspace_roots` filesystem permissions in `permission_profile`.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
@@ -3026,21 +3026,35 @@ pub struct TurnContextItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TurnContextItem {
|
impl TurnContextItem {
|
||||||
pub fn permission_profile(&self) -> PermissionProfile {
|
pub fn permission_profile(&self) -> std::io::Result<PermissionProfile> {
|
||||||
self.permission_profile.clone().unwrap_or_else(|| {
|
if let Some(permission_profile) = self.permission_profile.clone() {
|
||||||
let file_system_sandbox_policy =
|
return Ok(permission_profile);
|
||||||
self.file_system_sandbox_policy.clone().unwrap_or_else(|| {
|
}
|
||||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
|
let file_system_sandbox_policy = match self.file_system_sandbox_policy.clone() {
|
||||||
&self.sandbox_policy,
|
Some(file_system_sandbox_policy) => file_system_sandbox_policy,
|
||||||
&self.cwd,
|
None => {
|
||||||
|
let cwd = self.cwd.to_abs_path().map_err(|err| {
|
||||||
|
std::io::Error::new(
|
||||||
|
err.kind(),
|
||||||
|
format!(
|
||||||
|
"cannot hydrate legacy permission profile for cwd {}: {err}",
|
||||||
|
self.cwd
|
||||||
|
),
|
||||||
)
|
)
|
||||||
});
|
})?;
|
||||||
|
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
|
||||||
|
&self.sandbox_policy,
|
||||||
|
cwd.as_path(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(
|
||||||
PermissionProfile::from_runtime_permissions_with_enforcement(
|
PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||||
SandboxEnforcement::from_legacy_sandbox_policy(&self.sandbox_policy),
|
SandboxEnforcement::from_legacy_sandbox_policy(&self.sandbox_policy),
|
||||||
&file_system_sandbox_policy,
|
&file_system_sandbox_policy,
|
||||||
NetworkSandboxPolicy::from(&self.sandbox_policy),
|
NetworkSandboxPolicy::from(&self.sandbox_policy),
|
||||||
)
|
),
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5301,21 +5315,50 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn turn_context_item_deserializes_without_network() -> Result<()> {
|
fn turn_context_item_accepts_legacy_cwd_and_serializes_path_uri() -> Result<()> {
|
||||||
|
let legacy_cwd = test_path_buf("/tmp");
|
||||||
let item: TurnContextItem = serde_json::from_value(json!({
|
let item: TurnContextItem = serde_json::from_value(json!({
|
||||||
"cwd": test_path_buf("/tmp"),
|
"cwd": legacy_cwd,
|
||||||
"approval_policy": "never",
|
"approval_policy": "never",
|
||||||
"sandbox_policy": { "type": "danger-full-access" },
|
"sandbox_policy": { "type": "danger-full-access" },
|
||||||
"model": "gpt-5",
|
"model": "gpt-5",
|
||||||
"summary": "auto",
|
"summary": "auto",
|
||||||
}))?;
|
}))?;
|
||||||
|
|
||||||
|
let expected_cwd = PathUri::from_path(legacy_cwd)?;
|
||||||
|
assert_eq!(item.cwd, expected_cwd);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(&item)?["cwd"],
|
||||||
|
json!(expected_cwd.to_string())
|
||||||
|
);
|
||||||
assert_eq!(item.network, None);
|
assert_eq!(item.network, None);
|
||||||
assert_eq!(item.file_system_sandbox_policy, None);
|
assert_eq!(item.file_system_sandbox_policy, None);
|
||||||
assert_eq!(item.comp_hash, None);
|
assert_eq!(item.comp_hash, None);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn turn_context_item_rejects_legacy_policy_hydration_for_foreign_cwd() -> Result<()> {
|
||||||
|
let foreign_cwd = if cfg!(windows) {
|
||||||
|
"file:///tmp"
|
||||||
|
} else {
|
||||||
|
"file://server/share"
|
||||||
|
};
|
||||||
|
let item: TurnContextItem = serde_json::from_value(json!({
|
||||||
|
"cwd": foreign_cwd,
|
||||||
|
"approval_policy": "never",
|
||||||
|
"sandbox_policy": { "type": "workspace-write" },
|
||||||
|
"model": "gpt-5",
|
||||||
|
"summary": "auto",
|
||||||
|
}))?;
|
||||||
|
|
||||||
|
let err = item
|
||||||
|
.permission_profile()
|
||||||
|
.expect_err("foreign cwd should not hydrate a legacy permission profile");
|
||||||
|
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn multi_agent_version_uses_newest_present_session_meta_value() -> Result<()> {
|
fn multi_agent_version_uses_newest_present_session_meta_value() -> Result<()> {
|
||||||
let thread_id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;
|
let thread_id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;
|
||||||
@@ -5353,7 +5396,7 @@ mod tests {
|
|||||||
fn turn_context_item_serializes_network_when_present() -> Result<()> {
|
fn turn_context_item_serializes_network_when_present() -> Result<()> {
|
||||||
let item = TurnContextItem {
|
let item = TurnContextItem {
|
||||||
turn_id: None,
|
turn_id: None,
|
||||||
cwd: test_path_buf("/tmp"),
|
cwd: PathUri::from_abs_path(&test_path_buf("/tmp").abs()),
|
||||||
workspace_roots: None,
|
workspace_roots: None,
|
||||||
current_date: None,
|
current_date: None,
|
||||||
timezone: None,
|
timezone: None,
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ pub async fn extract_metadata_from_rollout(
|
|||||||
})?;
|
})?;
|
||||||
let mut metadata = builder.build(default_provider);
|
let mut metadata = builder.build(default_provider);
|
||||||
for item in &items {
|
for item in &items {
|
||||||
apply_rollout_item(&mut metadata, item, default_provider);
|
apply_rollout_item(&mut metadata, item, default_provider)?;
|
||||||
}
|
}
|
||||||
if let Some(updated_at) = file_modified_time_utc(rollout_path).await {
|
if let Some(updated_at) = file_modified_time_utc(rollout_path).await {
|
||||||
metadata.updated_at = updated_at;
|
metadata.updated_at = updated_at;
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ async fn extract_metadata_from_rollout_uses_session_meta() {
|
|||||||
|
|
||||||
let builder = builder_from_session_meta(&session_meta_line, path.as_path()).expect("builder");
|
let builder = builder_from_session_meta(&session_meta_line, path.as_path()).expect("builder");
|
||||||
let mut expected = builder.build("openai");
|
let mut expected = builder.build("openai");
|
||||||
apply_rollout_item(&mut expected, &rollout_line.item, "openai");
|
apply_rollout_item(&mut expected, &rollout_line.item, "openai")
|
||||||
|
.expect("rollout item should apply");
|
||||||
expected.updated_at = file_modified_time_utc(&path).await.expect("mtime");
|
expected.updated_at = file_modified_time_utc(&path).await.expect("mtime");
|
||||||
|
|
||||||
assert_eq!(outcome.metadata, expected);
|
assert_eq!(outcome.metadata, expected);
|
||||||
|
|||||||
@@ -1765,15 +1765,16 @@ async fn resume_candidate_matches_cwd(
|
|||||||
|
|
||||||
if let Ok((items, _, _)) = RolloutRecorder::load_rollout_items(rollout_path).await
|
if let Ok((items, _, _)) = RolloutRecorder::load_rollout_items(rollout_path).await
|
||||||
&& let Some(latest_turn_context_cwd) = items.iter().rev().find_map(|item| match item {
|
&& let Some(latest_turn_context_cwd) = items.iter().rev().find_map(|item| match item {
|
||||||
RolloutItem::TurnContext(turn_context) => Some(turn_context.cwd.as_path()),
|
RolloutItem::TurnContext(turn_context) => Some(&turn_context.cwd),
|
||||||
RolloutItem::SessionMeta(_)
|
RolloutItem::SessionMeta(_)
|
||||||
| RolloutItem::ResponseItem(_)
|
| RolloutItem::ResponseItem(_)
|
||||||
| RolloutItem::InterAgentCommunication(_)
|
| RolloutItem::InterAgentCommunication(_)
|
||||||
| RolloutItem::Compacted(_)
|
| RolloutItem::Compacted(_)
|
||||||
| RolloutItem::EventMsg(_) => None,
|
| RolloutItem::EventMsg(_) => None,
|
||||||
})
|
})
|
||||||
|
&& let Ok(latest_turn_context_cwd) = latest_turn_context_cwd.to_abs_path()
|
||||||
{
|
{
|
||||||
return cwd_matches(latest_turn_context_cwd, cwd);
|
return cwd_matches(latest_turn_context_cwd.as_path(), cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata::extract_metadata_from_rollout(rollout_path, default_provider)
|
metadata::extract_metadata_from_rollout(rollout_path, default_provider)
|
||||||
|
|||||||
@@ -1134,7 +1134,8 @@ async fn resume_candidate_matches_cwd_reads_latest_turn_context() -> std::io::Re
|
|||||||
timestamp: "2025-01-03T13:00:01Z".to_string(),
|
timestamp: "2025-01-03T13:00:01Z".to_string(),
|
||||||
item: RolloutItem::TurnContext(TurnContextItem {
|
item: RolloutItem::TurnContext(TurnContextItem {
|
||||||
turn_id: Some("turn-1".to_string()),
|
turn_id: Some("turn-1".to_string()),
|
||||||
cwd: latest_cwd.clone(),
|
cwd: serde_json::from_value(serde_json::json!(&latest_cwd))
|
||||||
|
.expect("absolute latest cwd"),
|
||||||
workspace_roots: None,
|
workspace_roots: None,
|
||||||
current_date: None,
|
current_date: None,
|
||||||
timezone: None,
|
timezone: None,
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ pub fn apply_rollout_item(
|
|||||||
metadata: &mut ThreadMetadata,
|
metadata: &mut ThreadMetadata,
|
||||||
item: &RolloutItem,
|
item: &RolloutItem,
|
||||||
default_provider: &str,
|
default_provider: &str,
|
||||||
) {
|
) -> std::io::Result<()> {
|
||||||
match item {
|
match item {
|
||||||
RolloutItem::SessionMeta(meta_line) => apply_session_meta_from_item(metadata, meta_line),
|
RolloutItem::SessionMeta(meta_line) => apply_session_meta_from_item(metadata, meta_line),
|
||||||
RolloutItem::TurnContext(turn_ctx) => apply_turn_context(metadata, turn_ctx),
|
RolloutItem::TurnContext(turn_ctx) => apply_turn_context(metadata, turn_ctx)?,
|
||||||
RolloutItem::EventMsg(event) => apply_event_msg(metadata, event),
|
RolloutItem::EventMsg(event) => apply_event_msg(metadata, event),
|
||||||
RolloutItem::ResponseItem(item) => apply_response_item(metadata, item),
|
RolloutItem::ResponseItem(item) => apply_response_item(metadata, item),
|
||||||
RolloutItem::InterAgentCommunication(_) => {}
|
RolloutItem::InterAgentCommunication(_) => {}
|
||||||
@@ -28,6 +28,7 @@ pub fn apply_rollout_item(
|
|||||||
if metadata.model_provider.is_empty() {
|
if metadata.model_provider.is_empty() {
|
||||||
metadata.model_provider = default_provider.to_string();
|
metadata.model_provider = default_provider.to_string();
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return whether this rollout item can mutate thread metadata stored in SQLite.
|
/// Return whether this rollout item can mutate thread metadata stored in SQLite.
|
||||||
@@ -72,15 +73,21 @@ fn apply_session_meta_from_item(metadata: &mut ThreadMetadata, meta_line: &Sessi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_turn_context(metadata: &mut ThreadMetadata, turn_ctx: &TurnContextItem) {
|
fn apply_turn_context(
|
||||||
if metadata.cwd.as_os_str().is_empty() {
|
metadata: &mut ThreadMetadata,
|
||||||
metadata.cwd = turn_ctx.cwd.clone();
|
turn_ctx: &TurnContextItem,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
if metadata.cwd.as_os_str().is_empty()
|
||||||
|
&& let Ok(cwd) = turn_ctx.cwd.to_abs_path()
|
||||||
|
{
|
||||||
|
metadata.cwd = cwd.into_path_buf();
|
||||||
}
|
}
|
||||||
metadata.model = Some(turn_ctx.model.clone());
|
metadata.model = Some(turn_ctx.model.clone());
|
||||||
metadata.reasoning_effort = turn_ctx.effort.clone();
|
metadata.reasoning_effort = turn_ctx.effort.clone();
|
||||||
metadata.sandbox_policy =
|
metadata.sandbox_policy =
|
||||||
serde_json::to_string(&turn_ctx.permission_profile()).unwrap_or_default();
|
serde_json::to_string(&turn_ctx.permission_profile()?).unwrap_or_default();
|
||||||
metadata.approval_mode = enum_to_string(&turn_ctx.approval_policy);
|
metadata.approval_mode = enum_to_string(&turn_ctx.approval_policy);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_event_msg(metadata: &mut ThreadMetadata, event: &EventMsg) {
|
fn apply_event_msg(metadata: &mut ThreadMetadata, event: &EventMsg) {
|
||||||
@@ -194,7 +201,8 @@ mod tests {
|
|||||||
metadata: None,
|
metadata: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
apply_rollout_item(&mut metadata, &item, "test-provider");
|
apply_rollout_item(&mut metadata, &item, "test-provider")
|
||||||
|
.expect("rollout item should apply");
|
||||||
|
|
||||||
assert_eq!(metadata.first_user_message, None);
|
assert_eq!(metadata.first_user_message, None);
|
||||||
assert_eq!(metadata.preview, None);
|
assert_eq!(metadata.preview, None);
|
||||||
@@ -213,7 +221,8 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
apply_rollout_item(&mut metadata, &item, "test-provider");
|
apply_rollout_item(&mut metadata, &item, "test-provider")
|
||||||
|
.expect("rollout item should apply");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
metadata.first_user_message.as_deref(),
|
metadata.first_user_message.as_deref(),
|
||||||
@@ -235,7 +244,8 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
apply_rollout_item(&mut metadata, &item, "test-provider");
|
apply_rollout_item(&mut metadata, &item, "test-provider")
|
||||||
|
.expect("rollout item should apply");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
metadata.first_user_message.as_deref(),
|
metadata.first_user_message.as_deref(),
|
||||||
@@ -260,7 +270,8 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
apply_rollout_item(&mut metadata, &item, "test-provider");
|
apply_rollout_item(&mut metadata, &item, "test-provider")
|
||||||
|
.expect("rollout item should apply");
|
||||||
|
|
||||||
assert_eq!(metadata.first_user_message, None);
|
assert_eq!(metadata.first_user_message, None);
|
||||||
assert_eq!(metadata.preview, None);
|
assert_eq!(metadata.preview, None);
|
||||||
@@ -286,7 +297,8 @@ mod tests {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
apply_rollout_item(&mut metadata, &goal_item, "test-provider");
|
apply_rollout_item(&mut metadata, &goal_item, "test-provider")
|
||||||
|
.expect("rollout item should apply");
|
||||||
|
|
||||||
assert_eq!(metadata.preview.as_deref(), Some("optimize the benchmark"));
|
assert_eq!(metadata.preview.as_deref(), Some("optimize the benchmark"));
|
||||||
assert_eq!(metadata.first_user_message, None);
|
assert_eq!(metadata.first_user_message, None);
|
||||||
@@ -301,7 +313,8 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
apply_rollout_item(&mut metadata, &user_item, "test-provider");
|
apply_rollout_item(&mut metadata, &user_item, "test-provider")
|
||||||
|
.expect("rollout item should apply");
|
||||||
|
|
||||||
assert_eq!(metadata.preview.as_deref(), Some("optimize the benchmark"));
|
assert_eq!(metadata.preview.as_deref(), Some("optimize the benchmark"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -344,12 +357,18 @@ mod tests {
|
|||||||
git: None,
|
git: None,
|
||||||
}),
|
}),
|
||||||
"test-provider",
|
"test-provider",
|
||||||
);
|
)
|
||||||
|
.expect("rollout item should apply");
|
||||||
apply_rollout_item(
|
apply_rollout_item(
|
||||||
&mut metadata,
|
&mut metadata,
|
||||||
&RolloutItem::TurnContext(TurnContextItem {
|
&RolloutItem::TurnContext(TurnContextItem {
|
||||||
turn_id: Some("turn-1".to_string()),
|
turn_id: Some("turn-1".to_string()),
|
||||||
cwd: PathBuf::from("/parent/workspace"),
|
cwd: serde_json::from_value(serde_json::json!(
|
||||||
|
std::env::current_dir()
|
||||||
|
.expect("current directory")
|
||||||
|
.join("parent/workspace")
|
||||||
|
))
|
||||||
|
.expect("absolute parent cwd"),
|
||||||
workspace_roots: None,
|
workspace_roots: None,
|
||||||
current_date: None,
|
current_date: None,
|
||||||
timezone: None,
|
timezone: None,
|
||||||
@@ -368,7 +387,8 @@ mod tests {
|
|||||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||||
}),
|
}),
|
||||||
"test-provider",
|
"test-provider",
|
||||||
);
|
)
|
||||||
|
.expect("rollout item should apply");
|
||||||
|
|
||||||
assert_eq!(metadata.cwd, PathBuf::from("/child/worktree"));
|
assert_eq!(metadata.cwd, PathBuf::from("/child/worktree"));
|
||||||
let permission_profile: PermissionProfile = PermissionProfile::Disabled;
|
let permission_profile: PermissionProfile = PermissionProfile::Disabled;
|
||||||
@@ -388,7 +408,12 @@ mod tests {
|
|||||||
&mut metadata,
|
&mut metadata,
|
||||||
&RolloutItem::TurnContext(TurnContextItem {
|
&RolloutItem::TurnContext(TurnContextItem {
|
||||||
turn_id: Some("turn-1".to_string()),
|
turn_id: Some("turn-1".to_string()),
|
||||||
cwd: PathBuf::from("/workspace"),
|
cwd: serde_json::from_value(serde_json::json!(
|
||||||
|
std::env::current_dir()
|
||||||
|
.expect("current directory")
|
||||||
|
.join("workspace")
|
||||||
|
))
|
||||||
|
.expect("absolute workspace cwd"),
|
||||||
workspace_roots: None,
|
workspace_roots: None,
|
||||||
current_date: None,
|
current_date: None,
|
||||||
timezone: None,
|
timezone: None,
|
||||||
@@ -407,7 +432,8 @@ mod tests {
|
|||||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||||
}),
|
}),
|
||||||
"test-provider",
|
"test-provider",
|
||||||
);
|
)
|
||||||
|
.expect("rollout item should apply");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
metadata.sandbox_policy,
|
metadata.sandbox_policy,
|
||||||
@@ -419,12 +445,16 @@ mod tests {
|
|||||||
fn turn_context_sets_cwd_when_session_cwd_missing() {
|
fn turn_context_sets_cwd_when_session_cwd_missing() {
|
||||||
let mut metadata = metadata_for_test();
|
let mut metadata = metadata_for_test();
|
||||||
metadata.cwd = PathBuf::new();
|
metadata.cwd = PathBuf::new();
|
||||||
|
let fallback_cwd = std::env::current_dir()
|
||||||
|
.expect("current directory")
|
||||||
|
.join("fallback/workspace");
|
||||||
|
|
||||||
apply_rollout_item(
|
apply_rollout_item(
|
||||||
&mut metadata,
|
&mut metadata,
|
||||||
&RolloutItem::TurnContext(TurnContextItem {
|
&RolloutItem::TurnContext(TurnContextItem {
|
||||||
turn_id: Some("turn-1".to_string()),
|
turn_id: Some("turn-1".to_string()),
|
||||||
cwd: PathBuf::from("/fallback/workspace"),
|
cwd: serde_json::from_value(serde_json::json!(&fallback_cwd))
|
||||||
|
.expect("absolute fallback cwd"),
|
||||||
workspace_roots: None,
|
workspace_roots: None,
|
||||||
current_date: None,
|
current_date: None,
|
||||||
timezone: None,
|
timezone: None,
|
||||||
@@ -443,9 +473,10 @@ mod tests {
|
|||||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||||
}),
|
}),
|
||||||
"test-provider",
|
"test-provider",
|
||||||
);
|
)
|
||||||
|
.expect("rollout item should apply");
|
||||||
|
|
||||||
assert_eq!(metadata.cwd, PathBuf::from("/fallback/workspace"));
|
assert_eq!(metadata.cwd, fallback_cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -456,7 +487,12 @@ mod tests {
|
|||||||
&mut metadata,
|
&mut metadata,
|
||||||
&RolloutItem::TurnContext(TurnContextItem {
|
&RolloutItem::TurnContext(TurnContextItem {
|
||||||
turn_id: Some("turn-1".to_string()),
|
turn_id: Some("turn-1".to_string()),
|
||||||
cwd: PathBuf::from("/fallback/workspace"),
|
cwd: serde_json::from_value(serde_json::json!(
|
||||||
|
std::env::current_dir()
|
||||||
|
.expect("current directory")
|
||||||
|
.join("fallback/workspace")
|
||||||
|
))
|
||||||
|
.expect("absolute fallback cwd"),
|
||||||
workspace_roots: None,
|
workspace_roots: None,
|
||||||
current_date: None,
|
current_date: None,
|
||||||
timezone: None,
|
timezone: None,
|
||||||
@@ -475,7 +511,8 @@ mod tests {
|
|||||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||||
}),
|
}),
|
||||||
"test-provider",
|
"test-provider",
|
||||||
);
|
)
|
||||||
|
.expect("rollout item should apply");
|
||||||
|
|
||||||
assert_eq!(metadata.model.as_deref(), Some("gpt-5"));
|
assert_eq!(metadata.model.as_deref(), Some("gpt-5"));
|
||||||
assert_eq!(metadata.reasoning_effort, Some(ReasoningEffort::High));
|
assert_eq!(metadata.reasoning_effort, Some(ReasoningEffort::High));
|
||||||
@@ -511,7 +548,8 @@ mod tests {
|
|||||||
git: None,
|
git: None,
|
||||||
}),
|
}),
|
||||||
"test-provider",
|
"test-provider",
|
||||||
);
|
)
|
||||||
|
.expect("rollout item should apply");
|
||||||
|
|
||||||
assert_eq!(metadata.model, None);
|
assert_eq!(metadata.model, None);
|
||||||
assert_eq!(metadata.reasoning_effort, None);
|
assert_eq!(metadata.reasoning_effort, None);
|
||||||
|
|||||||
@@ -829,7 +829,7 @@ ON CONFLICT(id) DO UPDATE SET
|
|||||||
.unwrap_or_else(|| builder.build(&self.default_provider));
|
.unwrap_or_else(|| builder.build(&self.default_provider));
|
||||||
metadata.rollout_path = builder.rollout_path.clone();
|
metadata.rollout_path = builder.rollout_path.clone();
|
||||||
for item in items {
|
for item in items {
|
||||||
apply_rollout_item(&mut metadata, item, &self.default_provider);
|
apply_rollout_item(&mut metadata, item, &self.default_provider)?;
|
||||||
}
|
}
|
||||||
if let Some(existing_metadata) = existing_metadata.as_ref() {
|
if let Some(existing_metadata) = existing_metadata.as_ref() {
|
||||||
metadata.prefer_existing_git_info(existing_metadata);
|
metadata.prefer_existing_git_info(existing_metadata);
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ impl LiveThread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let metadata_sync = ThreadMetadataSync::for_resume(¶ms);
|
let metadata_sync = ThreadMetadataSync::for_resume(¶ms)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
thread_id,
|
thread_id,
|
||||||
thread_store,
|
thread_store,
|
||||||
@@ -151,7 +151,7 @@ impl LiveThread {
|
|||||||
.metadata_sync
|
.metadata_sync
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.observe_appended_items(canonical_items.as_slice());
|
.observe_appended_items(canonical_items.as_slice())?;
|
||||||
if let Some(update) = update {
|
if let Some(update) = update {
|
||||||
self.thread_store
|
self.thread_store
|
||||||
.update_thread_metadata(UpdateThreadMetadataParams {
|
.update_thread_metadata(UpdateThreadMetadataParams {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ use crate::CreateThreadParams;
|
|||||||
use crate::GitInfoPatch;
|
use crate::GitInfoPatch;
|
||||||
use crate::ResumeThreadParams;
|
use crate::ResumeThreadParams;
|
||||||
use crate::ThreadMetadataPatch;
|
use crate::ThreadMetadataPatch;
|
||||||
|
use crate::ThreadStoreError;
|
||||||
|
use crate::ThreadStoreResult;
|
||||||
|
|
||||||
const IMAGE_ONLY_USER_MESSAGE_PLACEHOLDER: &str = "[Image]";
|
const IMAGE_ONLY_USER_MESSAGE_PLACEHOLDER: &str = "[Image]";
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
@@ -90,7 +92,7 @@ impl ThreadMetadataSync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn for_resume(params: &ResumeThreadParams) -> Self {
|
pub(crate) fn for_resume(params: &ResumeThreadParams) -> ThreadStoreResult<Self> {
|
||||||
let mut sync = Self {
|
let mut sync = Self {
|
||||||
thread_id: params.thread_id,
|
thread_id: params.thread_id,
|
||||||
cwd_seen: params
|
cwd_seen: params
|
||||||
@@ -108,11 +110,11 @@ impl ThreadMetadataSync {
|
|||||||
defer_resume_update_until_append: false,
|
defer_resume_update_until_append: false,
|
||||||
};
|
};
|
||||||
if let Some(history) = params.history.as_deref() {
|
if let Some(history) = params.history.as_deref() {
|
||||||
let update = sync.observe_resume_history(history);
|
let update = sync.observe_resume_history(history)?;
|
||||||
sync.merge_pending_update(update);
|
sync.merge_pending_update(update);
|
||||||
sync.defer_resume_update_until_append = sync.pending_update.is_some();
|
sync.defer_resume_update_until_append = sync.pending_update.is_some();
|
||||||
}
|
}
|
||||||
sync
|
Ok(sync)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn take_pending_update(&self) -> Option<PendingThreadMetadataPatch> {
|
pub(crate) fn take_pending_update(&self) -> Option<PendingThreadMetadataPatch> {
|
||||||
@@ -148,7 +150,7 @@ impl ThreadMetadataSync {
|
|||||||
pub(crate) fn observe_appended_items(
|
pub(crate) fn observe_appended_items(
|
||||||
&mut self,
|
&mut self,
|
||||||
items: &[RolloutItem],
|
items: &[RolloutItem],
|
||||||
) -> Option<PendingThreadMetadataPatch> {
|
) -> ThreadStoreResult<Option<PendingThreadMetadataPatch>> {
|
||||||
self.defer_create_update_until_history_exists = false;
|
self.defer_create_update_until_history_exists = false;
|
||||||
self.defer_resume_update_until_append = false;
|
self.defer_resume_update_until_append = false;
|
||||||
let affects_metadata = items
|
let affects_metadata = items
|
||||||
@@ -157,7 +159,10 @@ impl ThreadMetadataSync {
|
|||||||
let update = if affects_metadata {
|
let update = if affects_metadata {
|
||||||
self.observe_items(items)?
|
self.observe_items(items)?
|
||||||
} else {
|
} else {
|
||||||
thread_updated_at_touch()
|
Some(thread_updated_at_touch())
|
||||||
|
};
|
||||||
|
let Some(update) = update else {
|
||||||
|
return Ok(None);
|
||||||
};
|
};
|
||||||
self.merge_pending_update(Some(update));
|
self.merge_pending_update(Some(update));
|
||||||
if !affects_metadata
|
if !affects_metadata
|
||||||
@@ -169,12 +174,15 @@ impl ThreadMetadataSync {
|
|||||||
Instant::now().duration_since(last_touch) < THREAD_UPDATED_AT_TOUCH_INTERVAL
|
Instant::now().duration_since(last_touch) < THREAD_UPDATED_AT_TOUCH_INTERVAL
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
return None;
|
return Ok(None);
|
||||||
}
|
}
|
||||||
self.take_pending_update()
|
Ok(self.take_pending_update())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn observe_items(&mut self, items: &[RolloutItem]) -> Option<ThreadMetadataPatch> {
|
fn observe_items(
|
||||||
|
&mut self,
|
||||||
|
items: &[RolloutItem],
|
||||||
|
) -> ThreadStoreResult<Option<ThreadMetadataPatch>> {
|
||||||
self.observe_items_with_update(
|
self.observe_items_with_update(
|
||||||
items,
|
items,
|
||||||
ThreadMetadataPatch {
|
ThreadMetadataPatch {
|
||||||
@@ -184,7 +192,10 @@ impl ThreadMetadataSync {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn observe_resume_history(&mut self, items: &[RolloutItem]) -> Option<ThreadMetadataPatch> {
|
fn observe_resume_history(
|
||||||
|
&mut self,
|
||||||
|
items: &[RolloutItem],
|
||||||
|
) -> ThreadStoreResult<Option<ThreadMetadataPatch>> {
|
||||||
self.observe_items_with_update(items, ThreadMetadataPatch::default())
|
self.observe_items_with_update(items, ThreadMetadataPatch::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,9 +203,9 @@ impl ThreadMetadataSync {
|
|||||||
&mut self,
|
&mut self,
|
||||||
items: &[RolloutItem],
|
items: &[RolloutItem],
|
||||||
mut update: ThreadMetadataPatch,
|
mut update: ThreadMetadataPatch,
|
||||||
) -> Option<ThreadMetadataPatch> {
|
) -> ThreadStoreResult<Option<ThreadMetadataPatch>> {
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
return None;
|
return Ok(None);
|
||||||
}
|
}
|
||||||
for item in items {
|
for item in items {
|
||||||
match item {
|
match item {
|
||||||
@@ -227,14 +238,23 @@ impl ThreadMetadataSync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
RolloutItem::TurnContext(turn_ctx) => {
|
RolloutItem::TurnContext(turn_ctx) => {
|
||||||
if !self.cwd_seen && !turn_ctx.cwd.as_os_str().is_empty() {
|
if !self.cwd_seen
|
||||||
|
&& let Ok(cwd) = turn_ctx.cwd.to_abs_path()
|
||||||
|
{
|
||||||
self.cwd_seen = true;
|
self.cwd_seen = true;
|
||||||
update.cwd = Some(turn_ctx.cwd.clone());
|
update.cwd = Some(cwd.into_path_buf());
|
||||||
}
|
}
|
||||||
update.model = Some(turn_ctx.model.clone());
|
update.model = Some(turn_ctx.model.clone());
|
||||||
update.reasoning_effort = turn_ctx.effort.clone();
|
update.reasoning_effort = turn_ctx.effort.clone();
|
||||||
update.approval_mode = Some(turn_ctx.approval_policy);
|
update.approval_mode = Some(turn_ctx.approval_policy);
|
||||||
update.permission_profile = Some(turn_ctx.permission_profile());
|
update.permission_profile =
|
||||||
|
Some(turn_ctx.permission_profile().map_err(|err| {
|
||||||
|
ThreadStoreError::Internal {
|
||||||
|
message: format!(
|
||||||
|
"failed to hydrate turn permission profile: {err}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})?);
|
||||||
}
|
}
|
||||||
RolloutItem::EventMsg(EventMsg::UserMessage(user)) => {
|
RolloutItem::EventMsg(EventMsg::UserMessage(user)) => {
|
||||||
if let Some(preview) = user_message_preview(user) {
|
if let Some(preview) = user_message_preview(user) {
|
||||||
@@ -276,7 +296,7 @@ impl ThreadMetadataSync {
|
|||||||
| RolloutItem::Compacted(_) => {}
|
| RolloutItem::Compacted(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(update)
|
Ok(Some(update))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_pending_update(&mut self, update: Option<ThreadMetadataPatch>) {
|
fn merge_pending_update(&mut self, update: Option<ThreadMetadataPatch>) {
|
||||||
@@ -394,7 +414,8 @@ mod tests {
|
|||||||
RolloutItem::SessionMeta(session_meta(thread_id)),
|
RolloutItem::SessionMeta(session_meta(thread_id)),
|
||||||
RolloutItem::EventMsg(EventMsg::UserMessage(user_message("hello metadata"))),
|
RolloutItem::EventMsg(EventMsg::UserMessage(user_message("hello metadata"))),
|
||||||
],
|
],
|
||||||
));
|
))
|
||||||
|
.expect("resume metadata should hydrate");
|
||||||
|
|
||||||
let update = sync.take_pending_update().expect("pending metadata update");
|
let update = sync.take_pending_update().expect("pending metadata update");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -433,7 +454,8 @@ mod tests {
|
|||||||
))),
|
))),
|
||||||
RolloutItem::EventMsg(EventMsg::UserMessage(user_message("first user text"))),
|
RolloutItem::EventMsg(EventMsg::UserMessage(user_message("first user text"))),
|
||||||
],
|
],
|
||||||
));
|
))
|
||||||
|
.expect("resume metadata should hydrate");
|
||||||
|
|
||||||
let update = sync.take_pending_update().expect("pending metadata update");
|
let update = sync.take_pending_update().expect("pending metadata update");
|
||||||
assert_eq!(update.patch.preview.as_deref(), Some("ship the refactor"));
|
assert_eq!(update.patch.preview.as_deref(), Some("ship the refactor"));
|
||||||
@@ -452,7 +474,8 @@ mod tests {
|
|||||||
vec![RolloutItem::EventMsg(EventMsg::UserMessage(user_message(
|
vec![RolloutItem::EventMsg(EventMsg::UserMessage(user_message(
|
||||||
"first user text",
|
"first user text",
|
||||||
)))],
|
)))],
|
||||||
));
|
))
|
||||||
|
.expect("resume metadata should hydrate");
|
||||||
let pending = sync.take_pending_update().expect("pending resume metadata");
|
let pending = sync.take_pending_update().expect("pending resume metadata");
|
||||||
sync.mark_pending_update_applied(&pending);
|
sync.mark_pending_update_applied(&pending);
|
||||||
|
|
||||||
@@ -460,6 +483,7 @@ mod tests {
|
|||||||
.observe_appended_items(&[RolloutItem::EventMsg(EventMsg::UserMessage(user_message(
|
.observe_appended_items(&[RolloutItem::EventMsg(EventMsg::UserMessage(user_message(
|
||||||
"later user text",
|
"later user text",
|
||||||
)))])
|
)))])
|
||||||
|
.expect("appended metadata should hydrate")
|
||||||
.expect("updated_at touch");
|
.expect("updated_at touch");
|
||||||
|
|
||||||
assert_eq!(update.patch.preview, None);
|
assert_eq!(update.patch.preview, None);
|
||||||
@@ -471,7 +495,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn metadata_irrelevant_items_coalesce_updated_at_touches() {
|
fn metadata_irrelevant_items_coalesce_updated_at_touches() {
|
||||||
let thread_id = ThreadId::new();
|
let thread_id = ThreadId::new();
|
||||||
let mut sync = ThreadMetadataSync::for_resume(&resume_params(thread_id, Vec::new()));
|
let mut sync = ThreadMetadataSync::for_resume(&resume_params(thread_id, Vec::new()))
|
||||||
|
.expect("resume metadata should hydrate");
|
||||||
let item = RolloutItem::Compacted(CompactedItem {
|
let item = RolloutItem::Compacted(CompactedItem {
|
||||||
message: "compacted".to_string(),
|
message: "compacted".to_string(),
|
||||||
replacement_history: None,
|
replacement_history: None,
|
||||||
@@ -480,12 +505,14 @@ mod tests {
|
|||||||
|
|
||||||
let first = sync
|
let first = sync
|
||||||
.observe_appended_items(std::slice::from_ref(&item))
|
.observe_appended_items(std::slice::from_ref(&item))
|
||||||
|
.expect("appended metadata should hydrate")
|
||||||
.expect("first touch should apply immediately");
|
.expect("first touch should apply immediately");
|
||||||
assert!(first.patch.updated_at.is_some());
|
assert!(first.patch.updated_at.is_some());
|
||||||
sync.mark_pending_update_applied(&first);
|
sync.mark_pending_update_applied(&first);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
sync.observe_appended_items(std::slice::from_ref(&item))
|
sync.observe_appended_items(std::slice::from_ref(&item))
|
||||||
|
.expect("appended metadata should hydrate")
|
||||||
.is_none(),
|
.is_none(),
|
||||||
"second touch inside the coalescing window should wait for a barrier"
|
"second touch inside the coalescing window should wait for a barrier"
|
||||||
);
|
);
|
||||||
@@ -504,7 +531,8 @@ mod tests {
|
|||||||
RolloutItem::SessionMeta(session_meta(thread_id)),
|
RolloutItem::SessionMeta(session_meta(thread_id)),
|
||||||
RolloutItem::EventMsg(EventMsg::UserMessage(user_message("hello metadata"))),
|
RolloutItem::EventMsg(EventMsg::UserMessage(user_message("hello metadata"))),
|
||||||
],
|
],
|
||||||
));
|
))
|
||||||
|
.expect("resume metadata should hydrate");
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
sync.take_pending_update_for_existing_history().is_none(),
|
sync.take_pending_update_for_existing_history().is_none(),
|
||||||
@@ -514,6 +542,7 @@ mod tests {
|
|||||||
sync.observe_appended_items(&[RolloutItem::EventMsg(EventMsg::UserMessage(
|
sync.observe_appended_items(&[RolloutItem::EventMsg(EventMsg::UserMessage(
|
||||||
user_message("new append"),
|
user_message("new append"),
|
||||||
))])
|
))])
|
||||||
|
.expect("appended metadata should hydrate")
|
||||||
.is_some(),
|
.is_some(),
|
||||||
"the first append should flush resume metadata together with append metadata"
|
"the first append should flush resume metadata together with append metadata"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -164,6 +164,19 @@ impl PathUri {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Renders this URI using the native path syntax inferred from its shape.
|
||||||
|
///
|
||||||
|
/// This is independent of the current host: a Windows URI renders with
|
||||||
|
/// Windows separators on every host. If the convention cannot be inferred
|
||||||
|
/// or the URI cannot be represented using that convention, the canonical
|
||||||
|
/// URI string is returned instead.
|
||||||
|
pub fn inferred_native_path_string(&self) -> String {
|
||||||
|
self.infer_path_convention()
|
||||||
|
.and_then(|convention| LegacyAppPathString::from_path_uri(self, convention).ok())
|
||||||
|
.map(LegacyAppPathString::into_string)
|
||||||
|
.unwrap_or_else(|| self.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the decoded final URI path segment, or `None` for the URI root
|
/// Returns the decoded final URI path segment, or `None` for the URI root
|
||||||
/// or an opaque fallback URI created by [`Self::from_abs_path`].
|
/// or an opaque fallback URI created by [`Self::from_abs_path`].
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -100,6 +100,28 @@ fn drive_shaped_posix_uri_is_intentionally_inferred_as_windows() {
|
|||||||
assert_eq!(path.infer_path_convention(), Some(PathConvention::Windows));
|
assert_eq!(path.infer_path_convention(), Some(PathConvention::Windows));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inferred_native_path_string_uses_the_inferred_convention() {
|
||||||
|
for (uri, expected) in [
|
||||||
|
("file:///home/alice/a%20file.rs", "/home/alice/a file.rs"),
|
||||||
|
(
|
||||||
|
"file:///C:/Users/Alice%20Smith/main.rs",
|
||||||
|
r"C:\Users\Alice Smith\main.rs",
|
||||||
|
),
|
||||||
|
("file://server/share/main.rs", r"\\server\share\main.rs"),
|
||||||
|
("file://server/", "file://server/"),
|
||||||
|
("file:///%00/bad/path/YQ", "file:///%00/bad/path/YQ"),
|
||||||
|
] {
|
||||||
|
let path = PathUri::parse(uri).expect("valid path URI");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
path.inferred_native_path_string(),
|
||||||
|
expected,
|
||||||
|
"rendering {uri}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
#[test]
|
#[test]
|
||||||
fn file_uri_falls_back_for_windows_prefixes_without_a_uri_representation() {
|
fn file_uri_falls_back_for_windows_prefixes_without_a_uri_representation() {
|
||||||
|
|||||||
Reference in new issue
Block a user