Skip to content

Commit 988e8ba

Browse files
feat(sessions): add date filters to recall search
1 parent 6844e1b commit 988e8ba

12 files changed

Lines changed: 584 additions & 58 deletions

File tree

src/agents/hermes/templates/plugin_init.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,19 @@ def _resolve_auxiliary_client(agent=None):
156156
},
157157
"time_from": {
158158
"anyOf": [{"type": "number"}, {"type": "string"}],
159-
"description": "Optional inclusive minimum raw-message timestamp.",
159+
"description": "Optional inclusive minimum raw-message timestamp. Accepts Unix seconds, RFC3339, YYYY-MM-DD, or relative time like 'last hour'.",
160160
},
161161
"time_to": {
162162
"anyOf": [{"type": "number"}, {"type": "string"}],
163-
"description": "Optional inclusive maximum raw-message timestamp.",
163+
"description": "Optional inclusive maximum raw-message timestamp. Accepts Unix seconds, RFC3339, YYYY-MM-DD, or relative time like 'last hour'.",
164+
},
165+
"since": {
166+
"anyOf": [{"type": "number"}, {"type": "string"}],
167+
"description": "Alias for time_from.",
168+
},
169+
"until": {
170+
"anyOf": [{"type": "number"}, {"type": "string"}],
171+
"description": "Alias for time_to.",
164172
},
165173
},
166174
"required": ["query"],

src/cli.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,12 @@ pub enum SessionsAction {
624624
/// Maximum number of matches
625625
#[arg(long, default_value_t = 10)]
626626
limit: usize,
627+
/// Inclusive minimum message timestamp. Accepts Unix seconds, RFC3339, YYYY-MM-DD, or relative time like "last hour"
628+
#[arg(long, alias = "time-from", alias = "start-time")]
629+
since: Option<String>,
630+
/// Inclusive maximum message timestamp. Accepts Unix seconds, RFC3339, YYYY-MM-DD, or relative time like "last hour"
631+
#[arg(long, alias = "time-to", alias = "end-time")]
632+
until: Option<String>,
627633
/// Registered project id whose session store should be searched
628634
#[arg(long)]
629635
project_id: Option<String>,

src/cli/parse_tests.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1550,13 +1550,38 @@ fn parses_sessions_ingest_and_search_commands() {
15501550
limit,
15511551
project_id,
15521552
project_path,
1553+
since,
1554+
until,
15531555
},
15541556
}) => {
15551557
assert_eq!(query, "needle");
15561558
assert_eq!(provider.as_deref(), Some("codex"));
15571559
assert_eq!(limit, 5);
15581560
assert!(project_id.is_none());
15591561
assert!(project_path.is_none());
1562+
assert!(since.is_none());
1563+
assert!(until.is_none());
1564+
}
1565+
_ => panic!("expected sessions search command"),
1566+
}
1567+
1568+
let time_filtered_search = Cli::try_parse_from([
1569+
"tracedecay",
1570+
"sessions",
1571+
"search",
1572+
"needle",
1573+
"--since",
1574+
"last hour",
1575+
"--until",
1576+
"2026-07-04T00:00:00Z",
1577+
])
1578+
.unwrap();
1579+
match time_filtered_search.command {
1580+
Some(Commands::Sessions {
1581+
action: SessionsAction::Search { since, until, .. },
1582+
}) => {
1583+
assert_eq!(since.as_deref(), Some("last hour"));
1584+
assert_eq!(until.as_deref(), Some("2026-07-04T00:00:00Z"));
15601585
}
15611586
_ => panic!("expected sessions search command"),
15621587
}

src/global_db.rs

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use crate::sessions::{
1616
LcmSourceRef, LcmSummaryNode, LcmSummaryNodeDraft, LcmSummaryRequest,
1717
LcmSummarySourceMessage, LcmSummarySourceRange,
1818
},
19-
SessionMessageRecord, SessionMessageSearchResult, SessionRecord, SessionSearchScope,
19+
SessionMessageRecord, SessionMessageSearchResult, SessionRecord, SessionSearchFilters,
2020
};
2121

2222
const UNIX_TIMESTAMP_MILLIS_THRESHOLD: i64 = 1_000_000_000_000;
@@ -3178,8 +3178,7 @@ impl GlobalDb {
31783178
project_key,
31793179
query,
31803180
limit,
3181-
SessionSearchScope::All,
3182-
None,
3181+
SessionSearchFilters::default(),
31833182
)
31843183
.await
31853184
}
@@ -3191,16 +3190,14 @@ impl GlobalDb {
31913190
project_key: Option<&str>,
31923191
query: &str,
31933192
limit: usize,
3194-
scope: SessionSearchScope,
3195-
parent_session_id: Option<&str>,
3193+
filters: SessionSearchFilters<'_>,
31963194
) -> Vec<SessionMessageSearchResult> {
31973195
self.search_session_messages_filtered_inner(
31983196
Some(provider),
31993197
project_key,
32003198
query,
32013199
limit,
3202-
scope,
3203-
parent_session_id,
3200+
filters,
32043201
)
32053202
.await
32063203
}
@@ -3211,18 +3208,10 @@ impl GlobalDb {
32113208
project_key: Option<&str>,
32123209
query: &str,
32133210
limit: usize,
3214-
scope: SessionSearchScope,
3215-
parent_session_id: Option<&str>,
3211+
filters: SessionSearchFilters<'_>,
32163212
) -> Vec<SessionMessageSearchResult> {
3217-
self.search_session_messages_filtered_inner(
3218-
None,
3219-
project_key,
3220-
query,
3221-
limit,
3222-
scope,
3223-
parent_session_id,
3224-
)
3225-
.await
3213+
self.search_session_messages_filtered_inner(None, project_key, query, limit, filters)
3214+
.await
32263215
}
32273216

32283217
async fn search_session_messages_filtered_inner(
@@ -3231,8 +3220,7 @@ impl GlobalDb {
32313220
project_key: Option<&str>,
32323221
query: &str,
32333222
limit: usize,
3234-
scope: SessionSearchScope,
3235-
parent_session_id: Option<&str>,
3223+
filters: SessionSearchFilters<'_>,
32363224
) -> Vec<SessionMessageSearchResult> {
32373225
let fts_query = session_fts_query(query);
32383226
if fts_query.is_empty() || limit == 0 {
@@ -3265,14 +3253,36 @@ impl GlobalDb {
32653253
query_params.push(Value::Text(project_key.to_string()));
32663254
let _ = write!(sql, " AND s.project_key = ?{}", query_params.len());
32673255
}
3268-
if let Some(parent_session_id) = parent_session_id {
3256+
if let Some(parent_session_id) = filters.parent_session_id {
32693257
query_params.push(Value::Text(parent_session_id.to_string()));
32703258
let _ = write!(sql, " AND s.parent_session_id = ?{}", query_params.len());
32713259
}
3272-
if matches!(scope, SessionSearchScope::ParentsOnly) {
3260+
if let Some(start_time) = filters.time_range.start_time {
3261+
query_params.push(Value::Integer(start_time));
3262+
let _ = write!(
3263+
sql,
3264+
" AND m.timestamp IS NOT NULL AND m.timestamp >= ?{}",
3265+
query_params.len()
3266+
);
3267+
}
3268+
if let Some(end_time) = filters.time_range.end_time {
3269+
query_params.push(Value::Integer(end_time));
3270+
let _ = write!(
3271+
sql,
3272+
" AND m.timestamp IS NOT NULL AND m.timestamp <= ?{}",
3273+
query_params.len()
3274+
);
3275+
}
3276+
if matches!(
3277+
filters.scope,
3278+
crate::sessions::SessionSearchScope::ParentsOnly
3279+
) {
32733280
sql.push_str(" AND s.is_subagent = 0");
32743281
}
3275-
if matches!(scope, SessionSearchScope::SubagentsOnly) {
3282+
if matches!(
3283+
filters.scope,
3284+
crate::sessions::SessionSearchScope::SubagentsOnly
3285+
) {
32763286
sql.push_str(" AND s.is_subagent = 1");
32773287
}
32783288
for term in &literal_terms {

src/mcp/tools/definitions.rs

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2351,6 +2351,34 @@ fn def_message_search() -> ToolDefinition {
23512351
"type": "string",
23522352
"description": "Optional parent session id filter. Primarily useful with scope=subagents_only."
23532353
},
2354+
"since": {
2355+
"oneOf": [
2356+
{ "type": "integer", "minimum": 0 },
2357+
{ "type": "string" }
2358+
],
2359+
"description": "Optional inclusive minimum message timestamp. Accepts Unix seconds, RFC3339, YYYY-MM-DD, or relative time like 'last hour'."
2360+
},
2361+
"until": {
2362+
"oneOf": [
2363+
{ "type": "integer", "minimum": 0 },
2364+
{ "type": "string" }
2365+
],
2366+
"description": "Optional inclusive maximum message timestamp. Accepts Unix seconds, RFC3339, YYYY-MM-DD, or relative time like 'last hour'."
2367+
},
2368+
"time_from": {
2369+
"oneOf": [
2370+
{ "type": "integer", "minimum": 0 },
2371+
{ "type": "string" }
2372+
],
2373+
"description": "Alias for since."
2374+
},
2375+
"time_to": {
2376+
"oneOf": [
2377+
{ "type": "integer", "minimum": 0 },
2378+
{ "type": "string" }
2379+
],
2380+
"description": "Alias for until."
2381+
},
23542382
"scope": {
23552383
"type": "string",
23562384
"description": "Relationship scope for search results (default: all).",
@@ -2616,14 +2644,28 @@ fn def_lcm_grep() -> ToolDefinition {
26162644
{ "type": "integer", "minimum": 0 },
26172645
{ "type": "string" }
26182646
],
2619-
"description": "Optional inclusive minimum raw-message timestamp. Integer strings and timezone-aware ISO/RFC3339 strings are accepted."
2647+
"description": "Optional inclusive minimum raw-message timestamp. Accepts Unix seconds, RFC3339, YYYY-MM-DD, or relative time like 'last hour'."
26202648
},
26212649
"end_time": {
26222650
"oneOf": [
26232651
{ "type": "integer", "minimum": 0 },
26242652
{ "type": "string" }
26252653
],
2626-
"description": "Optional inclusive maximum raw-message timestamp. Integer strings and timezone-aware ISO/RFC3339 strings are accepted."
2654+
"description": "Optional inclusive maximum raw-message timestamp. Accepts Unix seconds, RFC3339, YYYY-MM-DD, or relative time like 'last hour'."
2655+
},
2656+
"since": {
2657+
"oneOf": [
2658+
{ "type": "integer", "minimum": 0 },
2659+
{ "type": "string" }
2660+
],
2661+
"description": "Alias for start_time."
2662+
},
2663+
"until": {
2664+
"oneOf": [
2665+
{ "type": "integer", "minimum": 0 },
2666+
{ "type": "string" }
2667+
],
2668+
"description": "Alias for end_time."
26272669
},
26282670
"limit": {
26292671
"type": "integer",

0 commit comments

Comments
 (0)