mirror of
https://github.com/openai/codex.git
synced 2026-04-24 06:35:50 +00:00
Fork remotely stored session
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
@@ -16,6 +17,8 @@ use reqwest::Url;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use time::OffsetDateTime;
|
||||
use time::format_description::FormatItem;
|
||||
use time::macros::format_description;
|
||||
|
||||
const SHARE_OBJECT_PREFIX: &str = "sessions";
|
||||
const SHARE_OBJECT_SUFFIX: &str = ".jsonl";
|
||||
@@ -181,6 +184,29 @@ pub async fn upload_rollout_with_owner(
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn download_rollout_if_available(
|
||||
base_url: &str,
|
||||
session_id: ThreadId,
|
||||
codex_home: &Path,
|
||||
) -> anyhow::Result<Option<PathBuf>> {
|
||||
let store = SessionObjectStore::new(base_url).await?;
|
||||
let key = object_key(session_id);
|
||||
let Some(data) = store.get_object_bytes(&key).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let path = build_rollout_download_path(codex_home, session_id)?;
|
||||
let parent = path
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow::anyhow!("failed to resolve rollout directory"))?;
|
||||
tokio::fs::create_dir_all(parent)
|
||||
.await
|
||||
.with_context(|| format!("failed to create rollout directory {}", parent.display()))?;
|
||||
tokio::fs::write(&path, data)
|
||||
.await
|
||||
.with_context(|| format!("failed to write rollout file {}", path.display()))?;
|
||||
Ok(Some(path))
|
||||
}
|
||||
|
||||
fn object_key(id: ThreadId) -> String {
|
||||
format!("{SHARE_OBJECT_PREFIX}/{id}{SHARE_OBJECT_SUFFIX}")
|
||||
}
|
||||
@@ -211,6 +237,23 @@ async fn upload_meta(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_rollout_download_path(codex_home: &Path, session_id: ThreadId) -> anyhow::Result<PathBuf> {
|
||||
let timestamp = OffsetDateTime::now_local()
|
||||
.map_err(|e| anyhow::anyhow!("failed to get local time: {e}"))?;
|
||||
let format: &[FormatItem] =
|
||||
format_description!("[year]-[month]-[day]T[hour]-[minute]-[second]");
|
||||
let date_str = timestamp
|
||||
.format(format)
|
||||
.map_err(|e| anyhow::anyhow!("failed to format timestamp: {e}"))?;
|
||||
let mut dir = codex_home.to_path_buf();
|
||||
dir.push(crate::rollout::SESSIONS_SUBDIR);
|
||||
dir.push(timestamp.year().to_string());
|
||||
dir.push(format!("{:02}", u8::from(timestamp.month())));
|
||||
dir.push(format!("{:02}", timestamp.day()));
|
||||
let filename = format!("rollout-{date_str}-{session_id}.jsonl");
|
||||
Ok(dir.join(filename))
|
||||
}
|
||||
|
||||
impl HttpObjectStore {
|
||||
fn object_url(&self, key: &str) -> anyhow::Result<Url> {
|
||||
self.base_url
|
||||
|
||||
@@ -432,7 +432,7 @@ fn parse_share_scope(response: &RequestUserInputResponse) -> ShareScope {
|
||||
response
|
||||
.answers
|
||||
.get(SHARE_SCOPE_QUESTION_ID)
|
||||
.and_then(|answer| selected_label(answer))
|
||||
.and_then(selected_label)
|
||||
.map(|label| {
|
||||
if label == SHARE_SCOPE_EMAIL_LABEL {
|
||||
ShareScope::Emails
|
||||
@@ -452,7 +452,7 @@ fn parse_share_emails(response: &RequestUserInputResponse) -> Vec<String> {
|
||||
return Vec::new();
|
||||
};
|
||||
note.split(',')
|
||||
.map(|entry| entry.trim())
|
||||
.map(str::trim)
|
||||
.filter(|entry| !entry.is_empty())
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
|
||||
@@ -32,8 +32,10 @@ use codex_core::format_exec_policy_error_with_source;
|
||||
use codex_core::path_utils;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::read_session_meta_line;
|
||||
use codex_core::session_share::download_rollout_if_available;
|
||||
use codex_core::terminal::Multiplexer;
|
||||
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::AltScreenMode;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
@@ -544,8 +546,15 @@ async fn run_ratatui_app(
|
||||
} else {
|
||||
initial_config
|
||||
};
|
||||
let mut missing_session_exit = |id_str: &str, action: &str| {
|
||||
error!("Error finding conversation path: {id_str}");
|
||||
let ollama_chat_support_notice = match ollama_chat_deprecation_notice(&config).await {
|
||||
Ok(notice) => notice,
|
||||
Err(err) => {
|
||||
tracing::warn!(?err, "Failed to detect Ollama wire API");
|
||||
None
|
||||
}
|
||||
};
|
||||
let mut fatal_exit = |message: String| {
|
||||
error!("{message}");
|
||||
restore();
|
||||
session_log::log_session_end();
|
||||
let _ = tui.terminal.clear();
|
||||
@@ -554,11 +563,14 @@ async fn run_ratatui_app(
|
||||
thread_id: None,
|
||||
thread_name: None,
|
||||
update_action: None,
|
||||
exit_reason: ExitReason::Fatal(format!(
|
||||
"No saved session found with ID {id_str}. Run `codex {action}` without an ID to choose from existing sessions."
|
||||
)),
|
||||
exit_reason: ExitReason::Fatal(message),
|
||||
})
|
||||
};
|
||||
let mut missing_session_exit = |id_str: &str, action: &str| {
|
||||
fatal_exit(format!(
|
||||
"No saved session found with ID {id_str}. Run `codex {action}` without an ID to choose from existing sessions."
|
||||
))
|
||||
};
|
||||
|
||||
let use_fork = cli.fork_picker || cli.fork_last || cli.fork_session_id.is_some();
|
||||
let session_selection = if use_fork {
|
||||
@@ -571,7 +583,25 @@ async fn run_ratatui_app(
|
||||
};
|
||||
match path {
|
||||
Some(path) => resume_picker::SessionSelection::Fork(path),
|
||||
None => return missing_session_exit(id_str, "fork"),
|
||||
None => {
|
||||
let Some(storage_url) = config.session_object_storage_url.as_deref() else {
|
||||
return missing_session_exit(id_str, "fork");
|
||||
};
|
||||
let Ok(session_id) = ThreadId::from_string(id_str) else {
|
||||
return missing_session_exit(id_str, "fork");
|
||||
};
|
||||
match download_rollout_if_available(storage_url, session_id, &config.codex_home)
|
||||
.await
|
||||
{
|
||||
Ok(Some(path)) => resume_picker::SessionSelection::Fork(path),
|
||||
Ok(None) => return missing_session_exit(id_str, "fork"),
|
||||
Err(err) => {
|
||||
return fatal_exit(format!(
|
||||
"Failed to fetch remote session {id_str}: {err}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if cli.fork_last {
|
||||
let provider_filter = vec![config.model_provider_id.clone()];
|
||||
|
||||
Reference in New Issue
Block a user