Share resumed rollout history (#28426)

## Summary

Resuming a persisted thread currently deep-clones its complete rollout
history several times. `InitialHistory` is retained for the app-server
response, copied into thread persistence, and copied again by read-only
accessors. These copies scale with the complete rollout rather than the
bounded model context and add measurable latency for large sessions.

This change stores resumed rollout history in `Arc<Vec<RolloutItem>>`.
Rollout loading wraps the parsed vector once, while app-server response
construction, session initialization, and thread persistence share it
through inexpensive `Arc` clones. Read-only history access now returns a
borrowed slice, and fork paths use `Arc::unwrap_or_clone` where they
genuinely need mutable ownership. Rollout reconstruction also consumes
its temporary context instead of cloning the reconstructed model
history.

The serialized representation remains unchanged. In an artificial 123 MB
rollout benchmark, sharing resumed history reduced cold resume latency
by roughly 9–10%. The affected crates compile with their test targets,
all 80 thread-store tests pass, and the Bazel dependency lock remains
valid.
This commit is contained in:
Charlie Marsh
2026-06-23 10:23:25 -04:00
committed by GitHub
Unverified
parent 4147824509
commit 330ae6a516
17 changed files with 102 additions and 85 deletions
+1 -1
View File
@@ -380,7 +380,7 @@ schemars = "0.8.22"
seccompiler = "0.5.0"
semver = "1.0"
sentry = "0.46.0"
serde = "1"
serde = { version = "1", features = ["rc"] }
serde_ignored = "0.1.14"
serde_json = "1"
serde_path_to_error = "0.1.20"
@@ -2604,8 +2604,8 @@ impl ThreadRequestProcessor {
self.resume_thread_from_history(history.as_slice())
.await
.map(|thread_history| (thread_history, None))
} else if let Some(stored_thread) = stored_thread_from_running_probe {
self.stored_thread_to_initial_history(&stored_thread)
} else if let Some(mut stored_thread) = stored_thread_from_running_probe {
self.stored_thread_to_initial_history(&mut stored_thread)
.await
.map(|thread_history| (thread_history, Some(*stored_thread)))
} else {
@@ -2758,7 +2758,7 @@ impl ThreadRequestProcessor {
let token_usage_thread = include_turns.then(|| thread.clone());
let mut initial_turns_page = if let Some(params) = initial_turns_page.as_ref() {
match build_thread_resume_initial_turns_page(
&response_history.get_rollout_items(),
response_history.get_rollout_items(),
thread.status.clone(),
/*has_live_running_thread*/ false,
/*active_turn*/ None,
@@ -2803,7 +2803,7 @@ impl ThreadRequestProcessor {
// rebuilding history only to attribute a replayed usage update.
if let Some(token_usage_thread) = token_usage_thread {
let token_usage_turn_id = latest_token_usage_turn_id_from_rollout_items(
&response_history.get_rollout_items(),
response_history.get_rollout_items(),
token_usage_thread.turns.as_slice(),
);
// The client needs restored usage before it starts another turn.
@@ -2901,7 +2901,7 @@ impl ThreadRequestProcessor {
}
};
if let Some((existing_thread_id, existing_thread, source_thread)) = running_thread {
if let Some((existing_thread_id, existing_thread, mut source_thread)) = running_thread {
let existing_thread_rollout_path = existing_thread.rollout_path();
let active_path = existing_thread_rollout_path
.as_ref()
@@ -2963,8 +2963,8 @@ impl ThreadRequestProcessor {
should_redact_thread_resume_payloads(app_server_client_name.as_deref());
let history_items = source_thread
.history
.as_ref()
.map(|history| history.items.clone())
.take()
.map(|history| history.items)
.ok_or_else(|| {
internal_error(format!(
"thread {existing_thread_id} did not include persisted history"
@@ -2988,10 +2988,8 @@ impl ThreadRequestProcessor {
)
.await?;
let mut summary_source_thread = source_thread;
summary_source_thread.history = None;
let mut thread_summary = self.stored_thread_to_api_thread(
summary_source_thread,
source_thread,
config_snapshot.model_provider_id.as_str(),
/*include_turns*/ false,
);
@@ -3060,11 +3058,11 @@ impl ThreadRequestProcessor {
thread_id: &str,
path: Option<&PathBuf>,
) -> Result<(InitialHistory, StoredThread), JSONRPCErrorError> {
let stored_thread = self
let mut stored_thread = self
.read_stored_thread_for_resume(thread_id, path, /*include_history*/ true)
.await?;
let history = self
.stored_thread_to_initial_history(&stored_thread)
.stored_thread_to_initial_history(&mut stored_thread)
.await?;
Ok((history, stored_thread))
}
@@ -3112,13 +3110,13 @@ impl ThreadRequestProcessor {
#[tracing::instrument(level = "trace", skip_all)]
async fn stored_thread_to_initial_history(
&self,
stored_thread: &StoredThread,
stored_thread: &mut StoredThread,
) -> Result<InitialHistory, JSONRPCErrorError> {
let thread_id = stored_thread.thread_id;
let history = stored_thread
.history
.as_ref()
.map(|history| history.items.clone())
.take()
.map(|history| history.items)
.ok_or_else(|| {
internal_error(format!(
"thread {thread_id} did not include persisted history"
@@ -3126,7 +3124,7 @@ impl ThreadRequestProcessor {
})?;
Ok(InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history,
history: Arc::new(history),
rollout_path: stored_thread.rollout_path.clone(),
}))
}
@@ -3255,7 +3253,7 @@ impl ThreadRequestProcessor {
let history_items = thread_history.get_rollout_items();
populate_thread_turns_from_history(
&mut thread,
&history_items,
history_items,
/*active_turn*/ None,
);
}
@@ -3313,7 +3311,7 @@ impl ThreadRequestProcessor {
"`permissions` cannot be combined with `sandbox`",
));
}
let source_thread = self
let mut source_thread = self
.read_stored_thread_for_resume(&thread_id, path.as_ref(), /*include_history*/ true)
.await?;
let source_thread_id = source_thread.thread_id;
@@ -3323,8 +3321,8 @@ impl ThreadRequestProcessor {
.and_then(codex_core::util::normalize_thread_name);
let history_items = source_thread
.history
.as_ref()
.map(|history| history.items.clone())
.take()
.map(|history| Arc::new(history.items))
.ok_or_else(|| {
internal_error(format!(
"thread {source_thread_id} did not include persisted history"
@@ -3391,7 +3389,7 @@ impl ThreadRequestProcessor {
config,
InitialHistory::Resumed(ResumedHistory {
conversation_id: source_thread_id,
history: history_items.clone(),
history: Arc::clone(&history_items),
rollout_path: source_thread.rollout_path.clone(),
}),
thread_source.map(Into::into),
@@ -1237,7 +1237,7 @@ impl TurnRequestProcessor {
config.clone(),
InitialHistory::Resumed(ResumedHistory {
conversation_id: parent_thread_id,
history: parent_history.items,
history: Arc::new(parent_history.items),
rollout_path: parent_thread.rollout_path(),
}),
/*thread_source*/ None,
+2 -2
View File
@@ -143,7 +143,7 @@ impl AgentControl {
.items;
let initial_history = InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history,
history: Arc::new(history),
rollout_path: stored_thread.rollout_path,
});
if initial_history.get_multi_agent_version() != Some(MultiAgentVersion::V2) {
@@ -626,7 +626,7 @@ impl AgentControl {
.items;
let initial_history = InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history,
history: Arc::new(history),
rollout_path: stored_thread.rollout_path,
});
let parent_thread_id = stored_thread.parent_thread_id;
@@ -355,7 +355,7 @@ impl Session {
id: None,
});
RolloutReconstruction {
history: history.raw_items().to_vec(),
history: history.into_raw_items(),
previous_turn_settings,
reference_context_item,
window_number: window.number,
@@ -69,7 +69,9 @@ async fn record_initial_history_reconstructs_typed_inter_agent_message() {
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: vec![RolloutItem::InterAgentCommunication(communication.clone())],
history: Arc::new(vec![RolloutItem::InterAgentCommunication(
communication.clone(),
)]),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -112,7 +114,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -189,7 +191,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -757,7 +759,7 @@ async fn record_initial_history_resumed_rollback_skips_only_user_turns() {
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -841,7 +843,7 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -871,7 +873,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_seed_referenc
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -898,7 +900,7 @@ async fn record_initial_history_resumed_does_not_seed_reference_context_item_aft
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -1070,7 +1072,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -1215,7 +1217,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -1338,7 +1340,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -1454,7 +1456,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -1505,7 +1507,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_preserves_turn_
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -1633,7 +1635,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
+11 -11
View File
@@ -1697,7 +1697,7 @@ async fn record_initial_history_reconstructs_resumed_transcript() {
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -1862,7 +1862,7 @@ async fn prepares_resumed_history_before_installing_it() {
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: vec![RolloutItem::ResponseItem(resumed_item)],
history: Arc::new(vec![RolloutItem::ResponseItem(resumed_item)]),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -1905,7 +1905,7 @@ fn resolve_multi_agent_version_handles_unset_and_legacy_history() {
resolve_multi_agent_version(
&InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history: Vec::new(),
history: Arc::new(Vec::new()),
rollout_path: None,
}),
/*inherited_multi_agent_version*/ None,
@@ -1916,7 +1916,7 @@ fn resolve_multi_agent_version_handles_unset_and_legacy_history() {
resolve_multi_agent_version(
&InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history: Vec::new(),
history: Arc::new(Vec::new()),
rollout_path: None,
}),
Some(MultiAgentVersion::V2),
@@ -1927,10 +1927,10 @@ fn resolve_multi_agent_version_handles_unset_and_legacy_history() {
resolve_multi_agent_version(
&InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history: vec![session_meta_item(
history: Arc::new(vec![session_meta_item(
thread_id,
Some(MultiAgentVersion::Disabled)
)],
)]),
rollout_path: None,
}),
Some(MultiAgentVersion::V2),
@@ -1991,7 +1991,7 @@ async fn resumed_history_injects_initial_context_on_first_context_update_only()
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -2085,7 +2085,7 @@ async fn record_initial_history_seeds_token_info_from_rollout() {
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -5508,7 +5508,7 @@ async fn resumed_root_session_uses_thread_id_as_session_id() {
let (session, rx_event) = make_session_with_history_source_and_agent_control_and_rx(
InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history: Vec::new(),
history: Arc::new(Vec::new()),
rollout_path: None,
}),
SessionSource::Exec,
@@ -5543,7 +5543,7 @@ async fn resumed_subagent_session_restores_persisted_session_id() {
let (session, rx_event) = make_session_with_history_source_and_agent_control_and_rx(
InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history: vec![RolloutItem::SessionMeta(SessionMetaLine {
history: Arc::new(vec![RolloutItem::SessionMeta(SessionMetaLine {
meta: SessionMeta {
session_id: parent_session_id,
id: thread_id,
@@ -5551,7 +5551,7 @@ async fn resumed_subagent_session_restores_persisted_session_id() {
..SessionMeta::default()
},
git: None,
})],
})]),
rollout_path: None,
}),
session_source,
+12 -9
View File
@@ -1602,7 +1602,7 @@ fn stored_thread_to_initial_history(
})?;
Ok(InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history: history.items,
history: Arc::new(history.items),
rollout_path: rollout_path.or(stored_thread.rollout_path),
}))
}
@@ -1639,7 +1639,7 @@ fn truncate_before_nth_user_message(
n: usize,
snapshot_state: &SnapshotTurnState,
) -> InitialHistory {
let items: Vec<RolloutItem> = history.get_rollout_items();
let items = history.get_rollout_items().to_vec();
let user_positions = truncation::user_message_positions_in_rollout(&items);
let rolled = if snapshot_state.ends_mid_turn && n >= user_positions.len() {
if let Some(cut_idx) = snapshot_state
@@ -1671,7 +1671,7 @@ struct SnapshotTurnState {
fn snapshot_turn_state(history: &InitialHistory) -> SnapshotTurnState {
let rollout_items = history.get_rollout_items();
let mut builder = ThreadHistoryBuilder::new();
for item in &rollout_items {
for item in rollout_items {
builder.handle_rollout_item(item);
}
let active_turn_id = builder.active_turn_id_if_explicit();
@@ -1695,7 +1695,7 @@ fn snapshot_turn_state(history: &InitialHistory) -> SnapshotTurnState {
};
}
let Some(last_user_position) = truncation::user_message_positions_in_rollout(&rollout_items)
let Some(last_user_position) = truncation::user_message_positions_in_rollout(rollout_items)
.last()
.copied()
else {
@@ -1736,7 +1736,9 @@ fn fork_history_from_snapshot(
InitialHistory::New => InitialHistory::New,
InitialHistory::Cleared => InitialHistory::Cleared,
InitialHistory::Forked(history) => InitialHistory::Forked(history),
InitialHistory::Resumed(resumed) => InitialHistory::Forked(resumed.history),
InitialHistory::Resumed(resumed) => {
InitialHistory::Forked(Arc::unwrap_or_clone(resumed.history))
}
};
if snapshot_state.ends_mid_turn {
append_interrupted_boundary(
@@ -1782,12 +1784,13 @@ fn append_interrupted_boundary(
history.push(aborted_event);
InitialHistory::Forked(history)
}
InitialHistory::Resumed(mut resumed) => {
InitialHistory::Resumed(resumed) => {
let mut history = Arc::unwrap_or_clone(resumed.history);
if let Some(marker) = interrupted_turn_history_marker(interrupted_marker) {
resumed.history.push(RolloutItem::ResponseItem(marker));
history.push(RolloutItem::ResponseItem(marker));
}
resumed.history.push(aborted_event);
InitialHistory::Forked(resumed.history)
history.push(aborted_event);
InitialHistory::Forked(history)
}
}
}
+7 -7
View File
@@ -116,7 +116,7 @@ fn truncates_before_requested_user_message() {
RolloutItem::ResponseItem(items[2].clone()),
];
assert_eq!(
serde_json::to_value(&got_items).unwrap(),
serde_json::to_value(got_items).unwrap(),
serde_json::to_value(&expected_items).unwrap()
);
@@ -256,7 +256,7 @@ async fn ignores_session_prefix_messages_when_truncating() {
];
assert_eq!(
serde_json::to_value(&got_items).unwrap(),
serde_json::to_value(got_items).unwrap(),
serde_json::to_value(&expected).unwrap()
);
}
@@ -963,7 +963,7 @@ async fn rollout_path_resume_and_fork_read_history_through_thread_store() {
config.clone(),
InitialHistory::Resumed(ResumedHistory {
conversation_id: source.thread_id,
history: vec![RolloutItem::ResponseItem(user_msg("hello"))],
history: Arc::new(vec![RolloutItem::ResponseItem(user_msg("hello"))]),
rollout_path: Some(rollout_path.clone()),
}),
auth_manager.clone(),
@@ -1315,7 +1315,7 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
assert!(!snapshot_turn_state(&history).ends_mid_turn);
let rollout_items: Vec<_> = history
.get_rollout_items()
.into_iter()
.iter()
.filter(|item| !matches!(item, RolloutItem::SessionMeta(_)))
.collect();
let interrupted_marker_json = serde_json::to_value(RolloutItem::ResponseItem(
@@ -1435,7 +1435,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
.expect("read forked rollout history");
let rollout_items: Vec<_> = history
.get_rollout_items()
.into_iter()
.iter()
.filter(|item| !matches!(item, RolloutItem::SessionMeta(_)))
.collect();
@@ -1522,7 +1522,7 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_
let forked_rollout_items: Vec<_> = history
.get_rollout_items()
.into_iter()
.iter()
.filter(|item| !matches!(item, RolloutItem::SessionMeta(_)))
.collect();
let interrupted_marker_json = serde_json::to_value(RolloutItem::ResponseItem(
@@ -1560,7 +1560,7 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_
.expect("read re-forked rollout history");
let reforked_rollout_items: Vec<_> = reforked_history
.get_rollout_items()
.into_iter()
.iter()
.filter(|item| !matches!(item, RolloutItem::SessionMeta(_)))
.collect();
+3 -1
View File
@@ -1,3 +1,5 @@
use std::sync::Arc;
use codex_core::ForkSnapshot;
use codex_core::NewThread;
use codex_core::parse_turn_item;
@@ -196,7 +198,7 @@ async fn fork_thread_from_history_does_not_require_source_rollout_path() {
test.config.clone(),
InitialHistory::Resumed(ResumedHistory {
conversation_id: test.session_configured.thread_id,
history: source_items.clone(),
history: Arc::new(source_items.clone()),
rollout_path: None,
}),
/*thread_source*/ None,
+4 -2
View File
@@ -1,5 +1,7 @@
#![allow(clippy::unwrap_used)]
use std::sync::Arc;
use codex_core::NewThread;
use codex_login::CodexAuth;
use codex_protocol::ThreadId;
@@ -51,7 +53,7 @@ fn resume_history(
InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: vec![
history: Arc::new(vec![
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_id.clone(),
trace_id: None,
@@ -75,7 +77,7 @@ fn resume_history(
duration_ms: None,
time_to_first_token_ms: None,
})),
],
]),
rollout_path: Some(rollout_path.to_path_buf()),
})
}
+6 -5
View File
@@ -10,6 +10,7 @@ use std::ops::Mul;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use strum_macros::EnumIter;
@@ -2460,7 +2461,7 @@ pub struct ConversationPathResponseEvent {
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ResumedHistory {
pub conversation_id: ThreadId,
pub history: Vec<RolloutItem>,
pub history: Arc<Vec<RolloutItem>>,
pub rollout_path: Option<PathBuf>,
}
@@ -2505,11 +2506,11 @@ impl InitialHistory {
}
}
pub fn get_rollout_items(&self) -> Vec<RolloutItem> {
pub fn get_rollout_items(&self) -> &[RolloutItem] {
match self {
InitialHistory::New | InitialHistory::Cleared => Vec::new(),
InitialHistory::Resumed(resumed) => resumed.history.clone(),
InitialHistory::Forked(items) => items.clone(),
InitialHistory::New | InitialHistory::Cleared => &[],
InitialHistory::Resumed(resumed) => &resumed.history,
InitialHistory::Forked(items) => items,
}
}
+1 -1
View File
@@ -947,7 +947,7 @@ impl RolloutRecorder {
info!("Resumed rollout successfully from {path:?}");
Ok(InitialHistory::Resumed(ResumedHistory {
conversation_id,
history: items,
history: Arc::new(items),
rollout_path: Some(compression::plain_rollout_path(path)),
}))
}
+3 -1
View File
@@ -266,7 +266,9 @@ impl InMemoryThreadStore {
let mut state = self.state.lock().await;
state.calls.resume_thread += 1;
if let Some(history) = params.history {
state.histories.insert(params.thread_id, history);
state
.histories
.insert(params.thread_id, Arc::unwrap_or_clone(history));
} else {
state.histories.entry(params.thread_id).or_default();
}
+4 -4
View File
@@ -100,12 +100,13 @@ impl LiveThread {
pub async fn resume(
thread_store: Arc<dyn ThreadStore>,
mut params: ResumeThreadParams,
params: ResumeThreadParams,
) -> 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?;
let mut metadata_sync = ThreadMetadataSync::for_resume(&params);
thread_store.resume_thread(params).await?;
if should_load_history {
match thread_store
.load_history(LoadThreadHistoryParams {
@@ -114,7 +115,7 @@ impl LiveThread {
})
.await
{
Ok(history) => params.history = Some(history.items),
Ok(history) => metadata_sync.record_resume_history(&history.items),
Err(err) => {
if let Err(discard_err) = thread_store.discard_thread(thread_id).await {
warn!(
@@ -125,7 +126,6 @@ impl LiveThread {
}
}
}
let metadata_sync = ThreadMetadataSync::for_resume(&params);
Ok(Self {
thread_id,
thread_store,
@@ -1,3 +1,5 @@
#[cfg(test)]
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
@@ -108,13 +110,17 @@ impl ThreadMetadataSync {
defer_resume_update_until_append: false,
};
if let Some(history) = params.history.as_deref() {
let update = sync.observe_resume_history(history);
sync.merge_pending_update(update);
sync.defer_resume_update_until_append = sync.pending_update.is_some();
sync.record_resume_history(history);
}
sync
}
pub(crate) fn record_resume_history(&mut self, history: &[RolloutItem]) {
let update = self.observe_resume_history(history);
self.merge_pending_update(update);
self.defer_resume_update_until_append = self.pending_update.is_some();
}
pub(crate) fn take_pending_update(&self) -> Option<PendingThreadMetadataPatch> {
self.pending_update
.clone()
@@ -555,7 +561,7 @@ mod tests {
ResumeThreadParams {
thread_id,
rollout_path: None,
history: Some(history),
history: Some(Arc::new(history)),
include_archived: false,
metadata: ThreadPersistenceMetadata {
cwd: None,
+2 -1
View File
@@ -1,4 +1,5 @@
use std::path::PathBuf;
use std::sync::Arc;
use chrono::DateTime;
use chrono::Utc;
@@ -96,7 +97,7 @@ pub struct ResumeThreadParams {
/// Known local rollout path when the caller resumed from a specific file.
pub rollout_path: Option<PathBuf>,
/// Known replay history for the resumed thread, if already loaded by the caller.
pub history: Option<Vec<RolloutItem>>,
pub history: Option<Arc<Vec<RolloutItem>>>,
/// Whether archived threads may be reopened.
pub include_archived: bool,
/// Metadata for future writes appended to the resumed live thread.