mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Add rollout-backed thread content search (#23519)
## Summary - add experimental `thread/search` for local rollout-backed thread search using `rg` over JSONL rollouts - return search-specific result rows with optional previews instead of storing preview data on `StoredThread` or ordinary `Thread` responses - keep `thread/list` separate from full-content search and document the new app-server surface ## Testing - `cargo test -p codex-app-server-protocol` - `cargo test -p codex-app-server thread_search_returns_content_and_title_matches -- --nocapture`
This commit is contained in:
@@ -88,6 +88,7 @@ use codex_app_server_protocol::ThreadRealtimeStartParams;
|
||||
use codex_app_server_protocol::ThreadRealtimeStopParams;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
use codex_app_server_protocol::ThreadRollbackParams;
|
||||
use codex_app_server_protocol::ThreadSearchParams;
|
||||
use codex_app_server_protocol::ThreadSetNameParams;
|
||||
use codex_app_server_protocol::ThreadSettingsUpdateParams;
|
||||
use codex_app_server_protocol::ThreadShellCommandParams;
|
||||
@@ -508,6 +509,15 @@ impl McpProcess {
|
||||
self.send_request("thread/list", params).await
|
||||
}
|
||||
|
||||
/// Send a `thread/search` JSON-RPC request.
|
||||
pub async fn send_thread_search_request(
|
||||
&mut self,
|
||||
params: ThreadSearchParams,
|
||||
) -> anyhow::Result<i64> {
|
||||
let params = Some(serde_json::to_value(params)?);
|
||||
self.send_request("thread/search", params).await
|
||||
}
|
||||
|
||||
/// Send a `thread/loaded/list` JSON-RPC request.
|
||||
pub async fn send_thread_loaded_list_request(
|
||||
&mut self,
|
||||
|
||||
@@ -17,6 +17,7 @@ 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::ThreadSearchResponse;
|
||||
use codex_app_server_protocol::ThreadSortKey;
|
||||
use codex_app_server_protocol::ThreadSourceKind;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
@@ -660,6 +661,161 @@ sqlite = true
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_search_returns_content_matches() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_minimal_config(codex_home.path())?;
|
||||
|
||||
let older_match = create_fake_rollout(
|
||||
codex_home.path(),
|
||||
"2025-01-02T10-00-00",
|
||||
"2025-01-02T10:00:00Z",
|
||||
"match: needle",
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
)?;
|
||||
let _non_match = create_fake_rollout(
|
||||
codex_home.path(),
|
||||
"2025-01-02T11-00-00",
|
||||
"2025-01-02T11:00:00Z",
|
||||
"no hit here",
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
)?;
|
||||
let newer_match = create_fake_rollout(
|
||||
codex_home.path(),
|
||||
"2025-01-02T12-00-00",
|
||||
"2025-01-02T12:00:00Z",
|
||||
"needle suffix",
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
)?;
|
||||
|
||||
let mut mcp = init_mcp(codex_home.path()).await?;
|
||||
let request_id = mcp
|
||||
.send_thread_search_request(codex_app_server_protocol::ThreadSearchParams {
|
||||
cursor: None,
|
||||
limit: Some(10),
|
||||
sort_key: None,
|
||||
sort_direction: None,
|
||||
source_kinds: None,
|
||||
archived: None,
|
||||
search_term: "needle".to_string(),
|
||||
})
|
||||
.await?;
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadSearchResponse {
|
||||
data, next_cursor, ..
|
||||
} = to_response::<ThreadSearchResponse>(resp)?;
|
||||
|
||||
assert_eq!(next_cursor, None);
|
||||
let ids: Vec<_> = data
|
||||
.iter()
|
||||
.map(|result| result.thread.id.as_str())
|
||||
.collect();
|
||||
assert_eq!(ids, vec![newer_match, older_match]);
|
||||
assert_eq!(data[0].snippet, "needle suffix");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_search_matches_json_escaped_content() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_minimal_config(codex_home.path())?;
|
||||
|
||||
let search_term = r#"quoted "needle" \ path"#;
|
||||
let thread_id = create_fake_rollout(
|
||||
codex_home.path(),
|
||||
"2025-01-02T10-00-00",
|
||||
"2025-01-02T10:00:00Z",
|
||||
search_term,
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
)?;
|
||||
|
||||
let mut mcp = init_mcp(codex_home.path()).await?;
|
||||
let request_id = mcp
|
||||
.send_thread_search_request(codex_app_server_protocol::ThreadSearchParams {
|
||||
cursor: None,
|
||||
limit: Some(10),
|
||||
sort_key: None,
|
||||
sort_direction: None,
|
||||
source_kinds: None,
|
||||
archived: None,
|
||||
search_term: search_term.to_string(),
|
||||
})
|
||||
.await?;
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadSearchResponse { data, .. } = to_response::<ThreadSearchResponse>(resp)?;
|
||||
|
||||
assert_eq!(data.len(), 1);
|
||||
assert_eq!(data[0].thread.id, thread_id);
|
||||
assert_eq!(data[0].snippet, search_term);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_search_filters_by_source_kind() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_minimal_config(codex_home.path())?;
|
||||
|
||||
let cli_id = create_fake_rollout(
|
||||
codex_home.path(),
|
||||
"2025-02-01T10-00-00",
|
||||
"2025-02-01T10:00:00Z",
|
||||
"shared needle",
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
)?;
|
||||
let exec_id = create_fake_rollout_with_source(
|
||||
codex_home.path(),
|
||||
"2025-02-01T11-00-00",
|
||||
"2025-02-01T11:00:00Z",
|
||||
"shared needle",
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
CoreSessionSource::Exec,
|
||||
)?;
|
||||
|
||||
let mut mcp = init_mcp(codex_home.path()).await?;
|
||||
let request_id = mcp
|
||||
.send_thread_search_request(codex_app_server_protocol::ThreadSearchParams {
|
||||
cursor: None,
|
||||
limit: Some(10),
|
||||
sort_key: None,
|
||||
sort_direction: None,
|
||||
source_kinds: Some(vec![ThreadSourceKind::Exec]),
|
||||
archived: None,
|
||||
search_term: "needle".to_string(),
|
||||
})
|
||||
.await?;
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadSearchResponse { data, .. } = to_response::<ThreadSearchResponse>(resp)?;
|
||||
|
||||
let ids: Vec<_> = data
|
||||
.iter()
|
||||
.map(|result| result.thread.id.as_str())
|
||||
.collect();
|
||||
assert_eq!(ids, vec![exec_id.as_str()]);
|
||||
assert_ne!(cli_id, exec_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_list_state_db_only_returns_sqlite_without_jsonl_repair() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
Reference in New Issue
Block a user