mirror of
https://github.com/openai/codex.git
synced 2026-02-27 11:13:46 +00:00
Compare commits
1 Commits
codex/load
...
dh--resume
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d2fddbeba |
@@ -44,6 +44,7 @@ use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::protocol::SessionMeta;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
use codex_state::ThreadMetadataBuilder;
|
||||
|
||||
/// Records all [`ResponseItem`]s for a session and flushes them to disk after
|
||||
@@ -460,6 +461,24 @@ impl RolloutRecorder {
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn read_latest_turn_context(path: &Path) -> std::io::Result<Option<TurnContextItem>> {
|
||||
let text = tokio::fs::read_to_string(path).await?;
|
||||
for line in text.lines().rev() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let rollout_line = match serde_json::from_str::<RolloutLine>(trimmed) {
|
||||
Ok(rollout_line) => rollout_line,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if let RolloutItem::TurnContext(turn_context) = rollout_line.item {
|
||||
return Ok(Some(turn_context));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub async fn shutdown(&self) -> std::io::Result<()> {
|
||||
let (tx_done, rx_done) = oneshot::channel();
|
||||
match self.tx.send(RolloutCmd::Shutdown { ack: tx_done }).await {
|
||||
|
||||
@@ -17,6 +17,7 @@ use time::macros::format_description;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::rollout::INTERACTIVE_SESSION_SOURCES;
|
||||
use crate::rollout::RolloutRecorder;
|
||||
use crate::rollout::list::Cursor;
|
||||
use crate::rollout::list::ThreadItem;
|
||||
use crate::rollout::list::ThreadSortKey;
|
||||
@@ -242,6 +243,121 @@ fn write_session_file_with_meta_payload(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_latest_turn_context_returns_last_turn_context_item() -> Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let rollout_path = temp_dir.path().join("rollout.jsonl");
|
||||
|
||||
let first_turn_context = codex_protocol::protocol::TurnContextItem {
|
||||
cwd: temp_dir.path().join("first"),
|
||||
approval_policy: codex_protocol::protocol::AskForApproval::OnRequest,
|
||||
sandbox_policy: codex_protocol::protocol::SandboxPolicy::ReadOnly,
|
||||
model: "older-model".to_string(),
|
||||
personality: None,
|
||||
collaboration_mode: None,
|
||||
effort: Some(codex_protocol::openai_models::ReasoningEffort::Low),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
user_instructions: None,
|
||||
developer_instructions: None,
|
||||
final_output_json_schema: None,
|
||||
truncation_policy: None,
|
||||
};
|
||||
let latest_turn_context = codex_protocol::protocol::TurnContextItem {
|
||||
cwd: temp_dir.path().join("latest"),
|
||||
approval_policy: codex_protocol::protocol::AskForApproval::OnRequest,
|
||||
sandbox_policy: codex_protocol::protocol::SandboxPolicy::ReadOnly,
|
||||
model: "latest-model".to_string(),
|
||||
personality: None,
|
||||
collaboration_mode: None,
|
||||
effort: Some(codex_protocol::openai_models::ReasoningEffort::High),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Detailed,
|
||||
user_instructions: None,
|
||||
developer_instructions: None,
|
||||
final_output_json_schema: None,
|
||||
truncation_policy: None,
|
||||
};
|
||||
|
||||
let lines = vec![
|
||||
RolloutLine {
|
||||
timestamp: "t0".to_string(),
|
||||
item: RolloutItem::TurnContext(first_turn_context),
|
||||
},
|
||||
RolloutLine {
|
||||
timestamp: "t1".to_string(),
|
||||
item: RolloutItem::ResponseItem(ResponseItem::Message {
|
||||
id: None,
|
||||
role: "assistant".to_string(),
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "hello".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}),
|
||||
},
|
||||
RolloutLine {
|
||||
timestamp: "t2".to_string(),
|
||||
item: RolloutItem::TurnContext(latest_turn_context.clone()),
|
||||
},
|
||||
];
|
||||
let mut text = String::new();
|
||||
for line in lines {
|
||||
text.push_str(&serde_json::to_string(&line)?);
|
||||
text.push('\n');
|
||||
}
|
||||
std::fs::write(&rollout_path, text)?;
|
||||
|
||||
let latest = RolloutRecorder::read_latest_turn_context(&rollout_path)
|
||||
.await?
|
||||
.expect("latest turn context");
|
||||
assert_eq!(
|
||||
serde_json::to_value(&latest)?,
|
||||
serde_json::to_value(&latest_turn_context)?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_latest_turn_context_skips_invalid_json_lines() -> Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let rollout_path = temp_dir.path().join("rollout.jsonl");
|
||||
|
||||
let latest_turn_context = codex_protocol::protocol::TurnContextItem {
|
||||
cwd: temp_dir.path().join("latest"),
|
||||
approval_policy: codex_protocol::protocol::AskForApproval::OnRequest,
|
||||
sandbox_policy: codex_protocol::protocol::SandboxPolicy::ReadOnly,
|
||||
model: "latest-model".to_string(),
|
||||
personality: None,
|
||||
collaboration_mode: None,
|
||||
effort: Some(codex_protocol::openai_models::ReasoningEffort::High),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Detailed,
|
||||
user_instructions: None,
|
||||
developer_instructions: None,
|
||||
final_output_json_schema: None,
|
||||
truncation_policy: None,
|
||||
};
|
||||
|
||||
let latest_line = RolloutLine {
|
||||
timestamp: "t1".to_string(),
|
||||
item: RolloutItem::TurnContext(latest_turn_context.clone()),
|
||||
};
|
||||
std::fs::write(
|
||||
&rollout_path,
|
||||
format!(
|
||||
"{{not-valid-json}}\n{}\n",
|
||||
serde_json::to_string(&latest_line)?
|
||||
),
|
||||
)?;
|
||||
|
||||
let latest = RolloutRecorder::read_latest_turn_context(&rollout_path)
|
||||
.await?
|
||||
.expect("latest turn context");
|
||||
assert_eq!(
|
||||
serde_json::to_value(&latest)?,
|
||||
serde_json::to_value(&latest_turn_context)?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_conversations_latest_first() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
||||
@@ -37,8 +37,7 @@ use codex_core::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use codex_protocol::config_types::AltScreenMode;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
use codex_state::log_db;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use cwd_prompt::CwdPromptAction;
|
||||
@@ -655,7 +654,7 @@ async fn run_ratatui_app(
|
||||
None => None,
|
||||
};
|
||||
|
||||
let config = match &session_selection {
|
||||
let mut config = match &session_selection {
|
||||
resume_picker::SessionSelection::Resume(_) | resume_picker::SessionSelection::Fork(_) => {
|
||||
load_config_or_exit_with_fallback_cwd(
|
||||
cli_kv_overrides.clone(),
|
||||
@@ -667,6 +666,8 @@ async fn run_ratatui_app(
|
||||
}
|
||||
_ => config,
|
||||
};
|
||||
maybe_apply_resume_turn_context_settings(&mut config, &session_selection, cli.model.is_some())
|
||||
.await;
|
||||
let active_profile = config.active_profile.clone();
|
||||
let should_show_trust_screen = should_show_trust_screen(&config);
|
||||
|
||||
@@ -708,7 +709,7 @@ pub(crate) async fn read_session_cwd(path: &Path) -> Option<PathBuf> {
|
||||
// mutating the SessionMeta line when the session cwd changes, but the rollout
|
||||
// is an append-only JSONL log and rewriting the head would be error-prone.
|
||||
// When rollouts move to SQLite, we can drop this scan.
|
||||
if let Some(cwd) = parse_latest_turn_context_cwd(path).await {
|
||||
if let Some(cwd) = read_latest_turn_context(path).await.map(|ctx| ctx.cwd) {
|
||||
return Some(cwd);
|
||||
}
|
||||
match read_session_meta_line(path).await {
|
||||
@@ -725,21 +726,41 @@ pub(crate) async fn read_session_cwd(path: &Path) -> Option<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn parse_latest_turn_context_cwd(path: &Path) -> Option<PathBuf> {
|
||||
let text = tokio::fs::read_to_string(path).await.ok()?;
|
||||
for line in text.lines().rev() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
async fn maybe_apply_resume_turn_context_settings(
|
||||
config: &mut Config,
|
||||
session_selection: &resume_picker::SessionSelection,
|
||||
cli_model_override: bool,
|
||||
) {
|
||||
if cli_model_override {
|
||||
return;
|
||||
}
|
||||
let rollout_path = match session_selection {
|
||||
resume_picker::SessionSelection::Resume(path)
|
||||
| resume_picker::SessionSelection::Fork(path) => path,
|
||||
resume_picker::SessionSelection::StartFresh | resume_picker::SessionSelection::Exit => {
|
||||
return;
|
||||
}
|
||||
let Ok(rollout_line) = serde_json::from_str::<RolloutLine>(trimmed) else {
|
||||
continue;
|
||||
};
|
||||
if let RolloutItem::TurnContext(item) = rollout_line.item {
|
||||
return Some(item.cwd);
|
||||
};
|
||||
if let Some(turn_context) = read_latest_turn_context(rollout_path).await {
|
||||
config.model = Some(turn_context.model);
|
||||
config.model_reasoning_effort = turn_context.effort;
|
||||
config.model_reasoning_summary = turn_context.summary;
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_latest_turn_context(path: &Path) -> Option<TurnContextItem> {
|
||||
match RolloutRecorder::read_latest_turn_context(path).await {
|
||||
Ok(turn_context) => turn_context,
|
||||
Err(err) => {
|
||||
let rollout_path = path.display().to_string();
|
||||
tracing::warn!(
|
||||
%rollout_path,
|
||||
%err,
|
||||
"Failed to read latest turn context from rollout"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn cwds_differ(current_cwd: &Path, session_cwd: &Path) -> bool {
|
||||
@@ -1019,6 +1040,154 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_rehydrates_model_effort_and_summary_from_latest_turn_context()
|
||||
-> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let mut config = build_config(&temp_dir).await?;
|
||||
config.model = Some("current-model".to_string());
|
||||
config.model_reasoning_effort = None;
|
||||
|
||||
let first_cwd = temp_dir.path().join("first");
|
||||
let latest_cwd = temp_dir.path().join("latest");
|
||||
std::fs::create_dir_all(&first_cwd)?;
|
||||
std::fs::create_dir_all(&latest_cwd)?;
|
||||
|
||||
let rollout_path = temp_dir.path().join("rollout.jsonl");
|
||||
let mut first_context = build_turn_context(&config, first_cwd);
|
||||
first_context.model = "older-model".to_string();
|
||||
first_context.effort = Some(codex_protocol::openai_models::ReasoningEffort::Low);
|
||||
first_context.summary = codex_protocol::config_types::ReasoningSummary::Auto;
|
||||
let mut latest_context = build_turn_context(&config, latest_cwd);
|
||||
latest_context.model = "latest-model".to_string();
|
||||
latest_context.effort = Some(codex_protocol::openai_models::ReasoningEffort::High);
|
||||
latest_context.summary = codex_protocol::config_types::ReasoningSummary::Detailed;
|
||||
let lines = vec![
|
||||
RolloutLine {
|
||||
timestamp: "t0".to_string(),
|
||||
item: RolloutItem::TurnContext(first_context),
|
||||
},
|
||||
RolloutLine {
|
||||
timestamp: "t1".to_string(),
|
||||
item: RolloutItem::TurnContext(latest_context),
|
||||
},
|
||||
];
|
||||
let mut text = String::new();
|
||||
for line in lines {
|
||||
text.push_str(&serde_json::to_string(&line).expect("serialize rollout"));
|
||||
text.push('\n');
|
||||
}
|
||||
std::fs::write(&rollout_path, text)?;
|
||||
|
||||
maybe_apply_resume_turn_context_settings(
|
||||
&mut config,
|
||||
&resume_picker::SessionSelection::Resume(rollout_path),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(config.model.as_deref(), Some("latest-model"));
|
||||
assert_eq!(
|
||||
config.model_reasoning_effort,
|
||||
Some(codex_protocol::openai_models::ReasoningEffort::High)
|
||||
);
|
||||
assert_eq!(
|
||||
config.model_reasoning_summary,
|
||||
codex_protocol::config_types::ReasoningSummary::Detailed
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_does_not_rehydrate_model_when_cli_model_override_present() -> std::io::Result<()>
|
||||
{
|
||||
let temp_dir = TempDir::new()?;
|
||||
let mut config = build_config(&temp_dir).await?;
|
||||
config.model = Some("cli-model".to_string());
|
||||
config.model_reasoning_effort =
|
||||
Some(codex_protocol::openai_models::ReasoningEffort::Medium);
|
||||
|
||||
let latest_cwd = temp_dir.path().join("latest");
|
||||
std::fs::create_dir_all(&latest_cwd)?;
|
||||
let rollout_path = temp_dir.path().join("rollout.jsonl");
|
||||
let mut latest_context = build_turn_context(&config, latest_cwd);
|
||||
latest_context.model = "history-model".to_string();
|
||||
latest_context.effort = Some(codex_protocol::openai_models::ReasoningEffort::Low);
|
||||
latest_context.summary = codex_protocol::config_types::ReasoningSummary::Concise;
|
||||
let line = RolloutLine {
|
||||
timestamp: "t0".to_string(),
|
||||
item: RolloutItem::TurnContext(latest_context),
|
||||
};
|
||||
std::fs::write(
|
||||
&rollout_path,
|
||||
format!(
|
||||
"{}\n",
|
||||
serde_json::to_string(&line).expect("serialize rollout")
|
||||
),
|
||||
)?;
|
||||
|
||||
maybe_apply_resume_turn_context_settings(
|
||||
&mut config,
|
||||
&resume_picker::SessionSelection::Resume(rollout_path),
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(config.model.as_deref(), Some("cli-model"));
|
||||
assert_eq!(
|
||||
config.model_reasoning_effort,
|
||||
Some(codex_protocol::openai_models::ReasoningEffort::Medium)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fork_rehydrates_model_effort_and_summary_from_latest_turn_context()
|
||||
-> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let mut config = build_config(&temp_dir).await?;
|
||||
config.model = Some("current-model".to_string());
|
||||
config.model_reasoning_effort = Some(codex_protocol::openai_models::ReasoningEffort::Low);
|
||||
config.model_reasoning_summary = codex_protocol::config_types::ReasoningSummary::Concise;
|
||||
|
||||
let latest_cwd = temp_dir.path().join("latest");
|
||||
std::fs::create_dir_all(&latest_cwd)?;
|
||||
let rollout_path = temp_dir.path().join("rollout.jsonl");
|
||||
let mut latest_context = build_turn_context(&config, latest_cwd);
|
||||
latest_context.model = "forked-history-model".to_string();
|
||||
latest_context.effort = Some(codex_protocol::openai_models::ReasoningEffort::High);
|
||||
latest_context.summary = codex_protocol::config_types::ReasoningSummary::Detailed;
|
||||
let line = RolloutLine {
|
||||
timestamp: "t0".to_string(),
|
||||
item: RolloutItem::TurnContext(latest_context),
|
||||
};
|
||||
std::fs::write(
|
||||
&rollout_path,
|
||||
format!(
|
||||
"{}\n",
|
||||
serde_json::to_string(&line).expect("serialize rollout")
|
||||
),
|
||||
)?;
|
||||
|
||||
maybe_apply_resume_turn_context_settings(
|
||||
&mut config,
|
||||
&resume_picker::SessionSelection::Fork(rollout_path),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(config.model.as_deref(), Some("forked-history-model"));
|
||||
assert_eq!(
|
||||
config.model_reasoning_effort,
|
||||
Some(codex_protocol::openai_models::ReasoningEffort::High)
|
||||
);
|
||||
assert_eq!(
|
||||
config.model_reasoning_summary,
|
||||
codex_protocol::config_types::ReasoningSummary::Detailed
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_session_cwd_prefers_latest_turn_context() -> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
|
||||
Reference in New Issue
Block a user