mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
optimize codex resume and fork history
This commit is contained in:
@@ -119,6 +119,7 @@ mod tests {
|
||||
base_instructions: BaseInstructions::default(),
|
||||
dynamic_tools: Vec::new(),
|
||||
multi_agent_version: None,
|
||||
initial_rollout_copy: None,
|
||||
metadata: ThreadPersistenceMetadata {
|
||||
cwd: None,
|
||||
model_provider: "test-provider".to_string(),
|
||||
|
||||
@@ -100,13 +100,28 @@ impl LiveThread {
|
||||
|
||||
pub async fn resume(
|
||||
thread_store: Arc<dyn ThreadStore>,
|
||||
mut params: ResumeThreadParams,
|
||||
params: ResumeThreadParams,
|
||||
) -> ThreadStoreResult<Self> {
|
||||
Self::resume_with_observed_history(thread_store, params, None).await
|
||||
}
|
||||
|
||||
pub async fn resume_with_observed_history(
|
||||
thread_store: Arc<dyn ThreadStore>,
|
||||
params: ResumeThreadParams,
|
||||
observed_history: Option<&[RolloutItem]>,
|
||||
) -> ThreadStoreResult<Self> {
|
||||
let thread_id = params.thread_id;
|
||||
let should_load_history = params.history.is_none();
|
||||
let include_archived = params.include_archived;
|
||||
thread_store.resume_thread(params.clone()).await?;
|
||||
if should_load_history {
|
||||
let metadata = params.metadata.clone();
|
||||
let metadata_sync = observed_history
|
||||
.or(params.history.as_deref())
|
||||
.map(|history| {
|
||||
ThreadMetadataSync::for_resume_parts(thread_id, &metadata, Some(history))
|
||||
});
|
||||
thread_store.resume_thread(params).await?;
|
||||
let metadata_sync = if let Some(metadata_sync) = metadata_sync {
|
||||
metadata_sync
|
||||
} else {
|
||||
match thread_store
|
||||
.load_history(LoadThreadHistoryParams {
|
||||
thread_id,
|
||||
@@ -114,7 +129,11 @@ impl LiveThread {
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(history) => params.history = Some(history.items),
|
||||
Ok(history) => ThreadMetadataSync::for_resume_parts(
|
||||
thread_id,
|
||||
&metadata,
|
||||
Some(history.items.as_slice()),
|
||||
),
|
||||
Err(err) => {
|
||||
if let Err(discard_err) = thread_store.discard_thread(thread_id).await {
|
||||
warn!(
|
||||
@@ -124,8 +143,7 @@ impl LiveThread {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
let metadata_sync = ThreadMetadataSync::for_resume(¶ms);
|
||||
};
|
||||
Ok(Self {
|
||||
thread_id,
|
||||
thread_store,
|
||||
|
||||
@@ -36,7 +36,8 @@ pub(super) async fn create_thread(
|
||||
params.base_instructions,
|
||||
params.dynamic_tools,
|
||||
)
|
||||
.with_multi_agent_version(params.multi_agent_version),
|
||||
.with_multi_agent_version(params.multi_agent_version)
|
||||
.with_initial_rollout_copy(params.initial_rollout_copy),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ThreadStoreError::Internal {
|
||||
|
||||
@@ -1132,6 +1132,7 @@ mod tests {
|
||||
base_instructions: BaseInstructions::default(),
|
||||
dynamic_tools: Vec::new(),
|
||||
multi_agent_version: None,
|
||||
initial_rollout_copy: None,
|
||||
metadata: thread_metadata(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,12 +39,8 @@ pub(super) async fn read_thread(
|
||||
metadata.rollout_path.as_path(),
|
||||
)))
|
||||
&& (!params.include_history
|
||||
|| sqlite_rollout_path_can_load_history_for_thread(
|
||||
store,
|
||||
&metadata.rollout_path,
|
||||
thread_id,
|
||||
)
|
||||
.await)
|
||||
|| sqlite_rollout_path_can_load_history_for_thread(&metadata.rollout_path, thread_id)
|
||||
.await)
|
||||
{
|
||||
let metadata_sandbox_policy = metadata.sandbox_policy.clone();
|
||||
let mut thread = stored_thread_from_sqlite_metadata(store, metadata).await;
|
||||
@@ -87,7 +83,6 @@ pub(super) async fn read_thread(
|
||||
}
|
||||
|
||||
async fn sqlite_rollout_path_can_load_history_for_thread(
|
||||
store: &LocalThreadStore,
|
||||
path: &std::path::Path,
|
||||
thread_id: codex_protocol::ThreadId,
|
||||
) -> bool {
|
||||
@@ -96,10 +91,11 @@ async fn sqlite_rollout_path_can_load_history_for_thread(
|
||||
}
|
||||
// SQLite metadata can outlive a moved/recreated rollout path. When history is
|
||||
// requested, verify the path still resolves to the requested thread before
|
||||
// trusting it as the source replay.
|
||||
read_thread_from_rollout_path(store, path.to_path_buf())
|
||||
// trusting it as the source replay. The session metadata line is enough for
|
||||
// this identity check and avoids doing a summary scan before full replay.
|
||||
read_session_meta_line(path)
|
||||
.await
|
||||
.is_ok_and(|thread| thread.thread_id == thread_id)
|
||||
.is_ok_and(|meta_line| meta_line.meta.id == thread_id)
|
||||
}
|
||||
|
||||
pub(super) async fn read_thread_by_rollout_path(
|
||||
@@ -288,6 +284,75 @@ async fn load_history_items(
|
||||
.map_err(|err| ThreadStoreError::Internal {
|
||||
message: format!("failed to load thread history {}: {err}", path.display()),
|
||||
})?;
|
||||
expand_fork_parent_rollout_ref(path, items).await
|
||||
}
|
||||
|
||||
async fn expand_fork_parent_rollout_ref(
|
||||
path: &std::path::Path,
|
||||
child_items: Vec<codex_protocol::protocol::RolloutItem>,
|
||||
) -> ThreadStoreResult<Vec<codex_protocol::protocol::RolloutItem>> {
|
||||
let Some(parent_ref) = codex_rollout::read_fork_parent_rollout_ref(path)
|
||||
.await
|
||||
.map_err(|err| ThreadStoreError::Internal {
|
||||
message: format!(
|
||||
"failed to read fork parent reference {}: {err}",
|
||||
path.display()
|
||||
),
|
||||
})?
|
||||
else {
|
||||
return Ok(child_items);
|
||||
};
|
||||
|
||||
let Some(child_forked_from_id) = child_items.iter().find_map(|item| match item {
|
||||
codex_protocol::protocol::RolloutItem::SessionMeta(meta_line) => {
|
||||
meta_line.meta.forked_from_id
|
||||
}
|
||||
_ => None,
|
||||
}) else {
|
||||
return Err(ThreadStoreError::Internal {
|
||||
message: format!(
|
||||
"failed to load fork parent history for {}: child fork parent missing",
|
||||
path.display()
|
||||
),
|
||||
});
|
||||
};
|
||||
let Some(parent_path) = codex_rollout::existing_rollout_path(parent_ref.path.as_path()).await
|
||||
else {
|
||||
return Err(ThreadStoreError::Internal {
|
||||
message: format!(
|
||||
"failed to load fork parent history for {}: parent rollout {} does not exist",
|
||||
path.display(),
|
||||
parent_ref.path.display()
|
||||
),
|
||||
});
|
||||
};
|
||||
let (parent_items, parent_thread_id, _) =
|
||||
RolloutRecorder::load_rollout_items_prefix(parent_path.as_path(), parent_ref.byte_len)
|
||||
.await
|
||||
.map_err(|err| ThreadStoreError::Internal {
|
||||
message: format!(
|
||||
"failed to load fork parent history {} for {}: {err}",
|
||||
parent_path.display(),
|
||||
path.display()
|
||||
),
|
||||
})?;
|
||||
if parent_thread_id != Some(child_forked_from_id) {
|
||||
return Err(ThreadStoreError::Internal {
|
||||
message: format!(
|
||||
"failed to load fork parent history for {}: parent identity mismatch",
|
||||
path.display()
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let mut child_items = child_items.into_iter();
|
||||
let Some(child_meta) = child_items.next() else {
|
||||
return Ok(parent_items);
|
||||
};
|
||||
let mut items = Vec::with_capacity(1 + parent_items.len() + child_items.size_hint().0);
|
||||
items.push(child_meta);
|
||||
items.extend(parent_items);
|
||||
items.extend(child_items);
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,10 @@ use codex_protocol::protocol::UserMessageEvent;
|
||||
|
||||
use crate::CreateThreadParams;
|
||||
use crate::GitInfoPatch;
|
||||
#[cfg(test)]
|
||||
use crate::ResumeThreadParams;
|
||||
use crate::ThreadMetadataPatch;
|
||||
use crate::ThreadPersistenceMetadata;
|
||||
|
||||
const IMAGE_ONLY_USER_MESSAGE_PLACEHOLDER: &str = "[Image]";
|
||||
#[cfg(not(test))]
|
||||
@@ -90,11 +92,23 @@ impl ThreadMetadataSync {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn for_resume(params: &ResumeThreadParams) -> Self {
|
||||
Self::for_resume_parts(
|
||||
params.thread_id,
|
||||
¶ms.metadata,
|
||||
params.history.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn for_resume_parts(
|
||||
thread_id: ThreadId,
|
||||
metadata: &ThreadPersistenceMetadata,
|
||||
history: Option<&[RolloutItem]>,
|
||||
) -> Self {
|
||||
let mut sync = Self {
|
||||
thread_id: params.thread_id,
|
||||
cwd_seen: params
|
||||
.metadata
|
||||
thread_id,
|
||||
cwd_seen: metadata
|
||||
.cwd
|
||||
.as_ref()
|
||||
.is_some_and(|cwd| !cwd.as_os_str().is_empty()),
|
||||
@@ -107,7 +121,7 @@ impl ThreadMetadataSync {
|
||||
defer_create_update_until_history_exists: false,
|
||||
defer_resume_update_until_append: false,
|
||||
};
|
||||
if let Some(history) = params.history.as_deref() {
|
||||
if let Some(history) = history {
|
||||
let update = sync.observe_resume_history(history);
|
||||
sync.merge_pending_update(update);
|
||||
sync.defer_resume_update_until_append = sync.pending_update.is_some();
|
||||
|
||||
@@ -81,6 +81,10 @@ pub struct CreateThreadParams {
|
||||
pub dynamic_tools: Vec<DynamicToolSpec>,
|
||||
/// Multi-agent runtime selected when the thread was created.
|
||||
pub multi_agent_version: Option<MultiAgentVersion>,
|
||||
/// Optional existing rollout JSONL to seed into the new rollout. Local
|
||||
/// stores may persist this as a copy-on-write parent reference when safe.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub initial_rollout_copy: Option<PathBuf>,
|
||||
/// Metadata captured for the newly created thread.
|
||||
pub metadata: ThreadPersistenceMetadata,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user