mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Migrate fork and resume reads to thread store (#18900)
- Route cold thread/resume and thread/fork source loading through ThreadStore reads instead of direct rollout path operations - Keep lookups that explicitly specify a rollout-path using the local thread store methods but return an invalid-request error for remote ThreadStore configurations - Add some additional unit tests for code path coverage
This commit is contained in:
@@ -332,6 +332,7 @@ use codex_protocol::protocol::McpServerRefreshConfig;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot;
|
||||
use codex_protocol::protocol::RealtimeVoicesList;
|
||||
use codex_protocol::protocol::ResumedHistory;
|
||||
use codex_protocol::protocol::ReviewDelivery as CoreReviewDelivery;
|
||||
use codex_protocol::protocol::ReviewRequest;
|
||||
use codex_protocol::protocol::ReviewTarget as CoreReviewTarget;
|
||||
@@ -354,6 +355,7 @@ use codex_state::log_db::LogDbLayer;
|
||||
use codex_thread_store::ArchiveThreadParams as StoreArchiveThreadParams;
|
||||
use codex_thread_store::ListThreadsParams as StoreListThreadsParams;
|
||||
use codex_thread_store::LocalThreadStore;
|
||||
use codex_thread_store::ReadThreadByRolloutPathParams as StoreReadThreadByRolloutPathParams;
|
||||
use codex_thread_store::ReadThreadParams as StoreReadThreadParams;
|
||||
use codex_thread_store::RemoteThreadStore;
|
||||
use codex_thread_store::SortDirection as StoreSortDirection;
|
||||
@@ -404,7 +406,6 @@ use crate::thread_state::ThreadState;
|
||||
use crate::thread_state::ThreadStateManager;
|
||||
use token_usage_replay::latest_token_usage_turn_id_for_thread_path;
|
||||
use token_usage_replay::latest_token_usage_turn_id_from_rollout_items;
|
||||
use token_usage_replay::latest_token_usage_turn_id_from_rollout_path;
|
||||
use token_usage_replay::send_thread_token_usage_update_to_connection;
|
||||
|
||||
const THREAD_LIST_DEFAULT_LIMIT: usize = 25;
|
||||
@@ -662,9 +663,7 @@ pub(crate) struct CodexMessageProcessorArgs {
|
||||
fn configured_thread_store(config: &Config) -> Arc<dyn ThreadStore> {
|
||||
match config.experimental_thread_store_endpoint.as_deref() {
|
||||
Some(endpoint) => Arc::new(RemoteThreadStore::new(endpoint)),
|
||||
None => Arc::new(LocalThreadStore::new(
|
||||
codex_rollout::RolloutConfig::from_view(config),
|
||||
)),
|
||||
None => Arc::new(configured_local_thread_store(config)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,6 +674,10 @@ fn environment_selection_error_message(err: CodexErr) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn configured_local_thread_store(config: &Config) -> LocalThreadStore {
|
||||
LocalThreadStore::new(codex_rollout::RolloutConfig::from_view(config))
|
||||
}
|
||||
|
||||
impl CodexMessageProcessor {
|
||||
async fn instruction_sources_from_config(config: &Config) -> Vec<AbsolutePathBuf> {
|
||||
codex_core::AgentsMdManager::new(config)
|
||||
@@ -4509,22 +4512,22 @@ impl CodexMessageProcessor {
|
||||
} = params;
|
||||
let include_turns = !exclude_turns;
|
||||
|
||||
let thread_history = if let Some(history) = history {
|
||||
let (thread_history, resume_source_thread) = if let Some(history) = history {
|
||||
let Some(thread_history) = self
|
||||
.resume_thread_from_history(request_id.clone(), history.as_slice())
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
thread_history
|
||||
(thread_history, None)
|
||||
} else {
|
||||
let Some(thread_history) = self
|
||||
let Some((thread_history, stored_thread)) = self
|
||||
.resume_thread_from_rollout(request_id.clone(), &thread_id, path.as_ref())
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
thread_history
|
||||
(thread_history, Some(stored_thread))
|
||||
};
|
||||
|
||||
let history_cwd = thread_history.session_cwd();
|
||||
@@ -4541,13 +4544,12 @@ impl CodexMessageProcessor {
|
||||
developer_instructions,
|
||||
personality,
|
||||
);
|
||||
let persisted_resume_metadata = self
|
||||
.load_and_apply_persisted_resume_metadata(
|
||||
&thread_history,
|
||||
&mut request_overrides,
|
||||
&mut typesafe_overrides,
|
||||
)
|
||||
.await;
|
||||
self.load_and_apply_persisted_resume_metadata(
|
||||
&thread_history,
|
||||
&mut request_overrides,
|
||||
&mut typesafe_overrides,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Derive a Config using the same logic as new conversation, honoring overrides if provided.
|
||||
let config = match self
|
||||
@@ -4612,7 +4614,7 @@ impl CodexMessageProcessor {
|
||||
codex_thread.as_ref(),
|
||||
&response_history,
|
||||
rollout_path.as_path(),
|
||||
persisted_resume_metadata.as_ref(),
|
||||
resume_source_thread,
|
||||
include_turns,
|
||||
)
|
||||
.await
|
||||
@@ -4738,77 +4740,58 @@ impl CodexMessageProcessor {
|
||||
return true;
|
||||
}
|
||||
|
||||
let rollout_path = if let Some(path) = existing_thread.rollout_path() {
|
||||
if path.exists() {
|
||||
path
|
||||
} else {
|
||||
match find_thread_path_by_id_str(
|
||||
&self.config.codex_home,
|
||||
&existing_thread_id.to_string(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(path)) => path,
|
||||
Ok(None) => {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
format!("no rollout found for thread id {existing_thread_id}"),
|
||||
)
|
||||
.await;
|
||||
return true;
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
format!("failed to locate thread id {existing_thread_id}: {err}"),
|
||||
)
|
||||
.await;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match find_thread_path_by_id_str(
|
||||
&self.config.codex_home,
|
||||
&existing_thread_id.to_string(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(path)) => path,
|
||||
Ok(None) => {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
format!("no rollout found for thread id {existing_thread_id}"),
|
||||
)
|
||||
.await;
|
||||
return true;
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
format!("failed to locate thread id {existing_thread_id}: {err}"),
|
||||
)
|
||||
.await;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(requested_path) = params.path.as_ref()
|
||||
&& requested_path != &rollout_path
|
||||
if let (Some(requested_path), Some(active_path)) = (
|
||||
params.path.as_ref(),
|
||||
existing_thread.rollout_path().as_ref(),
|
||||
) && requested_path != active_path
|
||||
{
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
format!(
|
||||
"cannot resume running thread {existing_thread_id} with mismatched path: requested `{}`, active `{}`",
|
||||
requested_path.display(),
|
||||
rollout_path.display()
|
||||
active_path.display()
|
||||
),
|
||||
)
|
||||
.await;
|
||||
return true;
|
||||
}
|
||||
|
||||
let Some(source_thread) = self
|
||||
.read_stored_thread_for_resume(
|
||||
request_id.clone(),
|
||||
¶ms.thread_id,
|
||||
params.path.as_ref(),
|
||||
/*include_history*/ true,
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return true;
|
||||
};
|
||||
if source_thread.thread_id != existing_thread_id {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
format!(
|
||||
"cannot resume running thread {existing_thread_id} from source thread {}",
|
||||
source_thread.thread_id
|
||||
),
|
||||
)
|
||||
.await;
|
||||
return true;
|
||||
}
|
||||
let Some(history_items) = source_thread
|
||||
.history
|
||||
.as_ref()
|
||||
.map(|history| history.items.clone())
|
||||
else {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!("thread {existing_thread_id} did not include persisted history"),
|
||||
)
|
||||
.await;
|
||||
return true;
|
||||
};
|
||||
|
||||
let thread_state = self
|
||||
.thread_state_manager
|
||||
.thread_state(existing_thread_id)
|
||||
@@ -4835,18 +4818,15 @@ impl CodexMessageProcessor {
|
||||
mismatch_details.join("; ")
|
||||
);
|
||||
}
|
||||
let mut config_for_instruction_sources = self.config.as_ref().clone();
|
||||
config_for_instruction_sources.cwd = config_snapshot.cwd.clone();
|
||||
let instruction_sources =
|
||||
Self::instruction_sources_from_config(&config_for_instruction_sources).await;
|
||||
let thread_summary = match load_thread_summary_for_rollout(
|
||||
&self.config,
|
||||
existing_thread_id,
|
||||
rollout_path.as_path(),
|
||||
config_snapshot.model_provider_id.as_str(),
|
||||
/*persisted_metadata*/ None,
|
||||
)
|
||||
.await
|
||||
let mut summary_source_thread = source_thread;
|
||||
summary_source_thread.history = None;
|
||||
let thread_summary = match self
|
||||
.stored_thread_to_api_thread(
|
||||
summary_source_thread,
|
||||
config_snapshot.model_provider_id.as_str(),
|
||||
/*include_turns*/ false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(thread) => thread,
|
||||
Err(message) => {
|
||||
@@ -4854,6 +4834,10 @@ impl CodexMessageProcessor {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
let mut config_for_instruction_sources = self.config.as_ref().clone();
|
||||
config_for_instruction_sources.cwd = config_snapshot.cwd.clone();
|
||||
let instruction_sources =
|
||||
Self::instruction_sources_from_config(&config_for_instruction_sources).await;
|
||||
|
||||
let listener_command_tx = {
|
||||
let thread_state = thread_state.lock().await;
|
||||
@@ -4874,7 +4858,7 @@ impl CodexMessageProcessor {
|
||||
let command = crate::thread_state::ThreadListenerCommand::SendThreadResumeResponse(
|
||||
Box::new(crate::thread_state::PendingThreadResumeRequest {
|
||||
request_id: request_id.clone(),
|
||||
rollout_path: rollout_path.clone(),
|
||||
history_items,
|
||||
config_snapshot,
|
||||
instruction_sources,
|
||||
thread_summary,
|
||||
@@ -4920,57 +4904,133 @@ impl CodexMessageProcessor {
|
||||
request_id: ConnectionRequestId,
|
||||
thread_id: &str,
|
||||
path: Option<&PathBuf>,
|
||||
) -> Option<InitialHistory> {
|
||||
let rollout_path = if let Some(path) = path {
|
||||
path.clone()
|
||||
) -> Option<(InitialHistory, StoredThread)> {
|
||||
match self
|
||||
.read_stored_thread_for_resume(
|
||||
request_id.clone(),
|
||||
thread_id,
|
||||
path,
|
||||
/*include_history*/ true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Some(stored_thread) => self
|
||||
.stored_thread_to_initial_history(request_id, &stored_thread)
|
||||
.await
|
||||
.map(|history| (history, stored_thread)),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_stored_thread_for_resume(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
thread_id: &str,
|
||||
path: Option<&PathBuf>,
|
||||
include_history: bool,
|
||||
) -> Option<StoredThread> {
|
||||
let result = if let Some(path) = path {
|
||||
self.thread_store
|
||||
.read_thread_by_rollout_path(StoreReadThreadByRolloutPathParams {
|
||||
rollout_path: path.clone(),
|
||||
include_archived: true,
|
||||
include_history,
|
||||
})
|
||||
.await
|
||||
} else {
|
||||
let existing_thread_id = match ThreadId::from_string(thread_id) {
|
||||
Ok(id) => id,
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("invalid thread id: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
format!("invalid thread id: {err}"),
|
||||
)
|
||||
.await;
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
match find_thread_path_by_id_str(
|
||||
&self.config.codex_home,
|
||||
&existing_thread_id.to_string(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(path)) => path,
|
||||
Ok(None) => {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
format!("no rollout found for thread id {existing_thread_id}"),
|
||||
)
|
||||
.await;
|
||||
return None;
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
format!("failed to locate thread id {existing_thread_id}: {err}"),
|
||||
)
|
||||
.await;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let params = StoreReadThreadParams {
|
||||
thread_id: existing_thread_id,
|
||||
include_archived: true,
|
||||
include_history,
|
||||
};
|
||||
self.thread_store.read_thread(params).await
|
||||
};
|
||||
|
||||
match RolloutRecorder::get_rollout_history(&rollout_path).await {
|
||||
Ok(initial_history) => Some(initial_history),
|
||||
match result {
|
||||
Ok(thread) => Some(thread),
|
||||
Err(err) => {
|
||||
self.send_invalid_request_error(
|
||||
self.outgoing
|
||||
.send_error(request_id, thread_store_resume_read_error(err))
|
||||
.await;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn stored_thread_to_initial_history(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
stored_thread: &StoredThread,
|
||||
) -> Option<InitialHistory> {
|
||||
let thread_id = stored_thread.thread_id;
|
||||
let history = match stored_thread.history.as_ref() {
|
||||
Some(history) => history.items.clone(),
|
||||
None => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!("failed to load rollout `{}`: {err}", rollout_path.display()),
|
||||
format!("thread {thread_id} did not include persisted history"),
|
||||
)
|
||||
.await;
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(InitialHistory::Resumed(ResumedHistory {
|
||||
conversation_id: thread_id,
|
||||
history,
|
||||
rollout_path: stored_thread.rollout_path.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn stored_thread_to_api_thread(
|
||||
&self,
|
||||
stored_thread: StoredThread,
|
||||
fallback_provider: &str,
|
||||
include_turns: bool,
|
||||
) -> std::result::Result<Thread, String> {
|
||||
let (mut thread, history) =
|
||||
thread_from_stored_thread(stored_thread, fallback_provider, &self.config.cwd);
|
||||
if include_turns && let Some(history) = history {
|
||||
populate_thread_turns(
|
||||
&mut thread,
|
||||
ThreadTurnSource::HistoryItems(&history.items),
|
||||
/*active_turn*/ None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(thread)
|
||||
}
|
||||
|
||||
async fn read_stored_thread_for_new_fork(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
thread_store: &dyn ThreadStore,
|
||||
thread_id: ThreadId,
|
||||
include_history: bool,
|
||||
) -> Option<StoredThread> {
|
||||
match thread_store
|
||||
.read_thread(StoreReadThreadParams {
|
||||
thread_id,
|
||||
include_archived: true,
|
||||
include_history,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(thread) => Some(thread),
|
||||
Err(err) => {
|
||||
self.outgoing
|
||||
.send_error(request_id, thread_store_resume_read_error(err))
|
||||
.await;
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -4982,20 +5042,42 @@ impl CodexMessageProcessor {
|
||||
thread: &CodexThread,
|
||||
thread_history: &InitialHistory,
|
||||
rollout_path: &Path,
|
||||
persisted_resume_metadata: Option<&ThreadMetadata>,
|
||||
resume_source_thread: Option<StoredThread>,
|
||||
include_turns: bool,
|
||||
) -> std::result::Result<Thread, String> {
|
||||
let config_snapshot = thread.config_snapshot().await;
|
||||
let thread = match thread_history {
|
||||
InitialHistory::Resumed(resumed) => {
|
||||
load_thread_summary_for_rollout(
|
||||
&self.config,
|
||||
resumed.conversation_id,
|
||||
resumed.rollout_path.as_path(),
|
||||
config_snapshot.model_provider_id.as_str(),
|
||||
persisted_resume_metadata,
|
||||
)
|
||||
.await
|
||||
let fallback_provider = config_snapshot.model_provider_id.as_str();
|
||||
if let Some(mut stored_thread) = resume_source_thread {
|
||||
stored_thread.history = None;
|
||||
Ok(thread_from_stored_thread(
|
||||
stored_thread,
|
||||
fallback_provider,
|
||||
&self.config.cwd,
|
||||
)
|
||||
.0)
|
||||
} else {
|
||||
match self
|
||||
.thread_store
|
||||
.read_thread(StoreReadThreadParams {
|
||||
thread_id: resumed.conversation_id,
|
||||
include_archived: true,
|
||||
include_history: false,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(stored_thread) => Ok(thread_from_stored_thread(
|
||||
stored_thread,
|
||||
fallback_provider,
|
||||
&self.config.cwd,
|
||||
)
|
||||
.0),
|
||||
Err(read_err) => {
|
||||
Err(format!("failed to read thread from store: {read_err}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
InitialHistory::Forked(items) => {
|
||||
let mut thread = build_thread_from_snapshot(
|
||||
@@ -5061,50 +5143,31 @@ impl CodexMessageProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
let (rollout_path, source_thread_id) = if let Some(path) = path {
|
||||
(path, None)
|
||||
} else {
|
||||
let existing_thread_id = match ThreadId::from_string(&thread_id) {
|
||||
Ok(id) => id,
|
||||
Err(err) => {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
format!("invalid thread id: {err}"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match find_thread_path_by_id_str(
|
||||
&self.config.codex_home,
|
||||
&existing_thread_id.to_string(),
|
||||
let Some(source_thread) = self
|
||||
.read_stored_thread_for_resume(
|
||||
request_id.clone(),
|
||||
&thread_id,
|
||||
path.as_ref(),
|
||||
/*include_history*/ true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(p)) => (p, Some(existing_thread_id)),
|
||||
Ok(None) => {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
format!("no rollout found for thread id {existing_thread_id}"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
format!("failed to locate thread id {existing_thread_id}: {err}"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let history_cwd =
|
||||
read_history_cwd_from_state_db(&self.config, source_thread_id, rollout_path.as_path())
|
||||
.await;
|
||||
let source_thread_id = source_thread.thread_id;
|
||||
let Some(history_items) = source_thread
|
||||
.history
|
||||
.as_ref()
|
||||
.map(|history| history.items.clone())
|
||||
else {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!("thread {source_thread_id} did not include persisted history"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
};
|
||||
let history_cwd = Some(source_thread.cwd.clone());
|
||||
|
||||
// Persist Windows sandbox mode.
|
||||
let mut cli_overrides = cli_overrides.unwrap_or_default();
|
||||
@@ -5159,6 +5222,7 @@ impl CodexMessageProcessor {
|
||||
|
||||
let fallback_model_provider = config.model_provider_id.clone();
|
||||
let instruction_sources = Self::instruction_sources_from_config(&config).await;
|
||||
let fork_thread_store = configured_thread_store(&config);
|
||||
|
||||
let NewThread {
|
||||
thread_id,
|
||||
@@ -5167,10 +5231,14 @@ impl CodexMessageProcessor {
|
||||
..
|
||||
} = match self
|
||||
.thread_manager
|
||||
.fork_thread(
|
||||
.fork_thread_from_history(
|
||||
ForkSnapshot::Interrupted,
|
||||
config,
|
||||
rollout_path.clone(),
|
||||
InitialHistory::Resumed(ResumedHistory {
|
||||
conversation_id: source_thread_id,
|
||||
history: history_items.clone(),
|
||||
rollout_path: source_thread.rollout_path.clone(),
|
||||
}),
|
||||
persist_extended_history,
|
||||
self.request_trace_context(&request_id).await,
|
||||
)
|
||||
@@ -5182,7 +5250,7 @@ impl CodexMessageProcessor {
|
||||
CodexErr::Io(_) | CodexErr::Json(_) => {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
format!("failed to load rollout `{}`: {err}", rollout_path.display()),
|
||||
format!("failed to load thread {source_thread_id}: {err}"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -5216,25 +5284,33 @@ impl CodexMessageProcessor {
|
||||
);
|
||||
|
||||
// Persistent forks materialize their own rollout immediately. Ephemeral forks stay
|
||||
// pathless, so they rebuild their visible history from the copied source rollout instead.
|
||||
// pathless, so they rebuild their visible history from the copied source history instead.
|
||||
let mut thread = if let Some(fork_rollout_path) = session_configured.rollout_path.as_ref() {
|
||||
match read_summary_from_rollout(
|
||||
fork_rollout_path.as_path(),
|
||||
fallback_model_provider.as_str(),
|
||||
)
|
||||
.await
|
||||
let Some(stored_thread) = self
|
||||
.read_stored_thread_for_new_fork(
|
||||
request_id.clone(),
|
||||
fork_thread_store.as_ref(),
|
||||
thread_id,
|
||||
include_turns,
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
match self
|
||||
.stored_thread_to_api_thread(
|
||||
stored_thread,
|
||||
fallback_model_provider.as_str(),
|
||||
include_turns,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(summary) => {
|
||||
let mut thread = summary_to_thread(summary, &self.config.cwd);
|
||||
thread.forked_from_id =
|
||||
forked_from_id_from_rollout(fork_rollout_path.as_path()).await;
|
||||
thread
|
||||
}
|
||||
Err(err) => {
|
||||
Ok(thread) => thread,
|
||||
Err(message) => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!(
|
||||
"failed to load rollout `{}` for thread {thread_id}: {err}",
|
||||
"failed to load rollout `{}` for thread {thread_id}: {message}",
|
||||
fork_rollout_path.display()
|
||||
),
|
||||
)
|
||||
@@ -5247,30 +5323,8 @@ impl CodexMessageProcessor {
|
||||
// forked thread names do not inherit the source thread name
|
||||
let mut thread =
|
||||
build_thread_from_snapshot(thread_id, &config_snapshot, /*path*/ None);
|
||||
let history_items = match read_rollout_items_from_rollout(rollout_path.as_path()).await
|
||||
{
|
||||
Ok(items) => items,
|
||||
Err(err) => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!(
|
||||
"failed to load source rollout `{}` for thread {thread_id}: {err}",
|
||||
rollout_path.display()
|
||||
),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
thread.preview = preview_from_rollout_items(&history_items);
|
||||
thread.forked_from_id = source_thread_id
|
||||
.or_else(|| {
|
||||
history_items.iter().find_map(|item| match item {
|
||||
RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.id),
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
.map(|id| id.to_string());
|
||||
thread.forked_from_id = Some(source_thread_id.to_string());
|
||||
if include_turns
|
||||
&& let Err(message) = populate_thread_turns(
|
||||
&mut thread,
|
||||
@@ -5285,19 +5339,6 @@ impl CodexMessageProcessor {
|
||||
thread
|
||||
};
|
||||
|
||||
if let Some(fork_rollout_path) = session_configured.rollout_path.as_ref()
|
||||
&& include_turns
|
||||
&& let Err(message) = populate_thread_turns(
|
||||
&mut thread,
|
||||
ThreadTurnSource::RolloutPath(fork_rollout_path.as_path()),
|
||||
/*active_turn*/ None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
self.send_internal_error(request_id, message).await;
|
||||
return;
|
||||
}
|
||||
|
||||
self.thread_watch_manager
|
||||
.upsert_thread_silently(thread.clone())
|
||||
.await;
|
||||
@@ -5346,11 +5387,10 @@ impl CodexMessageProcessor {
|
||||
{
|
||||
Some(turn_id)
|
||||
} else {
|
||||
latest_token_usage_turn_id_from_rollout_path(
|
||||
rollout_path.as_path(),
|
||||
latest_token_usage_turn_id_from_rollout_items(
|
||||
&history_items,
|
||||
token_usage_thread.turns.as_slice(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
// Mirror the resume contract for forks: the new thread is usable as soon
|
||||
// as the response arrives, so restored usage must follow immediately.
|
||||
@@ -5393,14 +5433,13 @@ impl CodexMessageProcessor {
|
||||
.as_any()
|
||||
.downcast_ref::<LocalThreadStore>()
|
||||
else {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message:
|
||||
"rollout path queries are only supported with the local thread store"
|
||||
.to_string(),
|
||||
data: None,
|
||||
};
|
||||
return self.outgoing.send_error(request_id, error).await;
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
"rollout path queries are only supported with the local thread store"
|
||||
.to_string(),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
};
|
||||
|
||||
local_thread_store
|
||||
@@ -8812,7 +8851,7 @@ async fn handle_pending_thread_resume_request(
|
||||
if pending.include_turns
|
||||
&& let Err(message) = populate_thread_turns(
|
||||
&mut thread,
|
||||
ThreadTurnSource::RolloutPath(pending.rollout_path.as_path()),
|
||||
ThreadTurnSource::HistoryItems(&pending.history_items),
|
||||
active_turn.as_ref(),
|
||||
)
|
||||
.await
|
||||
@@ -8904,11 +8943,10 @@ async fn handle_pending_thread_resume_request(
|
||||
// Match cold resume: metadata-only resume should attach the listener without
|
||||
// paying the cost of turn reconstruction for historical usage replay.
|
||||
if let Some(token_usage_thread) = token_usage_thread {
|
||||
let token_usage_turn_id = latest_token_usage_turn_id_from_rollout_path(
|
||||
pending.rollout_path.as_path(),
|
||||
let token_usage_turn_id = latest_token_usage_turn_id_from_rollout_items(
|
||||
&pending.history_items,
|
||||
token_usage_thread.turns.as_slice(),
|
||||
)
|
||||
.await;
|
||||
);
|
||||
// Rejoining a loaded thread has the same UI contract as a cold resume, but
|
||||
// uses the live conversation state instead of reconstructing a new session.
|
||||
send_thread_token_usage_update_to_connection(
|
||||
@@ -8927,7 +8965,6 @@ async fn handle_pending_thread_resume_request(
|
||||
}
|
||||
|
||||
enum ThreadTurnSource<'a> {
|
||||
RolloutPath(&'a Path),
|
||||
HistoryItems(&'a [RolloutItem]),
|
||||
}
|
||||
|
||||
@@ -8937,18 +8974,6 @@ async fn populate_thread_turns(
|
||||
active_turn: Option<&Turn>,
|
||||
) -> std::result::Result<(), String> {
|
||||
let mut turns = match turn_source {
|
||||
ThreadTurnSource::RolloutPath(rollout_path) => {
|
||||
read_rollout_items_from_rollout(rollout_path)
|
||||
.await
|
||||
.map(|items| build_turns_from_rollout_items(&items))
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"failed to load rollout `{}` for thread {}: {err}",
|
||||
rollout_path.display(),
|
||||
thread.id
|
||||
)
|
||||
})?
|
||||
}
|
||||
ThreadTurnSource::HistoryItems(items) => build_turns_from_rollout_items(items),
|
||||
};
|
||||
if let Some(active_turn) = active_turn {
|
||||
@@ -9372,36 +9397,6 @@ fn validate_dynamic_tools(tools: &[ApiDynamicToolSpec]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_history_cwd_from_state_db(
|
||||
config: &Config,
|
||||
thread_id: Option<ThreadId>,
|
||||
rollout_path: &Path,
|
||||
) -> Option<PathBuf> {
|
||||
if let Some(state_db_ctx) = get_state_db(config).await
|
||||
&& let Some(thread_id) = thread_id
|
||||
&& let Ok(Some(metadata)) = state_db_ctx.get_thread(thread_id).await
|
||||
{
|
||||
return Some(metadata.cwd);
|
||||
}
|
||||
|
||||
match read_session_meta_line(rollout_path).await {
|
||||
Ok(meta_line) => Some(meta_line.meta.cwd),
|
||||
Err(err) => {
|
||||
let rollout_path = rollout_path.display();
|
||||
warn!("failed to read session metadata from rollout {rollout_path}: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_summary_from_state_db_by_thread_id(
|
||||
config: &Config,
|
||||
thread_id: ThreadId,
|
||||
) -> Option<ConversationSummary> {
|
||||
let state_db_ctx = open_state_db_for_direct_thread_lookup(config).await;
|
||||
read_summary_from_state_db_context_by_thread_id(state_db_ctx.as_ref(), thread_id).await
|
||||
}
|
||||
|
||||
async fn read_summary_from_state_db_context_by_thread_id(
|
||||
state_db_ctx: Option<&StateDbHandle>,
|
||||
thread_id: ThreadId,
|
||||
@@ -9495,6 +9490,26 @@ fn thread_store_list_error(err: ThreadStoreError) -> JSONRPCErrorError {
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_store_resume_read_error(err: ThreadStoreError) -> JSONRPCErrorError {
|
||||
match err {
|
||||
ThreadStoreError::InvalidRequest { message } => JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message,
|
||||
data: None,
|
||||
},
|
||||
ThreadStoreError::ThreadNotFound { thread_id } => JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("no rollout found for thread id {thread_id}"),
|
||||
data: None,
|
||||
},
|
||||
err => JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to read thread: {err}"),
|
||||
data: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn conversation_summary_thread_id_read_error(
|
||||
conversation_id: ThreadId,
|
||||
err: ThreadStoreError,
|
||||
@@ -9914,45 +9929,6 @@ fn map_git_info(git_info: &CoreGitInfo) -> ConversationGitInfo {
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_thread_summary_for_rollout(
|
||||
config: &Config,
|
||||
thread_id: ThreadId,
|
||||
rollout_path: &Path,
|
||||
fallback_provider: &str,
|
||||
persisted_metadata: Option<&ThreadMetadata>,
|
||||
) -> std::result::Result<Thread, String> {
|
||||
let mut thread = read_summary_from_rollout(rollout_path, fallback_provider)
|
||||
.await
|
||||
.map(|summary| summary_to_thread(summary, &config.cwd))
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"failed to load rollout `{}` for thread {thread_id}: {err}",
|
||||
rollout_path.display()
|
||||
)
|
||||
})?;
|
||||
thread.forked_from_id = forked_from_id_from_rollout(rollout_path).await;
|
||||
if let Some(persisted_metadata) = persisted_metadata {
|
||||
merge_mutable_thread_metadata(
|
||||
&mut thread,
|
||||
summary_to_thread(
|
||||
summary_from_thread_metadata(persisted_metadata),
|
||||
&config.cwd,
|
||||
),
|
||||
);
|
||||
} else if let Some(summary) = read_summary_from_state_db_by_thread_id(config, thread_id).await {
|
||||
merge_mutable_thread_metadata(&mut thread, summary_to_thread(summary, &config.cwd));
|
||||
}
|
||||
let title = if let Some(metadata) = persisted_metadata {
|
||||
non_empty_title(metadata)
|
||||
} else {
|
||||
title_from_state_db(config, thread_id).await
|
||||
};
|
||||
if let Some(title) = title {
|
||||
set_thread_name_from_title(&mut thread, title);
|
||||
}
|
||||
Ok(thread)
|
||||
}
|
||||
|
||||
async fn forked_from_id_from_rollout(path: &Path) -> Option<String> {
|
||||
read_session_meta_line(path)
|
||||
.await
|
||||
@@ -9961,10 +9937,6 @@ async fn forked_from_id_from_rollout(path: &Path) -> Option<String> {
|
||||
.map(|thread_id| thread_id.to_string())
|
||||
}
|
||||
|
||||
fn merge_mutable_thread_metadata(thread: &mut Thread, persisted_thread: Thread) {
|
||||
thread.git_info = persisted_thread.git_info;
|
||||
}
|
||||
|
||||
fn preview_from_rollout_items(items: &[RolloutItem]) -> String {
|
||||
items
|
||||
.iter()
|
||||
|
||||
Reference in New Issue
Block a user