Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 22 additions & 11 deletions codex-rs/app-server/src/codex_message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ use codex_core::plugins::PluginUninstallError as CorePluginUninstallError;
use codex_core::plugins::load_plugin_apps;
use codex_core::plugins::load_plugin_mcp_servers;
use codex_core::read_head_for_summary;
use codex_core::read_latest_turn_context;
use codex_core::read_session_meta_line;
use codex_core::rollout_date_parts;
use codex_core::sandboxing::SandboxPermissions;
Expand Down Expand Up @@ -2972,17 +2973,14 @@ impl CodexMessageProcessor {
});
};

let required_suffix = format!("{thread_id}.jsonl");
let Some(file_name) = canonical_rollout_path.file_name().map(OsStr::to_owned) else {
return Err(JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: format!("rollout path `{rollout_path_display}` missing file name"),
data: None,
});
};
if !file_name
.to_string_lossy()
.ends_with(required_suffix.as_str())
if !rollout_file_name_matches_thread_id(file_name.to_string_lossy().as_ref(), thread_id)
{
return Err(JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
Expand Down Expand Up @@ -5324,7 +5322,6 @@ impl CodexMessageProcessor {
};

// Verify file name matches thread id.
let required_suffix = format!("{thread_id}.jsonl");
let Some(file_name) = canonical_rollout_path.file_name().map(OsStr::to_owned) else {
return Err(JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
Expand All @@ -5335,10 +5332,7 @@ impl CodexMessageProcessor {
data: None,
});
};
if !file_name
.to_string_lossy()
.ends_with(required_suffix.as_str())
{
if !rollout_file_name_matches_thread_id(file_name.to_string_lossy().as_ref(), thread_id) {
return Err(JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: format!(
Expand Down Expand Up @@ -8184,6 +8178,10 @@ async fn read_history_cwd_from_state_db(
thread_id: Option<ThreadId>,
rollout_path: &Path,
) -> Option<PathBuf> {
if let Ok(Some(turn_context)) = read_latest_turn_context(rollout_path).await {
return Some(turn_context.cwd);
}

if let Some(state_db_ctx) = get_state_db(config).await
&& let Some(thread_id) = thread_id
&& let Ok(Some(metadata)) = state_db_ctx.get_thread(thread_id).await
Expand Down Expand Up @@ -8274,7 +8272,7 @@ async fn summary_from_thread_list_item(

fn thread_id_from_rollout_path(path: &Path) -> Option<ThreadId> {
let file_name = path.file_name()?.to_str()?;
let stem = file_name.strip_suffix(".jsonl")?;
let stem = codex_core::strip_rollout_file_suffix(file_name)?;
if stem.len() < 37 {
return None;
}
Expand All @@ -8285,6 +8283,13 @@ fn thread_id_from_rollout_path(path: &Path) -> Option<ThreadId> {
ThreadId::from_string(&stem[uuid_start..]).ok()
}

fn rollout_file_name_matches_thread_id(file_name: &str, thread_id: ThreadId) -> bool {
let Some(stem) = codex_core::strip_rollout_file_suffix(file_name) else {
return false;
};
stem.ends_with(&format!("-{thread_id}"))
}

#[allow(clippy::too_many_arguments)]
fn summary_from_state_db_metadata(
conversation_id: ThreadId,
Expand Down Expand Up @@ -8412,6 +8417,12 @@ pub(crate) async fn read_summary_from_rollout(
.unwrap_or_else(|| fallback_provider.to_string());
let git_info = git.as_ref().map(map_git_info);
let updated_at = updated_at.or_else(|| timestamp.clone());
let resume_cwd = read_latest_turn_context(path)
.await
.ok()
.flatten()
.map(|turn_context| turn_context.cwd)
.unwrap_or_else(|| session_meta.cwd.clone());

Ok(ConversationSummary {
conversation_id: session_meta.id,
Expand All @@ -8420,7 +8431,7 @@ pub(crate) async fn read_summary_from_rollout(
path: path.to_path_buf(),
preview: String::new(),
model_provider,
cwd: session_meta.cwd,
cwd: resume_cwd,
cli_version: session_meta.cli_version,
source: session_meta.source,
git_info,
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,16 +172,21 @@ pub use rollout::find_conversation_path_by_id_str;
pub use rollout::find_thread_name_by_id;
pub use rollout::find_thread_path_by_id_str;
pub use rollout::find_thread_path_by_name_str;
pub use rollout::is_rollout_path;
pub use rollout::list::Cursor;
pub use rollout::list::ThreadItem;
pub use rollout::list::ThreadSortKey;
pub use rollout::list::ThreadsPage;
pub use rollout::list::parse_cursor;
pub use rollout::list::read_head_for_summary;
pub use rollout::list::read_latest_turn_context;
pub use rollout::list::read_session_meta_line;
pub use rollout::policy::EventPersistenceMode;
pub use rollout::read_nonempty_rollout_text;
pub use rollout::read_rollout_text;
pub use rollout::rollout_date_parts;
pub use rollout::session_index::find_thread_names_by_ids;
pub use rollout::strip_rollout_file_suffix;
mod function_tool;
mod state;
mod tasks;
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/core/src/rollout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ pub use codex_rollout::find_conversation_path_by_id_str;
pub use codex_rollout::find_thread_name_by_id;
pub use codex_rollout::find_thread_path_by_id_str;
pub use codex_rollout::find_thread_path_by_name_str;
pub use codex_rollout::is_rollout_path;
pub use codex_rollout::read_nonempty_rollout_text;
pub use codex_rollout::read_rollout_text;
pub use codex_rollout::rollout_date_parts;
pub use codex_rollout::strip_rollout_file_suffix;

impl codex_rollout::RolloutConfigView for Config {
fn codex_home(&self) -> &std::path::Path {
Expand Down
14 changes: 8 additions & 6 deletions codex-rs/core/tests/suite/cli_stream.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use assert_cmd::Command as AssertCommand;
use codex_core::auth::CODEX_API_KEY_ENV_VAR;
use codex_core::is_rollout_path;
use codex_core::read_rollout_text;
use codex_git_utils::collect_git_info;
use codex_protocol::protocol::GitInfo;
use codex_utils_cargo_bin::find_resource;
Expand Down Expand Up @@ -375,10 +377,10 @@ async fn integration_creates_and_checks_session_file() -> anyhow::Result<()> {
// Find the session file that contains `marker`.
let marker_clone = marker.clone();
let path = fs_wait::wait_for_matching_file(&sessions_dir, Duration::from_secs(10), move |p| {
if p.extension().and_then(|ext| ext.to_str()) != Some("jsonl") {
if !is_rollout_path(p) {
return false;
}
let Ok(content) = std::fs::read_to_string(p) else {
let Ok(content) = read_rollout_text(p) else {
return false;
};
content.contains(&marker_clone)
Expand Down Expand Up @@ -422,7 +424,7 @@ async fn integration_creates_and_checks_session_file() -> anyhow::Result<()> {
}

let content =
std::fs::read_to_string(&path).unwrap_or_else(|_| panic!("Failed to read session file"));
read_rollout_text(path.as_path()).unwrap_or_else(|_| panic!("Failed to read session file"));
let mut lines = content.lines();
let meta_line = lines
.next()
Expand Down Expand Up @@ -491,10 +493,10 @@ async fn integration_creates_and_checks_session_file() -> anyhow::Result<()> {
let marker2_clone = marker2.clone();
let resumed_path =
fs_wait::wait_for_matching_file(&sessions_dir, Duration::from_secs(10), move |p| {
if p.extension().and_then(|ext| ext.to_str()) != Some("jsonl") {
if !is_rollout_path(p) {
return false;
}
std::fs::read_to_string(p)
read_rollout_text(p)
.map(|content| content.contains(&marker2_clone))
.unwrap_or(false)
})
Expand All @@ -506,7 +508,7 @@ async fn integration_creates_and_checks_session_file() -> anyhow::Result<()> {
"resume should create a new session file"
);

let resumed_content = std::fs::read_to_string(&resumed_path)?;
let resumed_content = read_rollout_text(resumed_path.as_path())?;
assert!(
resumed_content.contains(&marker),
"resumed file missing original marker"
Expand Down
5 changes: 3 additions & 2 deletions codex-rs/core/tests/suite/compact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use codex_core::built_in_model_providers;
use codex_core::compact::SUMMARIZATION_PROMPT;
use codex_core::compact::SUMMARY_PREFIX;
use codex_core::config::Config;
use codex_core::read_rollout_text;
use codex_features::Feature;
use codex_protocol::items::TurnItem;
use codex_protocol::openai_models::ModelInfo;
Expand Down Expand Up @@ -368,7 +369,7 @@ async fn summarize_context_three_requests_and_instructions() {

// Verify rollout contains user-turn TurnContext entries and a Compacted entry.
println!("rollout path: {}", rollout_path.display());
let text = std::fs::read_to_string(&rollout_path).unwrap_or_else(|e| {
let text = read_rollout_text(rollout_path.as_path()).unwrap_or_else(|e| {
panic!(
"failed to read rollout file {}: {e}",
rollout_path.display()
Expand Down Expand Up @@ -2069,7 +2070,7 @@ async fn auto_compact_persists_rollout_entries() {
wait_for_event(&codex, |ev| matches!(ev, EventMsg::ShutdownComplete)).await;

let rollout_path = session_configured.rollout_path.expect("rollout path");
let text = std::fs::read_to_string(&rollout_path).unwrap_or_else(|e| {
let text = read_rollout_text(rollout_path.as_path()).unwrap_or_else(|e| {
panic!(
"failed to read rollout file {}: {e}",
rollout_path.display()
Expand Down
3 changes: 2 additions & 1 deletion codex-rs/core/tests/suite/compact_remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::path::PathBuf;
use anyhow::Result;
use codex_core::CodexAuth;
use codex_core::compact::SUMMARY_PREFIX;
use codex_core::read_rollout_text;
use codex_protocol::items::TurnItem;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
Expand Down Expand Up @@ -1160,7 +1161,7 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()>
assert_eq!(responses_mock.requests().len(), 1);
assert_eq!(compact_mock.requests().len(), 1);

let rollout_text = fs::read_to_string(&rollout_path)?;
let rollout_text = read_rollout_text(rollout_path.as_path())?;
let mut saw_compacted_history = false;
for line in rollout_text
.lines()
Expand Down
3 changes: 2 additions & 1 deletion codex-rs/core/tests/suite/fork_thread.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use codex_core::ForkSnapshot;
use codex_core::NewThread;
use codex_core::parse_turn_item;
use codex_core::read_rollout_text;
use codex_protocol::items::TurnItem;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
Expand Down Expand Up @@ -66,7 +67,7 @@ async fn fork_thread_twice_drops_to_first_message() {

// Helper: read rollout items (excluding SessionMeta) from a JSONL path.
let read_items = |p: &std::path::Path| -> Vec<RolloutItem> {
let text = std::fs::read_to_string(p).expect("read rollout file");
let text = read_rollout_text(p).expect("read rollout file");
let mut items: Vec<RolloutItem> = Vec::new();
for line in text.lines() {
if line.trim().is_empty() {
Expand Down
5 changes: 3 additions & 2 deletions codex-rs/core/tests/suite/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::path::Path;

use anyhow::Context;
use anyhow::Result;
use codex_core::read_rollout_text;
use codex_features::Feature;
use codex_protocol::items::parse_hook_prompt_fragment;
use codex_protocol::models::ContentItem;
Expand Down Expand Up @@ -558,7 +559,7 @@ async fn stop_hook_can_block_multiple_times_in_same_turn() -> Result<()> {
);

let rollout_path = test.codex.rollout_path().expect("rollout path");
let rollout_text = fs::read_to_string(&rollout_path)?;
let rollout_text = read_rollout_text(rollout_path.as_path())?;
let hook_prompt_texts = rollout_hook_prompt_texts(&rollout_text)?;
assert!(
hook_prompt_texts.contains(&FIRST_CONTINUATION_PROMPT.to_string()),
Expand Down Expand Up @@ -746,7 +747,7 @@ async fn multiple_blocking_stop_hooks_persist_multiple_hook_prompt_fragments() -
);

let rollout_path = test.codex.rollout_path().expect("rollout path");
let rollout_text = fs::read_to_string(&rollout_path)?;
let rollout_text = read_rollout_text(rollout_path.as_path())?;
assert_eq!(
rollout_hook_prompt_texts(&rollout_text)?,
vec![
Expand Down
24 changes: 7 additions & 17 deletions codex-rs/core/tests/suite/image_rollout.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::Context;
use codex_core::read_nonempty_rollout_text;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::AskForApproval;
Expand All @@ -22,7 +23,6 @@ use image::ImageBuffer;
use image::Rgba;
use pretty_assertions::assert_eq;
use std::path::Path;
use std::time::Duration;

fn find_user_message_with_image(text: &str) -> Option<ResponseItem> {
for line in text.lines() {
Expand Down Expand Up @@ -58,20 +58,6 @@ fn extract_image_url(item: &ResponseItem) -> Option<String> {
}
}

async fn read_rollout_text(path: &Path) -> anyhow::Result<String> {
for _ in 0..50 {
if path.exists()
&& let Ok(text) = std::fs::read_to_string(path)
&& !text.trim().is_empty()
{
return Ok(text);
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
std::fs::read_to_string(path)
.with_context(|| format!("read rollout file at {}", path.display()))
}

fn write_test_png(path: &Path, color: [u8; 4]) -> anyhow::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
Expand Down Expand Up @@ -138,7 +124,9 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu
wait_for_event(&codex, |event| matches!(event, EventMsg::ShutdownComplete)).await;

let rollout_path = codex.rollout_path().expect("rollout path");
let rollout_text = read_rollout_text(&rollout_path).await?;
let rollout_text = read_nonempty_rollout_text(&rollout_path)
.await
.with_context(|| format!("read rollout file at {}", rollout_path.display()))?;
let actual = find_user_message_with_image(&rollout_text)
.expect("expected user message with input image in rollout");

Expand Down Expand Up @@ -222,7 +210,9 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()>
wait_for_event(&codex, |event| matches!(event, EventMsg::ShutdownComplete)).await;

let rollout_path = codex.rollout_path().expect("rollout path");
let rollout_text = read_rollout_text(&rollout_path).await?;
let rollout_text = read_nonempty_rollout_text(&rollout_path)
.await
.with_context(|| format!("read rollout file at {}", rollout_path.display()))?;
let actual = find_user_message_with_image(&rollout_text)
.expect("expected user message with input image in rollout");

Expand Down
Loading
Loading