From 31d4bfdde029214bfc49e9d50498c540074dc789 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Tue, 17 Feb 2026 14:19:52 +0000 Subject: [PATCH] feat: add `--search` to `just log` (#11995) Summary - extend the log client to accept an optional `--search` substring filter when querying codex-state logs - propagate the filter through `LogQuery` and apply it in `push_log_filters` via `INSTR(message, ...)` - add an integration test that exercises the new search filtering behavior Testing - Not run (not requested) --- codex-rs/state/src/bin/logs_client.rs | 7 ++++ codex-rs/state/src/model/log.rs | 1 + codex-rs/state/src/runtime.rs | 58 +++++++++++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/codex-rs/state/src/bin/logs_client.rs b/codex-rs/state/src/bin/logs_client.rs index 796a968cb..099b06164 100644 --- a/codex-rs/state/src/bin/logs_client.rs +++ b/codex-rs/state/src/bin/logs_client.rs @@ -46,6 +46,10 @@ struct Args { #[arg(long = "thread-id")] thread_id: Vec, + /// Substring match against the log message. + #[arg(long)] + search: Option, + /// Include logs that do not have a thread id. #[arg(long)] threadless: bool, @@ -67,6 +71,7 @@ struct LogFilter { module_like: Vec, file_like: Vec, thread_ids: Vec, + search: Option, include_threadless: bool, } @@ -154,6 +159,7 @@ fn build_filter(args: &Args) -> anyhow::Result { module_like, file_like, thread_ids, + search: args.search.clone(), include_threadless: args.threadless, }) } @@ -233,6 +239,7 @@ fn to_log_query( module_like: filter.module_like.clone(), file_like: filter.file_like.clone(), thread_ids: filter.thread_ids.clone(), + search: filter.search.clone(), include_threadless: filter.include_threadless, after_id, limit, diff --git a/codex-rs/state/src/model/log.rs b/codex-rs/state/src/model/log.rs index 2aeaf2d5c..cf973bcee 100644 --- a/codex-rs/state/src/model/log.rs +++ b/codex-rs/state/src/model/log.rs @@ -37,6 +37,7 @@ pub struct LogQuery { pub module_like: Vec, pub file_like: Vec, pub thread_ids: Vec, + pub search: Option, pub include_threadless: bool, pub after_id: Option, pub limit: Option, diff --git a/codex-rs/state/src/runtime.rs b/codex-rs/state/src/runtime.rs index 3a743a9c7..f9beacdae 100644 --- a/codex-rs/state/src/runtime.rs +++ b/codex-rs/state/src/runtime.rs @@ -711,6 +711,11 @@ fn push_log_filters<'a>(builder: &mut QueryBuilder<'a, Sqlite>, query: &'a LogQu if let Some(after_id) = query.after_id { builder.push(" AND id > ").push_bind(after_id); } + if let Some(search) = query.search.as_ref() { + builder.push(" AND INSTR(message, "); + builder.push_bind(search.as_str()); + builder.push(") > 0"); + } } fn push_like_filters<'a>( @@ -906,6 +911,8 @@ mod tests { use super::StateRuntime; use super::ThreadMetadata; use super::state_db_filename; + use crate::LogEntry; + use crate::LogQuery; use crate::STATE_DB_FILENAME; use crate::STATE_DB_VERSION; use crate::model::Phase2JobClaimOutcome; @@ -2495,6 +2502,57 @@ VALUES (?, ?, ?, ?, ?) let _ = tokio::fs::remove_dir_all(codex_home).await; } + #[tokio::test] + async fn query_logs_with_search_matches_substring() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + runtime + .insert_logs(&[ + LogEntry { + ts: 1_700_000_001, + ts_nanos: 0, + level: "INFO".to_string(), + target: "cli".to_string(), + message: Some("alpha".to_string()), + thread_id: Some("thread-1".to_string()), + process_uuid: None, + file: Some("main.rs".to_string()), + line: Some(42), + module_path: None, + }, + LogEntry { + ts: 1_700_000_002, + ts_nanos: 0, + level: "INFO".to_string(), + target: "cli".to_string(), + message: Some("alphabet".to_string()), + thread_id: Some("thread-1".to_string()), + process_uuid: None, + file: Some("main.rs".to_string()), + line: Some(43), + module_path: None, + }, + ]) + .await + .expect("insert test logs"); + + let rows = runtime + .query_logs(&LogQuery { + search: Some("alphab".to_string()), + ..Default::default() + }) + .await + .expect("query matching logs"); + + assert_eq!(rows.len(), 1); + assert_eq!(rows[0].message.as_deref(), Some("alphabet")); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + fn test_thread_metadata( codex_home: &Path, thread_id: ThreadId,