mirror of
https://github.com/openai/codex.git
synced 2026-11-11 22:33:20 +00:00
Avoid paginated resume requests for verified legacy rollouts (#38604)
## Why Resuming a known legacy rollout with `excludeTurns` first requires the TUI to retry after the app server rejects paginated history loading. ## What changed - Carry the history mode reported by the session picker into the resume flow. - For embedded app-server sessions, resume a legacy rollout with its turns when the rollout is still legacy and background migration cannot race the check. - Keep requesting paginated history when the picker metadata may be stale, migration is enabled or active, or the app server is remote. ## Testing Added coverage for picker server replacement, background migration, maintenance lock contention, stale history metadata, and paginated resume requests. GitOrigin-RevId: 2300d1ad93a817eeeeb8f8ed60b073400ddf7106
This commit is contained in:
1 parent
588ee89324
commit
1bb6384c19
13 files changed
+455
-90
No files matched your search
@@ -946,6 +946,9 @@ impl App {
|
|||||||
(chat_widget, None)
|
(chat_widget, None)
|
||||||
}
|
}
|
||||||
SessionSelection::Resume(target_session) => {
|
SessionSelection::Resume(target_session) => {
|
||||||
|
if let Some(history_mode) = target_session.history_mode {
|
||||||
|
app_server.remember_thread_history_mode(target_session.thread_id, history_mode);
|
||||||
|
}
|
||||||
let model_settings = config_persistence::resume_model_settings_for_overrides(
|
let model_settings = config_persistence::resume_model_settings_for_overrides(
|
||||||
&config,
|
&config,
|
||||||
&harness_overrides,
|
&harness_overrides,
|
||||||
|
|||||||
@@ -1011,6 +1011,9 @@ impl App {
|
|||||||
self.chat_widget.thread_name(),
|
self.chat_widget.thread_name(),
|
||||||
self.chat_widget.rollout_path().as_deref(),
|
self.chat_widget.rollout_path().as_deref(),
|
||||||
);
|
);
|
||||||
|
if let Some(history_mode) = target_session.history_mode {
|
||||||
|
app_server.remember_thread_history_mode(target_session.thread_id, history_mode);
|
||||||
|
}
|
||||||
match app_server
|
match app_server
|
||||||
.resume_thread(
|
.resume_thread(
|
||||||
resume_config.clone(),
|
resume_config.clone(),
|
||||||
|
|||||||
@@ -5820,6 +5820,7 @@ fn session_start_error_surfaces_archived_guidance_without_rollout_path() {
|
|||||||
"/Users/me/.codex/archived_sessions/rollout.jsonl",
|
"/Users/me/.codex/archived_sessions/rollout.jsonl",
|
||||||
)),
|
)),
|
||||||
thread_id,
|
thread_id,
|
||||||
|
history_mode: None,
|
||||||
};
|
};
|
||||||
let expected = format!(
|
let expected = format!(
|
||||||
"session {thread_id} is archived. Run `codex unarchive {thread_id}` to unarchive it first."
|
"session {thread_id} is archived. Run `codex unarchive {thread_id}` to unarchive it first."
|
||||||
@@ -6123,6 +6124,7 @@ async fn remote_resume_current_cwd_rejection_snapshot() -> Result<()> {
|
|||||||
crate::resume_picker::SessionTarget {
|
crate::resume_picker::SessionTarget {
|
||||||
path: None,
|
path: None,
|
||||||
thread_id: ThreadId::new(),
|
thread_id: ThreadId::new(),
|
||||||
|
history_mode: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -6165,6 +6167,7 @@ async fn remote_exec_resume_current_cwd_is_rejected() -> Result<()> {
|
|||||||
crate::resume_picker::SessionTarget {
|
crate::resume_picker::SessionTarget {
|
||||||
path: None,
|
path: None,
|
||||||
thread_id: ThreadId::new(),
|
thread_id: ThreadId::new(),
|
||||||
|
history_mode: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -6202,6 +6205,7 @@ async fn in_app_resume_session_cwd_without_metadata_is_non_fatal() -> Result<()>
|
|||||||
crate::resume_picker::SessionTarget {
|
crate::resume_picker::SessionTarget {
|
||||||
path: None,
|
path: None,
|
||||||
thread_id: ThreadId::new(),
|
thread_id: ThreadId::new(),
|
||||||
|
history_mode: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -6264,6 +6268,7 @@ async fn remote_resume_keeps_server_only_cwd_out_of_local_config() -> Result<()>
|
|||||||
crate::resume_picker::SessionTarget {
|
crate::resume_picker::SessionTarget {
|
||||||
path: Some(rollout_path),
|
path: Some(rollout_path),
|
||||||
thread_id: ThreadId::from_string(&thread_id)?,
|
thread_id: ThreadId::from_string(&thread_id)?,
|
||||||
|
history_mode: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -6382,6 +6387,7 @@ async fn in_app_resume_uses_configured_or_explicit_cwd() -> Result<()> {
|
|||||||
crate::resume_picker::SessionTarget {
|
crate::resume_picker::SessionTarget {
|
||||||
path: Some(rollout_path),
|
path: Some(rollout_path),
|
||||||
thread_id,
|
thread_id,
|
||||||
|
history_mode: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -6494,6 +6500,7 @@ async fn remembered_current_cwd_stays_at_launch_across_in_app_resumes() -> Resul
|
|||||||
targets.push(crate::resume_picker::SessionTarget {
|
targets.push(crate::resume_picker::SessionTarget {
|
||||||
path: Some(rollout_path),
|
path: Some(rollout_path),
|
||||||
thread_id: ThreadId::from_string(&thread_id)?,
|
thread_id: ThreadId::from_string(&thread_id)?,
|
||||||
|
history_mode: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let state_db =
|
let state_db =
|
||||||
|
|||||||
@@ -1064,6 +1064,7 @@ async fn paginated_workflows_never_request_full_thread_history() -> Result<()> {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
app_server.remember_thread_history_mode(paginated_thread_id, ThreadHistoryMode::Legacy);
|
||||||
let resumed = app_server
|
let resumed = app_server
|
||||||
.resume_thread(
|
.resume_thread(
|
||||||
app.config.clone(),
|
app.config.clone(),
|
||||||
@@ -1072,6 +1073,10 @@ async fn paginated_workflows_never_request_full_thread_history() -> Result<()> {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(resumed.session.thread_id, paginated_thread_id);
|
assert_eq!(resumed.session.thread_id, paginated_thread_id);
|
||||||
|
assert!(recorded_params(&requests, "thread/read").is_empty());
|
||||||
|
let resume_requests = recorded_params(&requests, "thread/resume");
|
||||||
|
assert_eq!(resume_requests.len(), 1);
|
||||||
|
assert_eq!(resume_requests[0]["excludeTurns"], true);
|
||||||
let cells = crate::thread_transcript::load_session_transcript(
|
let cells = crate::thread_transcript::load_session_transcript(
|
||||||
&mut app_server,
|
&mut app_server,
|
||||||
paginated_thread_id,
|
paginated_thread_id,
|
||||||
@@ -1542,6 +1547,7 @@ fn session_lifecycle_avoids_redundant_subagent_metadata_reads() -> Result<()> {
|
|||||||
crate::resume_picker::SessionTarget {
|
crate::resume_picker::SessionTarget {
|
||||||
path: Some(root_rollout_path),
|
path: Some(root_rollout_path),
|
||||||
thread_id: root_thread_id,
|
thread_id: root_thread_id,
|
||||||
|
history_mode: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ fn startup_waiting_gate_is_only_for_fresh_or_exit_session_selection() {
|
|||||||
crate::resume_picker::SessionTarget {
|
crate::resume_picker::SessionTarget {
|
||||||
path: Some(PathBuf::from("/tmp/restore")),
|
path: Some(PathBuf::from("/tmp/restore")),
|
||||||
thread_id: ThreadId::new(),
|
thread_id: ThreadId::new(),
|
||||||
|
history_mode: None,
|
||||||
}
|
}
|
||||||
)),
|
)),
|
||||||
false
|
false
|
||||||
@@ -28,6 +29,7 @@ fn startup_waiting_gate_is_only_for_fresh_or_exit_session_selection() {
|
|||||||
crate::resume_picker::SessionTarget {
|
crate::resume_picker::SessionTarget {
|
||||||
path: Some(PathBuf::from("/tmp/fork")),
|
path: Some(PathBuf::from("/tmp/fork")),
|
||||||
thread_id: ThreadId::new(),
|
thread_id: ThreadId::new(),
|
||||||
|
history_mode: None,
|
||||||
}
|
}
|
||||||
)),
|
)),
|
||||||
false
|
false
|
||||||
@@ -39,10 +41,12 @@ fn startup_paused_goal_prompt_gate_is_only_for_quiet_resume() {
|
|||||||
let resume = SessionSelection::Resume(crate::resume_picker::SessionTarget {
|
let resume = SessionSelection::Resume(crate::resume_picker::SessionTarget {
|
||||||
path: Some(PathBuf::from("/tmp/restore")),
|
path: Some(PathBuf::from("/tmp/restore")),
|
||||||
thread_id: ThreadId::new(),
|
thread_id: ThreadId::new(),
|
||||||
|
history_mode: None,
|
||||||
});
|
});
|
||||||
let fork = SessionSelection::Fork(crate::resume_picker::SessionTarget {
|
let fork = SessionSelection::Fork(crate::resume_picker::SessionTarget {
|
||||||
path: Some(PathBuf::from("/tmp/fork")),
|
path: Some(PathBuf::from("/tmp/fork")),
|
||||||
thread_id: ThreadId::new(),
|
thread_id: ThreadId::new(),
|
||||||
|
history_mode: None,
|
||||||
});
|
});
|
||||||
let no_images: Vec<PathBuf> = Vec::new();
|
let no_images: Vec<PathBuf> = Vec::new();
|
||||||
let initial_images = vec![PathBuf::from("/tmp/image.png")];
|
let initial_images = vec![PathBuf::from("/tmp/image.png")];
|
||||||
@@ -111,6 +115,7 @@ fn startup_waiting_gate_not_applied_for_resume_or_fork_session_selection() {
|
|||||||
crate::resume_picker::SessionTarget {
|
crate::resume_picker::SessionTarget {
|
||||||
path: Some(PathBuf::from("/tmp/restore")),
|
path: Some(PathBuf::from("/tmp/restore")),
|
||||||
thread_id: ThreadId::new(),
|
thread_id: ThreadId::new(),
|
||||||
|
history_mode: None,
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -124,6 +129,7 @@ fn startup_waiting_gate_not_applied_for_resume_or_fork_session_selection() {
|
|||||||
crate::resume_picker::SessionTarget {
|
crate::resume_picker::SessionTarget {
|
||||||
path: Some(PathBuf::from("/tmp/fork")),
|
path: Some(PathBuf::from("/tmp/fork")),
|
||||||
thread_id: ThreadId::new(),
|
thread_id: ThreadId::new(),
|
||||||
|
history_mode: None,
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -268,6 +274,7 @@ async fn ignore_same_thread_resume_reports_noop_for_current_thread() {
|
|||||||
let ignored = app.ignore_same_thread_resume(&crate::resume_picker::SessionTarget {
|
let ignored = app.ignore_same_thread_resume(&crate::resume_picker::SessionTarget {
|
||||||
path: Some(test_path_buf("/tmp/project")),
|
path: Some(test_path_buf("/tmp/project")),
|
||||||
thread_id,
|
thread_id,
|
||||||
|
history_mode: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert!(ignored);
|
assert!(ignored);
|
||||||
@@ -292,6 +299,7 @@ async fn ignore_same_thread_resume_allows_reattaching_displayed_inactive_thread(
|
|||||||
let ignored = app.ignore_same_thread_resume(&crate::resume_picker::SessionTarget {
|
let ignored = app.ignore_same_thread_resume(&crate::resume_picker::SessionTarget {
|
||||||
path: Some(test_path_buf("/tmp/project")),
|
path: Some(test_path_buf("/tmp/project")),
|
||||||
thread_id,
|
thread_id,
|
||||||
|
history_mode: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert!(!ignored);
|
assert!(!ignored);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
mod fs;
|
mod fs;
|
||||||
mod history;
|
mod history;
|
||||||
|
mod rollout_history;
|
||||||
|
|
||||||
pub(crate) use history::HISTORY_ITEM_PAGE_LIMIT;
|
pub(crate) use history::HISTORY_ITEM_PAGE_LIMIT;
|
||||||
pub(crate) use history::HISTORY_ITEM_SCAN_LIMIT;
|
pub(crate) use history::HISTORY_ITEM_SCAN_LIMIT;
|
||||||
@@ -265,6 +266,7 @@ pub(crate) struct AppServerSession {
|
|||||||
history_pagination: HashMap<ThreadId, history::ThreadHistoryPagination>,
|
history_pagination: HashMap<ThreadId, history::ThreadHistoryPagination>,
|
||||||
remote_cwd_override: Option<PathBuf>,
|
remote_cwd_override: Option<PathBuf>,
|
||||||
thread_params_mode: ThreadParamsMode,
|
thread_params_mode: ThreadParamsMode,
|
||||||
|
background_rollout_migration_enabled: bool,
|
||||||
history_support: ThreadHistorySupport,
|
history_support: ThreadHistorySupport,
|
||||||
thread_settings_update_supported: bool,
|
thread_settings_update_supported: bool,
|
||||||
default_model: Option<String>,
|
default_model: Option<String>,
|
||||||
@@ -339,6 +341,7 @@ impl AppServerSession {
|
|||||||
history_pagination: HashMap::new(),
|
history_pagination: HashMap::new(),
|
||||||
remote_cwd_override: None,
|
remote_cwd_override: None,
|
||||||
thread_params_mode,
|
thread_params_mode,
|
||||||
|
background_rollout_migration_enabled: true,
|
||||||
history_support: ThreadHistorySupport::Paginated,
|
history_support: ThreadHistorySupport::Paginated,
|
||||||
thread_settings_update_supported: true,
|
thread_settings_update_supported: true,
|
||||||
default_model: None,
|
default_model: None,
|
||||||
@@ -616,77 +619,6 @@ impl AppServerSession {
|
|||||||
started_thread_from_start_response(response, config, self.thread_params_mode()).await
|
started_thread_from_start_response(response, config, self.thread_params_mode()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn resume_thread(
|
|
||||||
&mut self,
|
|
||||||
config: Config,
|
|
||||||
thread_id: ThreadId,
|
|
||||||
model_settings: ResumeModelSettings,
|
|
||||||
) -> Result<AppServerStartedThread> {
|
|
||||||
let request_id = self.next_request_id();
|
|
||||||
let session_config = if model_settings == ResumeModelSettings::RestoreFromThread {
|
|
||||||
config.clone()
|
|
||||||
} else {
|
|
||||||
self.session_config_with_effective_service_tier(&config)
|
|
||||||
};
|
|
||||||
let mut params = thread_resume_params_from_config(
|
|
||||||
session_config,
|
|
||||||
thread_id,
|
|
||||||
self.thread_params_mode(),
|
|
||||||
self.remote_cwd_override.as_deref(),
|
|
||||||
model_settings,
|
|
||||||
);
|
|
||||||
params.exclude_turns = self.history_support == ThreadHistorySupport::Paginated
|
|
||||||
&& self
|
|
||||||
.history_pagination
|
|
||||||
.get(&thread_id)
|
|
||||||
.is_none_or(|state| state.history_mode == ThreadHistoryMode::Paginated);
|
|
||||||
let mut response: ThreadResumeResponse = match self
|
|
||||||
.client
|
|
||||||
.request_typed(ClientRequest::ThreadResume {
|
|
||||||
request_id,
|
|
||||||
params: params.clone(),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(response) => response,
|
|
||||||
Err(TypedRequestError::Server { source, .. })
|
|
||||||
if params.exclude_turns && is_history_pagination_unsupported(&source) =>
|
|
||||||
{
|
|
||||||
self.history_support = ThreadHistorySupport::LegacyOnly;
|
|
||||||
params.exclude_turns = false;
|
|
||||||
let request_id = self.next_request_id();
|
|
||||||
self.client
|
|
||||||
.request_typed(ClientRequest::ThreadResume { request_id, params })
|
|
||||||
.await
|
|
||||||
.map_err(|err| {
|
|
||||||
bootstrap_request_error("thread/resume failed during TUI bootstrap", err)
|
|
||||||
})?
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
return Err(bootstrap_request_error(
|
|
||||||
"thread/resume failed during TUI bootstrap",
|
|
||||||
err,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
self.hydrate_initial_thread_history(
|
|
||||||
&mut response.thread,
|
|
||||||
response.turns_backwards_cursor.clone(),
|
|
||||||
response.items_backwards_cursor.clone(),
|
|
||||||
Some(&config),
|
|
||||||
HistoryHydrationScope::Initial,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let fork_parent_title = self
|
|
||||||
.fork_parent_title_from_app_server(response.thread.forked_from_id.as_deref())
|
|
||||||
.await;
|
|
||||||
let mut started =
|
|
||||||
started_thread_from_resume_response(response, &config, self.thread_params_mode())
|
|
||||||
.await?;
|
|
||||||
started.session.fork_parent_title = fork_parent_title;
|
|
||||||
Ok(started)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn fork_thread(
|
pub(crate) async fn fork_thread(
|
||||||
&mut self,
|
&mut self,
|
||||||
config: Config,
|
config: Config,
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
//! Resume history policy for local rollouts that may migrate between formats.
|
||||||
|
|
||||||
|
use super::AppServerSession;
|
||||||
|
use super::AppServerStartedThread;
|
||||||
|
use super::HistoryHydrationScope;
|
||||||
|
use super::ResumeModelSettings;
|
||||||
|
use super::ThreadHistorySupport;
|
||||||
|
use super::bootstrap_request_error;
|
||||||
|
use super::is_history_pagination_unsupported;
|
||||||
|
use super::started_thread_from_resume_response;
|
||||||
|
use super::thread_resume_params_from_config;
|
||||||
|
use crate::legacy_core::config::Config;
|
||||||
|
use codex_app_server_client::TypedRequestError;
|
||||||
|
use codex_app_server_protocol::ClientRequest;
|
||||||
|
use codex_app_server_protocol::ThreadHistoryMode;
|
||||||
|
use codex_app_server_protocol::ThreadResumeResponse;
|
||||||
|
use codex_features::Feature;
|
||||||
|
use codex_protocol::ThreadId;
|
||||||
|
use color_eyre::eyre::Result;
|
||||||
|
|
||||||
|
impl AppServerSession {
|
||||||
|
/// Captures the server's startup migration policy before workspace config can change.
|
||||||
|
///
|
||||||
|
/// Sessions without a recorded startup config conservatively assume migration is enabled.
|
||||||
|
pub(crate) fn with_startup_config(mut self, config: &Config) -> Self {
|
||||||
|
self.background_rollout_migration_enabled = config
|
||||||
|
.features
|
||||||
|
.enabled(Feature::BackgroundPaginatedRolloutMigration);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retains trusted embedded-server history metadata for a later resume.
|
||||||
|
///
|
||||||
|
/// Remote hints are ignored because another process can migrate the thread before resume.
|
||||||
|
pub(crate) fn remember_thread_history_mode(
|
||||||
|
&mut self,
|
||||||
|
thread_id: ThreadId,
|
||||||
|
history_mode: ThreadHistoryMode,
|
||||||
|
) {
|
||||||
|
if !self.uses_embedded_app_server() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.history_pagination
|
||||||
|
.entry(thread_id)
|
||||||
|
.or_default()
|
||||||
|
.history_mode = history_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn resume_thread(
|
||||||
|
&mut self,
|
||||||
|
config: Config,
|
||||||
|
thread_id: ThreadId,
|
||||||
|
model_settings: ResumeModelSettings,
|
||||||
|
) -> Result<AppServerStartedThread> {
|
||||||
|
let session_config = if model_settings == ResumeModelSettings::RestoreFromThread {
|
||||||
|
config.clone()
|
||||||
|
} else {
|
||||||
|
self.session_config_with_effective_service_tier(&config)
|
||||||
|
};
|
||||||
|
let mut params = thread_resume_params_from_config(
|
||||||
|
session_config,
|
||||||
|
thread_id,
|
||||||
|
self.thread_params_mode(),
|
||||||
|
self.remote_cwd_override.as_deref(),
|
||||||
|
model_settings,
|
||||||
|
);
|
||||||
|
let mut rollout_maintenance_guard = None;
|
||||||
|
params.exclude_turns = if self.history_support == ThreadHistorySupport::Paginated {
|
||||||
|
let known_legacy_history = self
|
||||||
|
.history_pagination
|
||||||
|
.get(&thread_id)
|
||||||
|
.is_some_and(|state| state.history_mode == ThreadHistoryMode::Legacy)
|
||||||
|
&& (!self.uses_embedded_app_server()
|
||||||
|
|| (!self.background_rollout_migration_enabled
|
||||||
|
&& !config
|
||||||
|
.features
|
||||||
|
.enabled(Feature::BackgroundPaginatedRolloutMigration)
|
||||||
|
&& {
|
||||||
|
rollout_maintenance_guard =
|
||||||
|
codex_rollout::try_acquire_rollout_maintenance_lock(
|
||||||
|
config.codex_home.as_path(),
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.flatten();
|
||||||
|
rollout_maintenance_guard.is_some()
|
||||||
|
}
|
||||||
|
&& self
|
||||||
|
.thread_read(thread_id, /*include_turns*/ false)
|
||||||
|
.await
|
||||||
|
.is_ok_and(|thread| thread.history_mode == ThreadHistoryMode::Legacy)));
|
||||||
|
!known_legacy_history
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
if params.exclude_turns {
|
||||||
|
rollout_maintenance_guard = None;
|
||||||
|
}
|
||||||
|
let request_id = self.next_request_id();
|
||||||
|
let resume_response = self
|
||||||
|
.client
|
||||||
|
.request_typed(ClientRequest::ThreadResume {
|
||||||
|
request_id,
|
||||||
|
params: params.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
drop(rollout_maintenance_guard);
|
||||||
|
let mut response: ThreadResumeResponse = match resume_response {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(TypedRequestError::Server { source, .. })
|
||||||
|
if params.exclude_turns && is_history_pagination_unsupported(&source) =>
|
||||||
|
{
|
||||||
|
self.history_support = ThreadHistorySupport::LegacyOnly;
|
||||||
|
params.exclude_turns = false;
|
||||||
|
let request_id = self.next_request_id();
|
||||||
|
self.client
|
||||||
|
.request_typed(ClientRequest::ThreadResume { request_id, params })
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
bootstrap_request_error("thread/resume failed during TUI bootstrap", err)
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(bootstrap_request_error(
|
||||||
|
"thread/resume failed during TUI bootstrap",
|
||||||
|
err,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.hydrate_initial_thread_history(
|
||||||
|
&mut response.thread,
|
||||||
|
response.turns_backwards_cursor.clone(),
|
||||||
|
response.items_backwards_cursor.clone(),
|
||||||
|
Some(&config),
|
||||||
|
HistoryHydrationScope::Initial,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let fork_parent_title = self
|
||||||
|
.fork_parent_title_from_app_server(response.thread.forked_from_id.as_deref())
|
||||||
|
.await;
|
||||||
|
let mut started =
|
||||||
|
started_thread_from_resume_response(response, &config, self.thread_params_mode())
|
||||||
|
.await?;
|
||||||
|
started.session.fork_parent_title = fork_parent_title;
|
||||||
|
Ok(started)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "rollout_history_tests.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
use super::super::ResumeModelSettings;
|
||||||
|
use crate::legacy_core::config::Config;
|
||||||
|
use crate::legacy_core::config::ConfigBuilder;
|
||||||
|
use app_test_support::create_fake_paginated_rollout;
|
||||||
|
use app_test_support::create_fake_rollout;
|
||||||
|
use codex_app_server_protocol::ThreadHistoryMode;
|
||||||
|
use codex_features::Feature;
|
||||||
|
use codex_protocol::ThreadId;
|
||||||
|
use color_eyre::eyre::Result;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
async fn build_config(temp_dir: &TempDir) -> Config {
|
||||||
|
ConfigBuilder::default()
|
||||||
|
.codex_home(temp_dir.path().to_path_buf())
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.expect("config should build")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn legacy_resume_preserves_history_mode_after_picker_server_replacement() -> Result<()> {
|
||||||
|
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||||
|
let config = build_config(&codex_home).await;
|
||||||
|
let thread_id = ThreadId::from_string(
|
||||||
|
&create_fake_rollout(
|
||||||
|
codex_home.path(),
|
||||||
|
"2025-01-05T12-00-00",
|
||||||
|
"2025-01-05T12:00:00Z",
|
||||||
|
"Saved user message",
|
||||||
|
Some(config.model_provider_id.as_str()),
|
||||||
|
/*git_info*/ None,
|
||||||
|
)
|
||||||
|
.expect("create source rollout"),
|
||||||
|
)?;
|
||||||
|
let mut picker_app_server = crate::start_embedded_app_server_for_picker(&config).await?;
|
||||||
|
let history_mode = picker_app_server
|
||||||
|
.thread_read(thread_id, /*include_turns*/ false)
|
||||||
|
.await?
|
||||||
|
.history_mode;
|
||||||
|
picker_app_server.shutdown().await?;
|
||||||
|
|
||||||
|
let mut app_server = crate::start_embedded_app_server_for_picker(&config).await?;
|
||||||
|
app_server.remember_thread_history_mode(thread_id, history_mode);
|
||||||
|
let next_request_id = app_server.next_request_id;
|
||||||
|
let resumed = app_server
|
||||||
|
.resume_thread(config, thread_id, ResumeModelSettings::RestoreFromThread)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_eq!(app_server.next_request_id, next_request_id + 2);
|
||||||
|
assert!(!resumed.turns.is_empty());
|
||||||
|
app_server.shutdown().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn background_migration_disables_cached_legacy_resume_shortcut() -> Result<()> {
|
||||||
|
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||||
|
let config = build_config(&codex_home).await;
|
||||||
|
let legacy_thread_id = ThreadId::from_string(
|
||||||
|
&create_fake_rollout(
|
||||||
|
codex_home.path(),
|
||||||
|
"2025-01-05T12-00-00",
|
||||||
|
"2025-01-05T12:00:00Z",
|
||||||
|
"Saved legacy user message",
|
||||||
|
Some(config.model_provider_id.as_str()),
|
||||||
|
/*git_info*/ None,
|
||||||
|
)
|
||||||
|
.expect("create legacy rollout"),
|
||||||
|
)?;
|
||||||
|
let thread_id = ThreadId::from_string(
|
||||||
|
&create_fake_paginated_rollout(
|
||||||
|
codex_home.path(),
|
||||||
|
"2025-01-05T12-00-01",
|
||||||
|
"2025-01-05T12:00:01Z",
|
||||||
|
"Saved paginated user message",
|
||||||
|
Some(config.model_provider_id.as_str()),
|
||||||
|
/*git_info*/ None,
|
||||||
|
)
|
||||||
|
.expect("create paginated rollout"),
|
||||||
|
)?;
|
||||||
|
let mut migration_config = config.clone();
|
||||||
|
migration_config
|
||||||
|
.features
|
||||||
|
.enable(Feature::BackgroundPaginatedRolloutMigration)?;
|
||||||
|
|
||||||
|
for (startup_config, resume_config) in
|
||||||
|
[(&config, &migration_config), (&migration_config, &config)]
|
||||||
|
{
|
||||||
|
// Keep the real worker disabled while exercising both startup/request mismatches.
|
||||||
|
let mut app_server = crate::start_embedded_app_server_for_picker(&config)
|
||||||
|
.await?
|
||||||
|
.with_startup_config(startup_config);
|
||||||
|
app_server.remember_thread_history_mode(legacy_thread_id, ThreadHistoryMode::Legacy);
|
||||||
|
let next_request_id = app_server.next_request_id;
|
||||||
|
let legacy = app_server
|
||||||
|
.resume_thread(
|
||||||
|
resume_config.clone(),
|
||||||
|
legacy_thread_id,
|
||||||
|
ResumeModelSettings::RestoreFromThread,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(app_server.next_request_id, next_request_id + 2);
|
||||||
|
assert!(!legacy.turns.is_empty());
|
||||||
|
|
||||||
|
app_server.remember_thread_history_mode(thread_id, ThreadHistoryMode::Legacy);
|
||||||
|
let next_request_id = app_server.next_request_id;
|
||||||
|
|
||||||
|
let resumed = app_server
|
||||||
|
.resume_thread(
|
||||||
|
resume_config.clone(),
|
||||||
|
thread_id,
|
||||||
|
ResumeModelSettings::RestoreFromThread,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert!(app_server.next_request_id > next_request_id + 1);
|
||||||
|
assert_eq!(resumed.session.thread_id, thread_id);
|
||||||
|
assert_eq!(
|
||||||
|
app_server
|
||||||
|
.history_pagination
|
||||||
|
.get(&thread_id)
|
||||||
|
.map(|state| state.history_mode),
|
||||||
|
Some(ThreadHistoryMode::Paginated)
|
||||||
|
);
|
||||||
|
app_server.shutdown().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn rollout_maintenance_contention_disables_cached_legacy_resume_shortcut() -> Result<()> {
|
||||||
|
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||||
|
let config = build_config(&codex_home).await;
|
||||||
|
let thread_id = ThreadId::from_string(
|
||||||
|
&create_fake_paginated_rollout(
|
||||||
|
codex_home.path(),
|
||||||
|
"2025-01-05T12-00-00",
|
||||||
|
"2025-01-05T12:00:00Z",
|
||||||
|
"Saved paginated user message",
|
||||||
|
Some(config.model_provider_id.as_str()),
|
||||||
|
/*git_info*/ None,
|
||||||
|
)
|
||||||
|
.expect("create paginated rollout"),
|
||||||
|
)?;
|
||||||
|
let mut app_server = crate::start_embedded_app_server_for_picker(&config).await?;
|
||||||
|
app_server.remember_thread_history_mode(thread_id, ThreadHistoryMode::Legacy);
|
||||||
|
let _maintenance_guard =
|
||||||
|
codex_rollout::try_acquire_rollout_maintenance_lock(codex_home.path())?
|
||||||
|
.expect("acquire rollout maintenance lock");
|
||||||
|
let next_request_id = app_server.next_request_id;
|
||||||
|
|
||||||
|
let resumed = app_server
|
||||||
|
.resume_thread(config, thread_id, ResumeModelSettings::RestoreFromThread)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_eq!(app_server.next_request_id, next_request_id + 3);
|
||||||
|
assert_eq!(resumed.session.thread_id, thread_id);
|
||||||
|
assert_eq!(
|
||||||
|
app_server
|
||||||
|
.history_pagination
|
||||||
|
.get(&thread_id)
|
||||||
|
.map(|state| state.history_mode),
|
||||||
|
Some(ThreadHistoryMode::Paginated)
|
||||||
|
);
|
||||||
|
|
||||||
|
app_server.shutdown().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn stale_legacy_history_mode_is_revalidated_before_resume() -> Result<()> {
|
||||||
|
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||||
|
let config = build_config(&codex_home).await;
|
||||||
|
let thread_id = ThreadId::from_string(
|
||||||
|
&create_fake_paginated_rollout(
|
||||||
|
codex_home.path(),
|
||||||
|
"2025-01-05T12-00-00",
|
||||||
|
"2025-01-05T12:00:00Z",
|
||||||
|
"Saved paginated user message",
|
||||||
|
Some(config.model_provider_id.as_str()),
|
||||||
|
/*git_info*/ None,
|
||||||
|
)
|
||||||
|
.expect("create paginated rollout"),
|
||||||
|
)?;
|
||||||
|
let mut app_server = crate::start_embedded_app_server_for_picker(&config).await?;
|
||||||
|
app_server.remember_thread_history_mode(thread_id, ThreadHistoryMode::Legacy);
|
||||||
|
let next_request_id = app_server.next_request_id;
|
||||||
|
|
||||||
|
let resumed = app_server
|
||||||
|
.resume_thread(
|
||||||
|
config.clone(),
|
||||||
|
thread_id,
|
||||||
|
ResumeModelSettings::RestoreFromThread,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(resumed.session.thread_id, thread_id);
|
||||||
|
assert!(app_server.next_request_id >= next_request_id + 4);
|
||||||
|
assert_eq!(
|
||||||
|
app_server
|
||||||
|
.history_pagination
|
||||||
|
.get(&thread_id)
|
||||||
|
.map(|state| state.history_mode),
|
||||||
|
Some(ThreadHistoryMode::Paginated)
|
||||||
|
);
|
||||||
|
|
||||||
|
let missing_thread_id = ThreadId::new();
|
||||||
|
app_server.remember_thread_history_mode(missing_thread_id, ThreadHistoryMode::Legacy);
|
||||||
|
let next_request_id = app_server.next_request_id;
|
||||||
|
assert!(
|
||||||
|
app_server
|
||||||
|
.resume_thread(
|
||||||
|
config,
|
||||||
|
missing_thread_id,
|
||||||
|
ResumeModelSettings::RestoreFromThread
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert_eq!(app_server.next_request_id, next_request_id + 2);
|
||||||
|
|
||||||
|
app_server.shutdown().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -511,10 +511,7 @@ pub(crate) async fn start_app_server_for_picker(
|
|||||||
environment_manager,
|
environment_manager,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(AppServerSession::new(
|
Ok(AppServerSession::new(app_server, target.thread_params_mode()).with_startup_config(config))
|
||||||
app_server,
|
|
||||||
target.thread_params_mode(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -601,6 +598,7 @@ fn session_target_from_app_server_thread(
|
|||||||
Ok(thread_id) => Some(resume_picker::SessionTarget {
|
Ok(thread_id) => Some(resume_picker::SessionTarget {
|
||||||
path: thread.path,
|
path: thread.path,
|
||||||
thread_id,
|
thread_id,
|
||||||
|
history_mode: Some(thread.history_mode),
|
||||||
}),
|
}),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(
|
warn!(
|
||||||
@@ -1384,7 +1382,8 @@ async fn run_ratatui_app(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(app_server) => AppServerSession::new(app_server, app_server_target.thread_params_mode()),
|
Ok(app_server) => AppServerSession::new(app_server, app_server_target.thread_params_mode())
|
||||||
|
.with_startup_config(&initial_config),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
terminal_restore_guard.restore_silently();
|
terminal_restore_guard.restore_silently();
|
||||||
session_log::log_session_end();
|
session_log::log_session_end();
|
||||||
@@ -1751,6 +1750,7 @@ async fn run_ratatui_app(
|
|||||||
{
|
{
|
||||||
Ok(app_server) => {
|
Ok(app_server) => {
|
||||||
AppServerSession::new(app_server, app_server_target.thread_params_mode())
|
AppServerSession::new(app_server, app_server_target.thread_params_mode())
|
||||||
|
.with_startup_config(&config)
|
||||||
.with_remote_cwd_override(remote_cwd_override.clone())
|
.with_remote_cwd_override(remote_cwd_override.clone())
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -2277,6 +2277,7 @@ mod tests {
|
|||||||
let target_session = resume_picker::SessionTarget {
|
let target_session = resume_picker::SessionTarget {
|
||||||
path: Some(rollout_path),
|
path: Some(rollout_path),
|
||||||
thread_id,
|
thread_id,
|
||||||
|
history_mode: None,
|
||||||
};
|
};
|
||||||
let session_selection = match action {
|
let session_selection = match action {
|
||||||
CwdPromptAction::Resume => resume_picker::SessionSelection::Resume(target_session),
|
CwdPromptAction::Resume => resume_picker::SessionSelection::Resume(target_session),
|
||||||
@@ -2364,6 +2365,7 @@ mod tests {
|
|||||||
&resume_picker::SessionSelection::Resume(resume_picker::SessionTarget {
|
&resume_picker::SessionSelection::Resume(resume_picker::SessionTarget {
|
||||||
path: None,
|
path: None,
|
||||||
thread_id: ThreadId::new(),
|
thread_id: ThreadId::new(),
|
||||||
|
history_mode: None,
|
||||||
}),
|
}),
|
||||||
/*cwd_override*/ None,
|
/*cwd_override*/ None,
|
||||||
/*uses_remote_workspace*/ false,
|
/*uses_remote_workspace*/ false,
|
||||||
@@ -2400,6 +2402,7 @@ mod tests {
|
|||||||
&resume_picker::SessionSelection::Resume(resume_picker::SessionTarget {
|
&resume_picker::SessionSelection::Resume(resume_picker::SessionTarget {
|
||||||
path: None,
|
path: None,
|
||||||
thread_id: ThreadId::new(),
|
thread_id: ThreadId::new(),
|
||||||
|
history_mode: None,
|
||||||
}),
|
}),
|
||||||
/*cwd_override*/ None,
|
/*cwd_override*/ None,
|
||||||
/*uses_remote_workspace*/ false,
|
/*uses_remote_workspace*/ false,
|
||||||
@@ -2441,6 +2444,7 @@ mod tests {
|
|||||||
let target = crate::resume_picker::SessionTarget {
|
let target = crate::resume_picker::SessionTarget {
|
||||||
path: None,
|
path: None,
|
||||||
thread_id,
|
thread_id,
|
||||||
|
history_mode: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(target.display_label(), format!("thread {thread_id}"));
|
assert_eq!(target.display_label(), format!("thread {thread_id}"));
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ async fn lookup_legacy_index_target(
|
|||||||
return Ok(Some(SessionTarget {
|
return Ok(Some(SessionTarget {
|
||||||
path: Some(path),
|
path: Some(path),
|
||||||
thread_id: session_meta.meta.id,
|
thread_id: session_meta.meta.id,
|
||||||
|
history_mode: Some(thread.history_mode),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ const PICKER_LIST_HORIZONTAL_INSET: u16 = 4;
|
|||||||
pub struct SessionTarget {
|
pub struct SessionTarget {
|
||||||
pub path: Option<PathBuf>,
|
pub path: Option<PathBuf>,
|
||||||
pub thread_id: ThreadId,
|
pub thread_id: ThreadId,
|
||||||
|
/// History mode observed during selection, if the server provided one.
|
||||||
|
pub history_mode: Option<ThreadHistoryMode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SessionTarget {
|
impl SessionTarget {
|
||||||
@@ -150,8 +152,7 @@ impl SessionPickerAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn selection(self, path: Option<PathBuf>, thread_id: ThreadId) -> SessionSelection {
|
fn selection(self, target_session: SessionTarget) -> SessionSelection {
|
||||||
let target_session = SessionTarget { path, thread_id };
|
|
||||||
match self {
|
match self {
|
||||||
SessionPickerAction::Resume => SessionSelection::Resume(target_session),
|
SessionPickerAction::Resume => SessionSelection::Resume(target_session),
|
||||||
SessionPickerAction::Fork => SessionSelection::Fork(target_session),
|
SessionPickerAction::Fork => SessionSelection::Fork(target_session),
|
||||||
@@ -315,6 +316,7 @@ enum PageCursor {
|
|||||||
|
|
||||||
struct PickerPage {
|
struct PickerPage {
|
||||||
rows: Vec<Row>,
|
rows: Vec<Row>,
|
||||||
|
history_modes: HashMap<ThreadId, ThreadHistoryMode>,
|
||||||
next_cursor: Option<PageCursor>,
|
next_cursor: Option<PageCursor>,
|
||||||
num_scanned_files: usize,
|
num_scanned_files: usize,
|
||||||
reached_scan_cap: bool,
|
reached_scan_cap: bool,
|
||||||
@@ -724,6 +726,7 @@ fn spawn_app_server_page_loader(
|
|||||||
.map(|response| SessionTarget {
|
.map(|response| SessionTarget {
|
||||||
path: response.thread.path,
|
path: response.thread.path,
|
||||||
thread_id,
|
thread_id,
|
||||||
|
history_mode: Some(response.thread.history_mode),
|
||||||
})
|
})
|
||||||
.map_err(std::io::Error::other);
|
.map_err(std::io::Error::other);
|
||||||
let _ = bg_tx.send(BackgroundEvent::Unarchive { thread_id, result });
|
let _ = bg_tx.send(BackgroundEvent::Unarchive { thread_id, result });
|
||||||
@@ -774,6 +777,7 @@ struct PickerState {
|
|||||||
pagination: PaginationState,
|
pagination: PaginationState,
|
||||||
all_rows: Vec<Row>,
|
all_rows: Vec<Row>,
|
||||||
filtered_rows: Vec<Row>,
|
filtered_rows: Vec<Row>,
|
||||||
|
thread_history_modes: HashMap<ThreadId, ThreadHistoryMode>,
|
||||||
seen_rows: HashSet<SeenRowKey>,
|
seen_rows: HashSet<SeenRowKey>,
|
||||||
selected: usize,
|
selected: usize,
|
||||||
scroll_top: usize,
|
scroll_top: usize,
|
||||||
@@ -858,13 +862,20 @@ async fn load_app_server_page(
|
|||||||
.await
|
.await
|
||||||
.map_err(std::io::Error::other)?;
|
.map_err(std::io::Error::other)?;
|
||||||
let num_scanned_files = response.data.len();
|
let num_scanned_files = response.data.len();
|
||||||
|
let (rows, history_modes): (Vec<_>, HashMap<_, _>) = response
|
||||||
|
.data
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|thread| {
|
||||||
|
let history_mode = thread.history_mode;
|
||||||
|
let row = row_from_app_server_thread(thread)?;
|
||||||
|
let thread_id = row.thread_id?;
|
||||||
|
Some((row, (thread_id, history_mode)))
|
||||||
|
})
|
||||||
|
.unzip();
|
||||||
|
|
||||||
Ok(PickerPage {
|
Ok(PickerPage {
|
||||||
rows: response
|
rows,
|
||||||
.data
|
history_modes,
|
||||||
.into_iter()
|
|
||||||
.filter_map(row_from_app_server_thread)
|
|
||||||
.collect(),
|
|
||||||
next_cursor: response.next_cursor.map(PageCursor::AppServer),
|
next_cursor: response.next_cursor.map(PageCursor::AppServer),
|
||||||
num_scanned_files,
|
num_scanned_files,
|
||||||
reached_scan_cap: false,
|
reached_scan_cap: false,
|
||||||
@@ -1101,6 +1112,7 @@ impl PickerState {
|
|||||||
pagination: PaginationState::new(),
|
pagination: PaginationState::new(),
|
||||||
all_rows: Vec::new(),
|
all_rows: Vec::new(),
|
||||||
filtered_rows: Vec::new(),
|
filtered_rows: Vec::new(),
|
||||||
|
thread_history_modes: HashMap::new(),
|
||||||
seen_rows: HashSet::new(),
|
seen_rows: HashSet::new(),
|
||||||
selected: 0,
|
selected: 0,
|
||||||
scroll_top: 0,
|
scroll_top: 0,
|
||||||
@@ -1362,7 +1374,11 @@ impl PickerState {
|
|||||||
self.request_unarchive(thread_id);
|
self.request_unarchive(thread_id);
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
return Ok(Some(self.action.selection(path, thread_id)));
|
return Ok(Some(self.action.selection(SessionTarget {
|
||||||
|
path,
|
||||||
|
thread_id,
|
||||||
|
history_mode: self.thread_history_modes.get(&thread_id).copied(),
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
self.inline_error = Some(match path {
|
self.inline_error = Some(match path {
|
||||||
Some(path) => {
|
Some(path) => {
|
||||||
@@ -1497,6 +1513,7 @@ impl PickerState {
|
|||||||
self.reset_pagination();
|
self.reset_pagination();
|
||||||
self.all_rows.clear();
|
self.all_rows.clear();
|
||||||
self.filtered_rows.clear();
|
self.filtered_rows.clear();
|
||||||
|
self.thread_history_modes.clear();
|
||||||
self.seen_rows.clear();
|
self.seen_rows.clear();
|
||||||
self.selected = 0;
|
self.selected = 0;
|
||||||
self.pending_page_down_target = None;
|
self.pending_page_down_target = None;
|
||||||
@@ -1629,12 +1646,14 @@ impl PickerState {
|
|||||||
fn ingest_page(&mut self, page: PickerPage) {
|
fn ingest_page(&mut self, page: PickerPage) {
|
||||||
let PickerPage {
|
let PickerPage {
|
||||||
rows,
|
rows,
|
||||||
|
history_modes,
|
||||||
next_cursor,
|
next_cursor,
|
||||||
num_scanned_files,
|
num_scanned_files,
|
||||||
reached_scan_cap,
|
reached_scan_cap,
|
||||||
} = page;
|
} = page;
|
||||||
self.pagination
|
self.pagination
|
||||||
.complete_page(next_cursor, num_scanned_files, reached_scan_cap);
|
.complete_page(next_cursor, num_scanned_files, reached_scan_cap);
|
||||||
|
self.thread_history_modes.extend(history_modes);
|
||||||
|
|
||||||
for row in rows {
|
for row in rows {
|
||||||
if let Some(seen_key) = row.seen_key() {
|
if let Some(seen_key) = row.seen_key() {
|
||||||
@@ -3573,6 +3592,7 @@ mod tests {
|
|||||||
) -> PickerPage {
|
) -> PickerPage {
|
||||||
PickerPage {
|
PickerPage {
|
||||||
rows,
|
rows,
|
||||||
|
history_modes: HashMap::new(),
|
||||||
next_cursor: next_cursor.map(|cursor| PageCursor::AppServer(cursor.to_string())),
|
next_cursor: next_cursor.map(|cursor| PageCursor::AppServer(cursor.to_string())),
|
||||||
num_scanned_files,
|
num_scanned_files,
|
||||||
reached_scan_cap,
|
reached_scan_cap,
|
||||||
@@ -3843,12 +3863,12 @@ mod tests {
|
|||||||
"indexed metadata",
|
"indexed metadata",
|
||||||
);
|
);
|
||||||
row.thread_id = Some(thread_id);
|
row.thread_id = Some(thread_id);
|
||||||
deliver_page(
|
let mut listed_page = ok_page(vec![row], /*next_cursor*/ None)
|
||||||
&mut state,
|
.expect("indexed thread page should be available");
|
||||||
&db_request,
|
listed_page
|
||||||
ok_page(vec![row], /*next_cursor*/ None),
|
.history_modes
|
||||||
)
|
.insert(thread_id, ThreadHistoryMode::Legacy);
|
||||||
.await;
|
deliver_page(&mut state, &db_request, Ok(listed_page)).await;
|
||||||
|
|
||||||
let selection = state
|
let selection = state
|
||||||
.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))
|
.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))
|
||||||
@@ -3858,6 +3878,7 @@ mod tests {
|
|||||||
selection,
|
selection,
|
||||||
Some(SessionSelection::Resume(SessionTarget {
|
Some(SessionSelection::Resume(SessionTarget {
|
||||||
thread_id: selected_thread_id,
|
thread_id: selected_thread_id,
|
||||||
|
history_mode: Some(ThreadHistoryMode::Legacy),
|
||||||
..
|
..
|
||||||
})) if selected_thread_id == thread_id
|
})) if selected_thread_id == thread_id
|
||||||
));
|
));
|
||||||
@@ -6342,6 +6363,7 @@ session_picker_view = "dense"
|
|||||||
Some(SessionSelection::Resume(SessionTarget {
|
Some(SessionSelection::Resume(SessionTarget {
|
||||||
path: None,
|
path: None,
|
||||||
thread_id: selected_thread_id,
|
thread_id: selected_thread_id,
|
||||||
|
history_mode: None,
|
||||||
})) => assert_eq!(selected_thread_id, thread_id),
|
})) => assert_eq!(selected_thread_id, thread_id),
|
||||||
other => panic!("unexpected selection: {other:?}"),
|
other => panic!("unexpected selection: {other:?}"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ async fn archived_session_restore_resumes_after_completion() {
|
|||||||
Ok(SessionTarget {
|
Ok(SessionTarget {
|
||||||
path: None,
|
path: None,
|
||||||
thread_id,
|
thread_id,
|
||||||
|
history_mode: None,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -264,6 +265,7 @@ async fn archived_session_restore_resumes_after_completion() {
|
|||||||
Some(SessionSelection::Resume(SessionTarget {
|
Some(SessionSelection::Resume(SessionTarget {
|
||||||
path: None,
|
path: None,
|
||||||
thread_id: resumed_thread_id,
|
thread_id: resumed_thread_id,
|
||||||
|
history_mode: None,
|
||||||
})) if resumed_thread_id == thread_id
|
})) if resumed_thread_id == thread_id
|
||||||
));
|
));
|
||||||
assert_eq!(state.archive_state, ArchiveState::Idle);
|
assert_eq!(state.archive_state, ArchiveState::Idle);
|
||||||
|
|||||||
@@ -502,6 +502,7 @@ mod tests {
|
|||||||
&SessionTarget {
|
&SessionTarget {
|
||||||
path: Some(rollout_path.clone()),
|
path: Some(rollout_path.clone()),
|
||||||
thread_id,
|
thread_id,
|
||||||
|
history_mode: None,
|
||||||
},
|
},
|
||||||
CwdPromptAction::Fork,
|
CwdPromptAction::Fork,
|
||||||
ResumeCwdContext {
|
ResumeCwdContext {
|
||||||
@@ -535,6 +536,7 @@ mod tests {
|
|||||||
&SessionTarget {
|
&SessionTarget {
|
||||||
path: None,
|
path: None,
|
||||||
thread_id: ThreadId::new(),
|
thread_id: ThreadId::new(),
|
||||||
|
history_mode: None,
|
||||||
},
|
},
|
||||||
CwdPromptAction::Resume,
|
CwdPromptAction::Resume,
|
||||||
ResumeCwdContext {
|
ResumeCwdContext {
|
||||||
|
|||||||
Reference in new issue
Block a user