mirror of
https://github.com/openai/codex.git
synced 2026-03-03 05:03:20 +00:00
Compare commits
1 Commits
fix/notify
...
codex/modi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bc48e2d85 |
@@ -115,7 +115,8 @@ impl ConversationManager {
|
||||
rollout_path: PathBuf,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
) -> CodexResult<NewConversation> {
|
||||
let initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?;
|
||||
let initial_history =
|
||||
RolloutRecorder::get_rollout_history(&rollout_path, &config.cwd).await?;
|
||||
let CodexSpawnOk {
|
||||
codex,
|
||||
conversation_id,
|
||||
@@ -145,7 +146,7 @@ impl ConversationManager {
|
||||
path: PathBuf,
|
||||
) -> CodexResult<NewConversation> {
|
||||
// Compute the prefix up to the cut point.
|
||||
let history = RolloutRecorder::get_rollout_history(&path).await?;
|
||||
let history = RolloutRecorder::get_rollout_history(&path, &config.cwd).await?;
|
||||
let history = truncate_after_nth_user_message(history, nth_user_message);
|
||||
|
||||
// Spawn a new conversation with the computed initial history.
|
||||
|
||||
@@ -17,6 +17,7 @@ use super::SESSIONS_SUBDIR;
|
||||
use crate::protocol::EventMsg;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
|
||||
/// Returned page of conversation summaries.
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
@@ -87,6 +88,7 @@ impl<'de> serde::Deserialize<'de> for Cursor {
|
||||
/// concurrent new sessions being appended. Ordering is stable by timestamp desc, then UUID desc.
|
||||
pub(crate) async fn get_conversations(
|
||||
codex_home: &Path,
|
||||
cwd: &Path,
|
||||
page_size: usize,
|
||||
cursor: Option<&Cursor>,
|
||||
) -> io::Result<ConversationsPage> {
|
||||
@@ -104,14 +106,26 @@ pub(crate) async fn get_conversations(
|
||||
|
||||
let anchor = cursor.cloned();
|
||||
|
||||
let result = traverse_directories_for_paths(root.clone(), page_size, anchor).await?;
|
||||
let result = traverse_directories_for_paths(root.clone(), cwd, page_size, anchor).await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Load the full contents of a single conversation session file at `path`.
|
||||
/// Returns the entire file contents as a String.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn get_conversation(path: &Path) -> io::Result<String> {
|
||||
pub(crate) async fn get_conversation(path: &Path, cwd: &Path) -> io::Result<String> {
|
||||
let (_head, session_meta, _, _) = read_head_and_flags(path, 1).await?;
|
||||
let Some(meta) = session_meta else {
|
||||
return Err(io::Error::other("missing session meta in rollout file"));
|
||||
};
|
||||
if meta.meta.cwd != cwd {
|
||||
return Err(io::Error::other(format!(
|
||||
"session cwd `{}` does not match requested cwd `{}`",
|
||||
meta.meta.cwd.display(),
|
||||
cwd.display()
|
||||
)));
|
||||
}
|
||||
|
||||
tokio::fs::read_to_string(path).await
|
||||
}
|
||||
|
||||
@@ -121,6 +135,7 @@ pub(crate) async fn get_conversation(path: &Path) -> io::Result<String> {
|
||||
/// Returned newest (latest) first.
|
||||
async fn traverse_directories_for_paths(
|
||||
root: PathBuf,
|
||||
cwd: &Path,
|
||||
page_size: usize,
|
||||
anchor: Option<Cursor>,
|
||||
) -> io::Result<ConversationsPage> {
|
||||
@@ -176,12 +191,20 @@ async fn traverse_directories_for_paths(
|
||||
}
|
||||
// Read head and simultaneously detect message events within the same
|
||||
// first N JSONL records to avoid a second file read.
|
||||
let (head, saw_session_meta, saw_user_event) =
|
||||
read_head_and_flags(&path, HEAD_RECORD_LIMIT)
|
||||
.await
|
||||
.unwrap_or((Vec::new(), false, false));
|
||||
// Apply filters: must have session meta and at least one user message event
|
||||
if saw_session_meta && saw_user_event {
|
||||
let (head, session_meta, saw_session_meta, saw_user_event) =
|
||||
match read_head_and_flags(&path, HEAD_RECORD_LIMIT).await {
|
||||
Ok(res) => res,
|
||||
Err(_) => continue,
|
||||
};
|
||||
// Apply filters: must have session meta, matching cwd, and at least one
|
||||
// user message event
|
||||
if saw_session_meta
|
||||
&& saw_user_event
|
||||
&& session_meta
|
||||
.as_ref()
|
||||
.map(|meta| meta.meta.cwd.as_path() == cwd)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
items.push(ConversationItem { path, head });
|
||||
}
|
||||
}
|
||||
@@ -289,7 +312,7 @@ fn parse_timestamp_uuid_from_filename(name: &str) -> Option<(OffsetDateTime, Uui
|
||||
async fn read_head_and_flags(
|
||||
path: &Path,
|
||||
max_records: usize,
|
||||
) -> io::Result<(Vec<serde_json::Value>, bool, bool)> {
|
||||
) -> io::Result<(Vec<serde_json::Value>, Option<SessionMetaLine>, bool, bool)> {
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
|
||||
let file = tokio::fs::File::open(path).await?;
|
||||
@@ -298,6 +321,7 @@ async fn read_head_and_flags(
|
||||
let mut head: Vec<serde_json::Value> = Vec::new();
|
||||
let mut saw_session_meta = false;
|
||||
let mut saw_user_event = false;
|
||||
let mut session_meta: Option<SessionMetaLine> = None;
|
||||
|
||||
while head.len() < max_records {
|
||||
let line_opt = lines.next_line().await?;
|
||||
@@ -312,10 +336,13 @@ async fn read_head_and_flags(
|
||||
|
||||
match rollout_line.item {
|
||||
RolloutItem::SessionMeta(session_meta_line) => {
|
||||
if let Ok(val) = serde_json::to_value(session_meta_line) {
|
||||
if let Ok(val) = serde_json::to_value(&session_meta_line) {
|
||||
head.push(val);
|
||||
saw_session_meta = true;
|
||||
}
|
||||
if session_meta.is_none() {
|
||||
session_meta = Some(session_meta_line);
|
||||
}
|
||||
}
|
||||
RolloutItem::ResponseItem(item) => {
|
||||
if let Ok(val) = serde_json::to_value(item) {
|
||||
@@ -336,7 +363,7 @@ async fn read_head_and_flags(
|
||||
}
|
||||
}
|
||||
|
||||
Ok((head, saw_session_meta, saw_user_event))
|
||||
Ok((head, session_meta, saw_session_meta, saw_user_event))
|
||||
}
|
||||
|
||||
/// Locate a recorded conversation rollout file by its UUID string using the existing
|
||||
|
||||
@@ -103,10 +103,11 @@ impl RolloutRecorder {
|
||||
/// List conversations (rollout files) under the provided Codex home directory.
|
||||
pub async fn list_conversations(
|
||||
codex_home: &Path,
|
||||
cwd: &Path,
|
||||
page_size: usize,
|
||||
cursor: Option<&Cursor>,
|
||||
) -> std::io::Result<ConversationsPage> {
|
||||
get_conversations(codex_home, page_size, cursor).await
|
||||
get_conversations(codex_home, cwd, page_size, cursor).await
|
||||
}
|
||||
|
||||
/// Attempt to create a new [`RolloutRecorder`]. If the sessions directory
|
||||
@@ -202,7 +203,10 @@ impl RolloutRecorder {
|
||||
.map_err(|e| IoError::other(format!("failed waiting for rollout flush: {e}")))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_rollout_history(path: &Path) -> std::io::Result<InitialHistory> {
|
||||
pub(crate) async fn get_rollout_history(
|
||||
path: &Path,
|
||||
cwd: &Path,
|
||||
) -> std::io::Result<InitialHistory> {
|
||||
info!("Resuming rollout from {path:?}");
|
||||
let text = tokio::fs::read_to_string(path).await?;
|
||||
if text.trim().is_empty() {
|
||||
@@ -211,6 +215,7 @@ impl RolloutRecorder {
|
||||
|
||||
let mut items: Vec<RolloutItem> = Vec::new();
|
||||
let mut conversation_id: Option<ConversationId> = None;
|
||||
let mut session_cwd: Option<PathBuf> = None;
|
||||
for line in text.lines() {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
@@ -232,6 +237,9 @@ impl RolloutRecorder {
|
||||
if conversation_id.is_none() {
|
||||
conversation_id = Some(session_meta_line.meta.id);
|
||||
}
|
||||
if session_cwd.is_none() {
|
||||
session_cwd = Some(session_meta_line.meta.cwd.clone());
|
||||
}
|
||||
items.push(RolloutItem::SessionMeta(session_meta_line));
|
||||
}
|
||||
RolloutItem::ResponseItem(item) => {
|
||||
@@ -261,6 +269,22 @@ impl RolloutRecorder {
|
||||
let conversation_id = conversation_id
|
||||
.ok_or_else(|| IoError::other("failed to parse conversation ID from rollout file"))?;
|
||||
|
||||
match session_cwd {
|
||||
Some(meta_cwd) if meta_cwd == cwd => {}
|
||||
Some(meta_cwd) => {
|
||||
return Err(IoError::other(format!(
|
||||
"session cwd `{}` does not match requested cwd `{}`",
|
||||
meta_cwd.display(),
|
||||
cwd.display()
|
||||
)));
|
||||
}
|
||||
None => {
|
||||
return Err(IoError::other(
|
||||
"failed to parse session cwd from rollout file",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if items.is_empty() {
|
||||
return Ok(InitialHistory::New);
|
||||
}
|
||||
|
||||
@@ -17,12 +17,14 @@ use crate::rollout::list::ConversationsPage;
|
||||
use crate::rollout::list::Cursor;
|
||||
use crate::rollout::list::get_conversation;
|
||||
use crate::rollout::list::get_conversations;
|
||||
use crate::rollout::recorder::RolloutRecorder;
|
||||
|
||||
fn write_session_file(
|
||||
root: &Path,
|
||||
ts_str: &str,
|
||||
uuid: Uuid,
|
||||
num_records: usize,
|
||||
cwd: &Path,
|
||||
) -> std::io::Result<(OffsetDateTime, Uuid)> {
|
||||
let format: &[FormatItem] =
|
||||
format_description!("[year]-[month]-[day]T[hour]-[minute]-[second]");
|
||||
@@ -40,6 +42,7 @@ fn write_session_file(
|
||||
let file_path = dir.join(filename);
|
||||
let mut file = File::create(file_path)?;
|
||||
|
||||
let cwd_str = cwd.to_string_lossy();
|
||||
let meta = serde_json::json!({
|
||||
"timestamp": ts_str,
|
||||
"type": "session_meta",
|
||||
@@ -47,7 +50,7 @@ fn write_session_file(
|
||||
"id": uuid,
|
||||
"timestamp": ts_str,
|
||||
"instructions": null,
|
||||
"cwd": ".",
|
||||
"cwd": cwd_str,
|
||||
"originator": "test_originator",
|
||||
"cli_version": "test_version"
|
||||
}
|
||||
@@ -86,12 +89,13 @@ async fn test_list_conversations_latest_first() {
|
||||
let u2 = Uuid::from_u128(2);
|
||||
let u3 = Uuid::from_u128(3);
|
||||
|
||||
let cwd = Path::new(".");
|
||||
// Create three sessions across three days
|
||||
write_session_file(home, "2025-01-01T12-00-00", u1, 3).unwrap();
|
||||
write_session_file(home, "2025-01-02T12-00-00", u2, 3).unwrap();
|
||||
write_session_file(home, "2025-01-03T12-00-00", u3, 3).unwrap();
|
||||
write_session_file(home, "2025-01-01T12-00-00", u1, 3, cwd).unwrap();
|
||||
write_session_file(home, "2025-01-02T12-00-00", u2, 3, cwd).unwrap();
|
||||
write_session_file(home, "2025-01-03T12-00-00", u3, 3, cwd).unwrap();
|
||||
|
||||
let page = get_conversations(home, 10, None).await.unwrap();
|
||||
let page = get_conversations(home, cwd, 10, None).await.unwrap();
|
||||
|
||||
// Build expected objects
|
||||
let p1 = home
|
||||
@@ -164,6 +168,46 @@ async fn test_list_conversations_latest_first() {
|
||||
assert_eq!(page, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_conversations_filters_by_cwd() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let home = temp.path();
|
||||
|
||||
let match_cwd = Path::new("/match");
|
||||
let other_cwd = Path::new("/other");
|
||||
|
||||
let id_match = Uuid::from_u128(11);
|
||||
let id_other = Uuid::from_u128(22);
|
||||
|
||||
write_session_file(home, "2025-02-01T00-00-00", id_match, 1, match_cwd).unwrap();
|
||||
write_session_file(home, "2025-02-02T00-00-00", id_other, 1, other_cwd).unwrap();
|
||||
|
||||
let page_match = get_conversations(home, match_cwd, 10, None).await.unwrap();
|
||||
assert_eq!(page_match.items.len(), 1);
|
||||
let head_id = page_match.items[0]
|
||||
.head
|
||||
.first()
|
||||
.and_then(|meta| meta.get("id"))
|
||||
.and_then(|id| id.as_str())
|
||||
.expect("id str");
|
||||
assert_eq!(head_id, id_match.to_string());
|
||||
|
||||
let page_other = get_conversations(home, other_cwd, 10, None).await.unwrap();
|
||||
assert_eq!(page_other.items.len(), 1);
|
||||
let other_head_id = page_other.items[0]
|
||||
.head
|
||||
.first()
|
||||
.and_then(|meta| meta.get("id"))
|
||||
.and_then(|id| id.as_str())
|
||||
.expect("id str");
|
||||
assert_eq!(other_head_id, id_other.to_string());
|
||||
|
||||
let none_page = get_conversations(home, Path::new("/missing"), 10, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(none_page.items.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_pagination_cursor() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
@@ -177,13 +221,15 @@ async fn test_pagination_cursor() {
|
||||
let u5 = Uuid::from_u128(55);
|
||||
|
||||
// Oldest to newest
|
||||
write_session_file(home, "2025-03-01T09-00-00", u1, 1).unwrap();
|
||||
write_session_file(home, "2025-03-02T09-00-00", u2, 1).unwrap();
|
||||
write_session_file(home, "2025-03-03T09-00-00", u3, 1).unwrap();
|
||||
write_session_file(home, "2025-03-04T09-00-00", u4, 1).unwrap();
|
||||
write_session_file(home, "2025-03-05T09-00-00", u5, 1).unwrap();
|
||||
write_session_file(home, "2025-03-01T09-00-00", u1, 1, Path::new(".")).unwrap();
|
||||
write_session_file(home, "2025-03-02T09-00-00", u2, 1, Path::new(".")).unwrap();
|
||||
write_session_file(home, "2025-03-03T09-00-00", u3, 1, Path::new(".")).unwrap();
|
||||
write_session_file(home, "2025-03-04T09-00-00", u4, 1, Path::new(".")).unwrap();
|
||||
write_session_file(home, "2025-03-05T09-00-00", u5, 1, Path::new(".")).unwrap();
|
||||
|
||||
let page1 = get_conversations(home, 2, None).await.unwrap();
|
||||
let page1 = get_conversations(home, Path::new("."), 2, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let p5 = home
|
||||
.join("sessions")
|
||||
.join("2025")
|
||||
@@ -231,7 +277,7 @@ async fn test_pagination_cursor() {
|
||||
};
|
||||
assert_eq!(page1, expected_page1);
|
||||
|
||||
let page2 = get_conversations(home, 2, page1.next_cursor.as_ref())
|
||||
let page2 = get_conversations(home, Path::new("."), 2, page1.next_cursor.as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
let p3 = home
|
||||
@@ -281,7 +327,7 @@ async fn test_pagination_cursor() {
|
||||
};
|
||||
assert_eq!(page2, expected_page2);
|
||||
|
||||
let page3 = get_conversations(home, 2, page2.next_cursor.as_ref())
|
||||
let page3 = get_conversations(home, Path::new("."), 2, page2.next_cursor.as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
let p1 = home
|
||||
@@ -319,12 +365,14 @@ async fn test_get_conversation_contents() {
|
||||
|
||||
let uuid = Uuid::new_v4();
|
||||
let ts = "2025-04-01T10-30-00";
|
||||
write_session_file(home, ts, uuid, 2).unwrap();
|
||||
write_session_file(home, ts, uuid, 2, Path::new(".")).unwrap();
|
||||
|
||||
let page = get_conversations(home, 1, None).await.unwrap();
|
||||
let page = get_conversations(home, Path::new("."), 1, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let path = &page.items[0].path;
|
||||
|
||||
let content = get_conversation(path).await.unwrap();
|
||||
let content = get_conversation(path, Path::new(".")).await.unwrap();
|
||||
|
||||
// Page equality (single item)
|
||||
let expected_path = home
|
||||
@@ -376,11 +424,13 @@ async fn test_stable_ordering_same_second_pagination() {
|
||||
let u2 = Uuid::from_u128(2);
|
||||
let u3 = Uuid::from_u128(3);
|
||||
|
||||
write_session_file(home, ts, u1, 0).unwrap();
|
||||
write_session_file(home, ts, u2, 0).unwrap();
|
||||
write_session_file(home, ts, u3, 0).unwrap();
|
||||
write_session_file(home, ts, u1, 0, Path::new(".")).unwrap();
|
||||
write_session_file(home, ts, u2, 0, Path::new(".")).unwrap();
|
||||
write_session_file(home, ts, u3, 0, Path::new(".")).unwrap();
|
||||
|
||||
let page1 = get_conversations(home, 2, None).await.unwrap();
|
||||
let page1 = get_conversations(home, Path::new("."), 2, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let p3 = home
|
||||
.join("sessions")
|
||||
@@ -422,7 +472,7 @@ async fn test_stable_ordering_same_second_pagination() {
|
||||
};
|
||||
assert_eq!(page1, expected_page1);
|
||||
|
||||
let page2 = get_conversations(home, 2, page1.next_cursor.as_ref())
|
||||
let page2 = get_conversations(home, Path::new("."), 2, page1.next_cursor.as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
let p1 = home
|
||||
@@ -443,3 +493,31 @@ async fn test_stable_ordering_same_second_pagination() {
|
||||
};
|
||||
assert_eq!(page2, expected_page2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_rollout_history_enforces_cwd() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let home = temp.path();
|
||||
|
||||
let ts = "2025-08-01T00-00-00";
|
||||
let uuid = Uuid::new_v4();
|
||||
let match_cwd = Path::new("/history-match");
|
||||
|
||||
write_session_file(home, ts, uuid, 1, match_cwd).unwrap();
|
||||
|
||||
let path = home
|
||||
.join("sessions")
|
||||
.join("2025")
|
||||
.join("08")
|
||||
.join("01")
|
||||
.join(format!("rollout-{ts}-{uuid}.jsonl"));
|
||||
|
||||
RolloutRecorder::get_rollout_history(&path, match_cwd)
|
||||
.await
|
||||
.expect("matching cwd should succeed");
|
||||
|
||||
let err = RolloutRecorder::get_rollout_history(&path, Path::new("/history-other"))
|
||||
.await
|
||||
.expect_err("mismatched cwd should error");
|
||||
assert!(err.to_string().contains("does not match"));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use assert_cmd::Command as AssertCommand;
|
||||
use codex_core::RolloutRecorder;
|
||||
use codex_core::protocol::GitInfo;
|
||||
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use tempfile::TempDir;
|
||||
@@ -81,7 +82,8 @@ async fn chat_mode_stream_cli() {
|
||||
server.verify().await;
|
||||
|
||||
// Verify a new session rollout was created and is discoverable via list_conversations
|
||||
let page = RolloutRecorder::list_conversations(home.path(), 10, None)
|
||||
let cwd = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let page = RolloutRecorder::list_conversations(home.path(), cwd, 10, None)
|
||||
.await
|
||||
.expect("list conversations");
|
||||
assert!(
|
||||
|
||||
@@ -8,12 +8,13 @@ use uuid::Uuid;
|
||||
|
||||
/// Create sessions/YYYY/MM/DD and write a minimal rollout file containing the
|
||||
/// provided conversation id in the SessionMeta line. Returns the absolute path.
|
||||
fn write_minimal_rollout_with_id(codex_home: &TempDir, id: Uuid) -> PathBuf {
|
||||
fn write_minimal_rollout_with_id(codex_home: &TempDir, id: Uuid, cwd: &std::path::Path) -> PathBuf {
|
||||
let sessions = codex_home.path().join("sessions/2024/01/01");
|
||||
std::fs::create_dir_all(&sessions).unwrap();
|
||||
|
||||
let file = sessions.join(format!("rollout-2024-01-01T00-00-00-{id}.jsonl"));
|
||||
let mut f = std::fs::File::create(&file).unwrap();
|
||||
let cwd_str = cwd.to_string_lossy();
|
||||
// Minimal first line: session_meta with the id so content search can find it
|
||||
writeln!(
|
||||
f,
|
||||
@@ -25,7 +26,7 @@ fn write_minimal_rollout_with_id(codex_home: &TempDir, id: Uuid) -> PathBuf {
|
||||
"id": id,
|
||||
"timestamp": "2024-01-01T00:00:00Z",
|
||||
"instructions": null,
|
||||
"cwd": ".",
|
||||
"cwd": cwd_str,
|
||||
"originator": "test",
|
||||
"cli_version": "test"
|
||||
}
|
||||
@@ -40,7 +41,8 @@ fn write_minimal_rollout_with_id(codex_home: &TempDir, id: Uuid) -> PathBuf {
|
||||
async fn find_locates_rollout_file_by_id() {
|
||||
let home = TempDir::new().unwrap();
|
||||
let id = Uuid::new_v4();
|
||||
let expected = write_minimal_rollout_with_id(&home, id);
|
||||
let cwd = std::path::Path::new(".");
|
||||
let expected = write_minimal_rollout_with_id(&home, id, cwd);
|
||||
|
||||
let found = find_conversation_path_by_id_str(home.path(), &id.to_string())
|
||||
.await
|
||||
|
||||
@@ -313,7 +313,14 @@ async fn resolve_resume_path(
|
||||
args: &crate::cli::ResumeArgs,
|
||||
) -> anyhow::Result<Option<PathBuf>> {
|
||||
if args.last {
|
||||
match codex_core::RolloutRecorder::list_conversations(&config.codex_home, 1, None).await {
|
||||
match codex_core::RolloutRecorder::list_conversations(
|
||||
&config.codex_home,
|
||||
&config.cwd,
|
||||
1,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(page) => Ok(page.items.first().map(|it| it.path.clone())),
|
||||
Err(e) => {
|
||||
error!("Error listing conversations: {e}");
|
||||
|
||||
@@ -675,6 +675,7 @@ impl CodexMessageProcessor {
|
||||
|
||||
let page = match RolloutRecorder::list_conversations(
|
||||
&self.config.codex_home,
|
||||
&self.config.cwd,
|
||||
page_size,
|
||||
cursor_ref,
|
||||
)
|
||||
|
||||
@@ -23,23 +23,27 @@ const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs
|
||||
async fn test_list_and_resume_conversations() {
|
||||
// Prepare a temporary CODEX_HOME with a few fake rollout files.
|
||||
let codex_home = TempDir::new().expect("create temp dir");
|
||||
let cwd = std::env::current_dir().expect("current dir");
|
||||
create_fake_rollout(
|
||||
codex_home.path(),
|
||||
"2025-01-02T12-00-00",
|
||||
"2025-01-02T12:00:00Z",
|
||||
"Hello A",
|
||||
&cwd,
|
||||
);
|
||||
create_fake_rollout(
|
||||
codex_home.path(),
|
||||
"2025-01-01T13-00-00",
|
||||
"2025-01-01T13:00:00Z",
|
||||
"Hello B",
|
||||
&cwd,
|
||||
);
|
||||
create_fake_rollout(
|
||||
codex_home.path(),
|
||||
"2025-01-01T12-00-00",
|
||||
"2025-01-01T12:00:00Z",
|
||||
"Hello C",
|
||||
&cwd,
|
||||
);
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path())
|
||||
@@ -145,7 +149,13 @@ async fn test_list_and_resume_conversations() {
|
||||
let _: uuid::Uuid = conversation_id.into();
|
||||
}
|
||||
|
||||
fn create_fake_rollout(codex_home: &Path, filename_ts: &str, meta_rfc3339: &str, preview: &str) {
|
||||
fn create_fake_rollout(
|
||||
codex_home: &Path,
|
||||
filename_ts: &str,
|
||||
meta_rfc3339: &str,
|
||||
preview: &str,
|
||||
cwd: &Path,
|
||||
) {
|
||||
let uuid = Uuid::new_v4();
|
||||
// sessions/YYYY/MM/DD/ derived from filename_ts (YYYY-MM-DDThh-mm-ss)
|
||||
let year = &filename_ts[0..4];
|
||||
@@ -156,6 +166,7 @@ fn create_fake_rollout(codex_home: &Path, filename_ts: &str, meta_rfc3339: &str,
|
||||
|
||||
let file_path = dir.join(format!("rollout-{filename_ts}-{uuid}.jsonl"));
|
||||
let mut lines = Vec::new();
|
||||
let cwd_str = cwd.to_string_lossy();
|
||||
// Meta line with timestamp (flattened meta in payload for new schema)
|
||||
lines.push(
|
||||
json!({
|
||||
@@ -164,7 +175,7 @@ fn create_fake_rollout(codex_home: &Path, filename_ts: &str, meta_rfc3339: &str,
|
||||
"payload": {
|
||||
"id": uuid,
|
||||
"timestamp": meta_rfc3339,
|
||||
"cwd": "/",
|
||||
"cwd": cwd_str,
|
||||
"originator": "codex",
|
||||
"cli_version": "0.0.0",
|
||||
"instructions": null
|
||||
|
||||
@@ -354,7 +354,7 @@ async fn run_ratatui_app(
|
||||
}
|
||||
}
|
||||
} else if cli.resume_last {
|
||||
match RolloutRecorder::list_conversations(&config.codex_home, 1, None).await {
|
||||
match RolloutRecorder::list_conversations(&config.codex_home, &config.cwd, 1, None).await {
|
||||
Ok(page) => page
|
||||
.items
|
||||
.first()
|
||||
@@ -363,7 +363,7 @@ async fn run_ratatui_app(
|
||||
Err(_) => resume_picker::ResumeSelection::StartFresh,
|
||||
}
|
||||
} else if cli.resume_picker {
|
||||
match resume_picker::run_resume_picker(&mut tui, &config.codex_home).await? {
|
||||
match resume_picker::run_resume_picker(&mut tui, &config.codex_home, &config.cwd).await? {
|
||||
resume_picker::ResumeSelection::Exit => {
|
||||
restore();
|
||||
session_log::log_session_end();
|
||||
|
||||
@@ -39,9 +39,17 @@ pub enum ResumeSelection {
|
||||
/// Interactive session picker that lists recorded rollout files with simple
|
||||
/// search and pagination. Shows the first user input as the preview, relative
|
||||
/// time (e.g., "5 seconds ago"), and the absolute path.
|
||||
pub async fn run_resume_picker(tui: &mut Tui, codex_home: &Path) -> Result<ResumeSelection> {
|
||||
pub async fn run_resume_picker(
|
||||
tui: &mut Tui,
|
||||
codex_home: &Path,
|
||||
cwd: &Path,
|
||||
) -> Result<ResumeSelection> {
|
||||
let alt = AltScreenGuard::enter(tui);
|
||||
let mut state = PickerState::new(codex_home.to_path_buf(), alt.tui.frame_requester());
|
||||
let mut state = PickerState::new(
|
||||
codex_home.to_path_buf(),
|
||||
cwd.to_path_buf(),
|
||||
alt.tui.frame_requester(),
|
||||
);
|
||||
state.load_page(None).await?;
|
||||
state.request_frame();
|
||||
|
||||
@@ -88,6 +96,7 @@ impl Drop for AltScreenGuard<'_> {
|
||||
|
||||
struct PickerState {
|
||||
codex_home: PathBuf,
|
||||
cwd: PathBuf,
|
||||
requester: FrameRequester,
|
||||
// pagination
|
||||
pagination: Pagination,
|
||||
@@ -115,9 +124,10 @@ struct Row {
|
||||
}
|
||||
|
||||
impl PickerState {
|
||||
fn new(codex_home: PathBuf, requester: FrameRequester) -> Self {
|
||||
fn new(codex_home: PathBuf, cwd: PathBuf, requester: FrameRequester) -> Self {
|
||||
Self {
|
||||
codex_home,
|
||||
cwd,
|
||||
requester,
|
||||
pagination: Pagination {
|
||||
current_anchor: None,
|
||||
@@ -225,7 +235,9 @@ impl PickerState {
|
||||
}
|
||||
|
||||
async fn load_page(&mut self, anchor: Option<&Cursor>) -> Result<()> {
|
||||
let page = RolloutRecorder::list_conversations(&self.codex_home, PAGE_SIZE, anchor).await?;
|
||||
let page =
|
||||
RolloutRecorder::list_conversations(&self.codex_home, &self.cwd, PAGE_SIZE, anchor)
|
||||
.await?;
|
||||
self.pagination.next_cursor = page.next_cursor.clone();
|
||||
self.all_rows = to_rows(page);
|
||||
self.apply_filter();
|
||||
|
||||
Reference in New Issue
Block a user