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
2026-04-22 06:10:09 -04:00
committed by GitHub
parent b04ffeee4c
commit 4f8c58f737
32 changed files with 1183 additions and 133 deletions

View File

@@ -3703,15 +3703,27 @@ pub struct ThreadListParams {
/// If false or null, only non-archived threads are returned.
#[ts(optional = nullable)]
pub archived: Option<bool>,
/// Optional cwd filter; when set, only threads whose session cwd exactly
/// matches this path are returned.
#[ts(optional = nullable)]
pub cwd: Option<String>,
/// Optional cwd filter or filters; when set, only threads whose session cwd
/// exactly matches one of these paths are returned.
#[ts(optional = nullable, type = "string | Array<string> | null")]
pub cwd: Option<ThreadListCwdFilter>,
/// If true, return from the state DB without scanning JSONL rollouts to
/// repair thread metadata. Omitted or false preserves scan-and-repair
/// behavior.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub use_state_db_only: bool,
/// Optional substring filter for the extracted thread title.
#[ts(optional = nullable)]
pub search_term: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
#[serde(untagged)]
pub enum ThreadListCwdFilter {
One(String),
Many(Vec<String>),
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase", export_to = "v2/")]
@@ -7294,6 +7306,46 @@ mod tests {
absolute_path("readable")
}
#[test]
fn thread_list_params_accepts_single_cwd() {
let params = serde_json::from_value::<ThreadListParams>(json!({
"cwd": "/workspace",
}))
.expect("single cwd should deserialize");
assert_eq!(
params.cwd,
Some(ThreadListCwdFilter::One("/workspace".to_string()))
);
assert!(!params.use_state_db_only);
}
#[test]
fn thread_list_params_accepts_multiple_cwds() {
let params = serde_json::from_value::<ThreadListParams>(json!({
"cwd": ["/workspace", "/other-workspace"],
}))
.expect("cwd array should deserialize");
assert_eq!(
params.cwd,
Some(ThreadListCwdFilter::Many(vec![
"/workspace".to_string(),
"/other-workspace".to_string(),
]))
);
}
#[test]
fn thread_list_params_accepts_state_db_only_flag() {
let params = serde_json::from_value::<ThreadListParams>(json!({
"useStateDbOnly": true,
}))
.expect("state db only flag should deserialize");
assert!(params.use_state_db_only);
}
#[test]
fn collab_agent_state_maps_interrupted_status() {
assert_eq!(