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:
acrognale-oai authored and GitHub committed 2026-04-22 06:10:09 -04:00
1 parent b04ffeee4c
commit 4f8c58f737
32 files changed
+1183 -133

No files matched your search

@@ -68,7 +68,35 @@ async fn list_rollout_threads(
sort_key: codex_rollout::ThreadSortKey,
sort_direction: codex_rollout::SortDirection,
) -> ThreadStoreResult<codex_rollout::ThreadsPage> {
let page = if params.archived {
let page = if params.use_state_db_only && params.archived {
RolloutRecorder::list_archived_threads_from_state_db(
config,
params.page_size,
cursor,
sort_key,
sort_direction,
params.allowed_sources.as_slice(),
params.model_providers.as_deref(),
params.cwd_filters.as_deref(),
config.model_provider_id.as_str(),
params.search_term.as_deref(),
)
.await
} else if params.use_state_db_only {
RolloutRecorder::list_threads_from_state_db(
config,
params.page_size,
cursor,
sort_key,
sort_direction,
params.allowed_sources.as_slice(),
params.model_providers.as_deref(),
params.cwd_filters.as_deref(),
config.model_provider_id.as_str(),
params.search_term.as_deref(),
)
.await
} else if params.archived {
RolloutRecorder::list_archived_threads(
config,
params.page_size,
@@ -77,6 +105,7 @@ async fn list_rollout_threads(
sort_direction,
params.allowed_sources.as_slice(),
params.model_providers.as_deref(),
params.cwd_filters.as_deref(),
config.model_provider_id.as_str(),
params.search_term.as_deref(),
)
@@ -90,6 +119,7 @@ async fn list_rollout_threads(
sort_direction,
params.allowed_sources.as_slice(),
params.model_providers.as_deref(),
params.cwd_filters.as_deref(),
config.model_provider_id.as_str(),
params.search_term.as_deref(),
)
@@ -140,8 +170,10 @@ mod tests {
sort_direction: SortDirection::Desc,
allowed_sources: Vec::new(),
model_providers: None,
cwd_filters: None,
archived: false,
search_term: None,
use_state_db_only: false,
})
.await
.expect("thread listing");
@@ -196,8 +228,10 @@ mod tests {
sort_direction: SortDirection::Desc,
allowed_sources: Vec::new(),
model_providers: None,
cwd_filters: None,
archived: false,
search_term: Some("needle".to_string()),
use_state_db_only: true,
})
.await
.expect("thread listing");
@@ -233,8 +267,10 @@ mod tests {
sort_direction: SortDirection::Desc,
allowed_sources: Vec::new(),
model_providers: None,
cwd_filters: None,
archived: false,
search_term: None,
use_state_db_only: false,
})
.await
.expect("active listing");
@@ -246,8 +282,10 @@ mod tests {
sort_direction: SortDirection::Desc,
allowed_sources: Vec::new(),
model_providers: None,
cwd_filters: None,
archived: true,
search_term: None,
use_state_db_only: false,
})
.await
.expect("archived listing");
@@ -295,8 +333,10 @@ mod tests {
sort_direction: SortDirection::Desc,
allowed_sources: vec![SessionSource::Cli],
model_providers: Some(vec!["test-provider".to_string()]),
cwd_filters: None,
archived: false,
search_term: None,
use_state_db_only: false,
})
.await
.expect("thread listing");
@@ -329,8 +369,10 @@ mod tests {
sort_direction: SortDirection::Desc,
allowed_sources: Vec::new(),
model_providers: None,
cwd_filters: None,
archived: false,
search_term: None,
use_state_db_only: false,
})
.await
.expect_err("invalid cursor should fail");