optimize codex resume and fork history

This commit is contained in:
anais
2026-06-19 10:45:37 -07:00
parent a57b268d61
commit 1d5dbfd346
25 changed files with 1688 additions and 247 deletions
+1
View File
@@ -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(),
+25 -7
View File
@@ -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(&params);
};
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 {
+1
View File
@@ -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(),
}
}
+75 -10
View File
@@ -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,
&params.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();
+4
View File
@@ -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,
}