mirror of
https://github.com/openai/codex.git
synced 2026-05-03 21:01:55 +03:00
Support multiple cwd filters for thread list (#18502)
## Summary - Teach app-server `thread/list` to accept either a single `cwd` or an array of cwd filters, returning threads whose recorded session cwd matches any requested path - Add `useStateDbOnly` as an explicit opt-in fast path for callers that want to answer `thread/list` from SQLite without scanning JSONL rollout files - Preserve backwards compatibility: by default, `thread/list` still scans JSONL rollouts and repairs SQLite state - Wire the new cwd array and SQLite-only options through app-server, local/remote thread-store, rollout listing, generated TypeScript/schema fixtures, proto output, and docs ## Test Plan - `cargo test -p codex-app-server-protocol` - `cargo test -p codex-rollout` - `cargo test -p codex-thread-store` - `cargo test -p codex-app-server thread_list` - `just fmt` - `just fix -p codex-app-server-protocol -p codex-rollout -p codex-thread-store -p codex-app-server` - `cargo build -p codex-cli --bin codex`
This commit is contained in:
@@ -548,6 +548,7 @@ async fn thread_fork_ephemeral_remains_pathless_and_omits_listing() -> Result<()
|
||||
source_kinds: None,
|
||||
archived: None,
|
||||
cwd: None,
|
||||
use_state_db_only: false,
|
||||
search_term: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -15,6 +15,7 @@ use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SessionSource;
|
||||
use codex_app_server_protocol::SortDirection;
|
||||
use codex_app_server_protocol::ThreadListCwdFilter;
|
||||
use codex_app_server_protocol::ThreadListResponse;
|
||||
use codex_app_server_protocol::ThreadSortKey;
|
||||
use codex_app_server_protocol::ThreadSourceKind;
|
||||
@@ -90,6 +91,7 @@ async fn list_threads_with_sort(
|
||||
source_kinds,
|
||||
archived,
|
||||
cwd: None,
|
||||
use_state_db_only: false,
|
||||
search_term: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -468,15 +470,23 @@ async fn thread_list_respects_provider_filter() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_list_respects_cwd_filter() -> Result<()> {
|
||||
async fn thread_list_respects_cwd_filters() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_minimal_config(codex_home.path())?;
|
||||
|
||||
let filtered_id = create_fake_rollout(
|
||||
let first_filtered_id = create_fake_rollout(
|
||||
codex_home.path(),
|
||||
"2025-01-02T10-00-00",
|
||||
"2025-01-02T10:00:00Z",
|
||||
"filtered",
|
||||
"first filtered",
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
)?;
|
||||
let second_filtered_id = create_fake_rollout(
|
||||
codex_home.path(),
|
||||
"2025-01-02T12-00-00",
|
||||
"2025-01-02T12:00:00Z",
|
||||
"second filtered",
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
)?;
|
||||
@@ -489,11 +499,22 @@ async fn thread_list_respects_cwd_filter() -> Result<()> {
|
||||
/*git_info*/ None,
|
||||
)?;
|
||||
|
||||
let target_cwd = codex_home.path().join("target-cwd");
|
||||
fs::create_dir_all(&target_cwd)?;
|
||||
let first_target_cwd = codex_home.path().join("first-target-cwd");
|
||||
let second_target_cwd = codex_home.path().join("second-target-cwd");
|
||||
fs::create_dir_all(&first_target_cwd)?;
|
||||
fs::create_dir_all(&second_target_cwd)?;
|
||||
set_rollout_cwd(
|
||||
rollout_path(codex_home.path(), "2025-01-02T10-00-00", &filtered_id).as_path(),
|
||||
&target_cwd,
|
||||
rollout_path(codex_home.path(), "2025-01-02T10-00-00", &first_filtered_id).as_path(),
|
||||
&first_target_cwd,
|
||||
)?;
|
||||
set_rollout_cwd(
|
||||
rollout_path(
|
||||
codex_home.path(),
|
||||
"2025-01-02T12-00-00",
|
||||
&second_filtered_id,
|
||||
)
|
||||
.as_path(),
|
||||
&second_target_cwd,
|
||||
)?;
|
||||
|
||||
let mut mcp = init_mcp(codex_home.path()).await?;
|
||||
@@ -506,7 +527,11 @@ async fn thread_list_respects_cwd_filter() -> Result<()> {
|
||||
model_providers: Some(vec!["mock_provider".to_string()]),
|
||||
source_kinds: None,
|
||||
archived: None,
|
||||
cwd: Some(target_cwd.to_string_lossy().into_owned()),
|
||||
cwd: Some(ThreadListCwdFilter::Many(vec![
|
||||
first_target_cwd.to_string_lossy().into_owned(),
|
||||
second_target_cwd.to_string_lossy().into_owned(),
|
||||
])),
|
||||
use_state_db_only: false,
|
||||
search_term: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -520,10 +545,14 @@ async fn thread_list_respects_cwd_filter() -> Result<()> {
|
||||
} = to_response::<ThreadListResponse>(resp)?;
|
||||
|
||||
assert_eq!(next_cursor, None);
|
||||
assert_eq!(data.len(), 1);
|
||||
assert_eq!(data[0].id, filtered_id);
|
||||
assert_ne!(data[0].id, unfiltered_id);
|
||||
assert_eq!(data[0].cwd.as_path(), target_cwd.as_path());
|
||||
let filtered_ids: Vec<_> = data.iter().map(|thread| thread.id.as_str()).collect();
|
||||
assert_eq!(
|
||||
filtered_ids,
|
||||
vec![second_filtered_id.as_str(), first_filtered_id.as_str()]
|
||||
);
|
||||
assert!(!filtered_ids.contains(&unfiltered_id.as_str()));
|
||||
assert_eq!(data[0].cwd.as_path(), second_target_cwd.as_path());
|
||||
assert_eq!(data[1].cwd.as_path(), first_target_cwd.as_path());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -592,6 +621,7 @@ sqlite = true
|
||||
codex_core::SortDirection::Desc,
|
||||
&[],
|
||||
/*model_providers*/ None,
|
||||
/*cwd_filters*/ None,
|
||||
"mock_provider",
|
||||
/*search_term*/ None,
|
||||
)
|
||||
@@ -609,6 +639,7 @@ sqlite = true
|
||||
source_kinds: None,
|
||||
archived: None,
|
||||
cwd: None,
|
||||
use_state_db_only: false,
|
||||
search_term: Some("needle".to_string()),
|
||||
})
|
||||
.await?;
|
||||
@@ -628,6 +659,129 @@ sqlite = true
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_list_state_db_only_returns_sqlite_without_jsonl_repair() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "never"
|
||||
suppress_unstable_features_warning = true
|
||||
|
||||
[features]
|
||||
sqlite = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let thread_id = create_fake_rollout(
|
||||
codex_home.path(),
|
||||
"2025-01-02T10-00-00",
|
||||
"2025-01-02T10:00:00Z",
|
||||
"state db only should not see this before repair",
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
)?;
|
||||
let state_db =
|
||||
codex_state::StateRuntime::init(codex_home.path().to_path_buf(), "mock_provider".into())
|
||||
.await?;
|
||||
state_db
|
||||
.mark_backfill_complete(/*last_watermark*/ None)
|
||||
.await?;
|
||||
let mut mcp = init_mcp(codex_home.path()).await?;
|
||||
|
||||
let request_id = mcp
|
||||
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
||||
cursor: None,
|
||||
limit: Some(10),
|
||||
sort_key: None,
|
||||
sort_direction: None,
|
||||
model_providers: Some(vec!["mock_provider".to_string()]),
|
||||
source_kinds: None,
|
||||
archived: None,
|
||||
cwd: None,
|
||||
use_state_db_only: false,
|
||||
search_term: None,
|
||||
})
|
||||
.await?;
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let repaired_response = to_response::<ThreadListResponse>(resp)?;
|
||||
let ids: Vec<_> = repaired_response
|
||||
.data
|
||||
.iter()
|
||||
.map(|thread| thread.id.as_str())
|
||||
.collect();
|
||||
assert_eq!(ids, vec![thread_id.as_str()]);
|
||||
|
||||
let thread_uuid = ThreadId::from_string(&thread_id)?;
|
||||
let stale_cwd = codex_home.path().join("stale-cwd");
|
||||
let mut metadata = state_db
|
||||
.get_thread(thread_uuid)
|
||||
.await?
|
||||
.expect("thread should be repaired into sqlite");
|
||||
metadata.cwd = stale_cwd.clone();
|
||||
state_db.upsert_thread(&metadata).await?;
|
||||
|
||||
let request_id = mcp
|
||||
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
||||
cursor: None,
|
||||
limit: Some(10),
|
||||
sort_key: None,
|
||||
sort_direction: None,
|
||||
model_providers: Some(vec!["mock_provider".to_string()]),
|
||||
source_kinds: None,
|
||||
archived: None,
|
||||
cwd: Some(ThreadListCwdFilter::One(
|
||||
stale_cwd.to_string_lossy().into_owned(),
|
||||
)),
|
||||
use_state_db_only: true,
|
||||
search_term: None,
|
||||
})
|
||||
.await?;
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let state_db_only_response = to_response::<ThreadListResponse>(resp)?;
|
||||
let ids: Vec<_> = state_db_only_response
|
||||
.data
|
||||
.iter()
|
||||
.map(|thread| thread.id.as_str())
|
||||
.collect();
|
||||
assert_eq!(ids, vec![thread_id.as_str()]);
|
||||
|
||||
let request_id = mcp
|
||||
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
||||
cursor: None,
|
||||
limit: Some(10),
|
||||
sort_key: None,
|
||||
sort_direction: None,
|
||||
model_providers: Some(vec!["mock_provider".to_string()]),
|
||||
source_kinds: None,
|
||||
archived: None,
|
||||
cwd: Some(ThreadListCwdFilter::One(
|
||||
stale_cwd.to_string_lossy().into_owned(),
|
||||
)),
|
||||
use_state_db_only: false,
|
||||
search_term: None,
|
||||
})
|
||||
.await?;
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let scanned_response = to_response::<ThreadListResponse>(resp)?;
|
||||
assert_eq!(scanned_response.data.len(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_list_empty_source_kinds_defaults_to_interactive_only() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -1307,6 +1461,7 @@ async fn thread_list_backwards_cursor_can_seed_forward_delta_sync() -> Result<()
|
||||
source_kinds: None,
|
||||
archived: None,
|
||||
cwd: None,
|
||||
use_state_db_only: false,
|
||||
search_term: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1348,6 +1503,7 @@ async fn thread_list_backwards_cursor_can_seed_forward_delta_sync() -> Result<()
|
||||
source_kinds: None,
|
||||
archived: None,
|
||||
cwd: None,
|
||||
use_state_db_only: false,
|
||||
search_term: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1585,6 +1741,7 @@ async fn thread_list_invalid_cursor_returns_error() -> Result<()> {
|
||||
source_kinds: None,
|
||||
archived: None,
|
||||
cwd: None,
|
||||
use_state_db_only: false,
|
||||
search_term: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -548,6 +548,7 @@ async fn thread_name_set_is_reflected_in_read_list_and_resume() -> Result<()> {
|
||||
source_kinds: None,
|
||||
archived: None,
|
||||
cwd: None,
|
||||
use_state_db_only: false,
|
||||
search_term: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Reference in New Issue
Block a user