#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HistoryEntry {
pub turn_id: String,
pub position: u64,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HistoryHit {
pub turn_id: String,
pub position: u64,
pub snippet: String,
}
#[must_use]
pub fn has_searchable_terms(query: &str) -> bool {
!terms_of(query).is_empty()
}
#[must_use]
pub fn search(entries: &[HistoryEntry], query: &str, limit: usize) -> Vec<HistoryHit> {
let terms = distinct_terms_of(query);
if terms.is_empty() {
return Vec::new();
}
let use_substring = terms.iter().any(|t| !t.is_ascii());
let mut best: std::collections::HashMap<&str, (usize, &HistoryEntry)> =
std::collections::HashMap::new();
for e in entries {
let entry_terms = terms_of(&e.text);
let lowered = use_substring.then(|| e.text.to_lowercase());
let overlap = terms
.iter()
.filter(|t| {
entry_terms.contains(t)
|| (!t.is_ascii() && lowered.as_deref().is_some_and(|l| l.contains(t.as_str())))
})
.count();
if overlap == 0 {
continue;
}
match best.entry(e.turn_id.as_str()) {
std::collections::hash_map::Entry::Occupied(mut slot) => {
let (score, prev) = *slot.get();
if (overlap, e.position) > (score, prev.position) {
slot.insert((overlap, e));
}
}
std::collections::hash_map::Entry::Vacant(slot) => {
slot.insert((overlap, e));
}
}
}
let mut scored: Vec<(usize, &HistoryEntry)> = best.into_values().collect();
scored.sort_by(|(a_score, a), (b_score, b)| {
b_score
.cmp(a_score)
.then_with(|| b.position.cmp(&a.position))
});
scored
.into_iter()
.take(limit)
.map(|(_, e)| HistoryHit {
turn_id: e.turn_id.clone(),
position: e.position,
snippet: snippet_of(&e.text, &terms),
})
.collect()
}
const MAX_SNIPPET_CHARS: usize = 200;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TurnText {
pub turn_id: String,
pub text: String,
pub truncated: bool,
}
pub const MAX_PEEK_CHARS: usize = 8_000;
#[must_use]
pub fn peek(entries: &[HistoryEntry], turn_id: &str) -> Option<TurnText> {
let joined = entries
.iter()
.filter(|e| e.turn_id == turn_id)
.map(|e| e.text.as_str())
.collect::<Vec<_>>();
if joined.is_empty() {
return None;
}
let full = joined.join("\n");
let (text, truncated) = middle_elide(&full, MAX_PEEK_CHARS);
Some(TurnText {
turn_id: turn_id.to_owned(),
text,
truncated,
})
}
pub const MAX_PATTERN_CHARS: usize = 512;
const REGEX_SIZE_LIMIT: usize = 1 << 18;
#[derive(Debug, thiserror::Error)]
pub enum GrepError {
#[error("pattern is empty")]
EmptyPattern,
#[error("pattern is longer than {MAX_PATTERN_CHARS} characters")]
PatternTooLong,
#[error("pattern does not compile: {0}")]
InvalidPattern(String),
}
pub fn grep(
entries: &[HistoryEntry],
pattern: &str,
limit: usize,
) -> Result<Vec<HistoryHit>, GrepError> {
if pattern.is_empty() {
return Err(GrepError::EmptyPattern);
}
if pattern.chars().count() > MAX_PATTERN_CHARS {
return Err(GrepError::PatternTooLong);
}
let re = regex::RegexBuilder::new(pattern)
.case_insensitive(true)
.size_limit(REGEX_SIZE_LIMIT)
.build()
.map_err(|e| GrepError::InvalidPattern(e.to_string()))?;
let mut best: std::collections::HashMap<&str, (&HistoryEntry, usize)> =
std::collections::HashMap::new();
for e in entries {
let Some(m) = re.find(&e.text) else {
continue;
};
match best.entry(e.turn_id.as_str()) {
std::collections::hash_map::Entry::Occupied(mut slot) => {
if e.position > slot.get().0.position {
slot.insert((e, m.start()));
}
}
std::collections::hash_map::Entry::Vacant(slot) => {
slot.insert((e, m.start()));
}
}
}
let mut hits: Vec<(&HistoryEntry, usize)> = best.into_values().collect();
hits.sort_by_key(|(e, _)| std::cmp::Reverse(e.position));
Ok(hits
.into_iter()
.take(limit)
.map(|(e, match_start)| HistoryHit {
turn_id: e.turn_id.clone(),
position: e.position,
snippet: snippet_around_byte(&e.text, match_start),
})
.collect())
}
const ELISION_MARKER: &str = "\n…[middle elided]…\n";
fn middle_elide(text: &str, max_chars: usize) -> (String, bool) {
let chars: Vec<char> = text.chars().collect();
if chars.len() <= max_chars {
return (text.to_owned(), false);
}
let marker_len = ELISION_MARKER.chars().count();
if max_chars <= marker_len {
return (chars[..max_chars].iter().collect(), true);
}
let budget = max_chars - marker_len;
let head = budget / 2;
let tail = budget - head;
let head_text: String = chars[..head].iter().collect();
let tail_text: String = chars[chars.len() - tail..].iter().collect();
(format!("{head_text}{ELISION_MARKER}{tail_text}"), true)
}
fn snippet_of(text: &str, terms: &[String]) -> String {
let chars: Vec<char> = text.chars().collect();
if chars.len() <= MAX_SNIPPET_CHARS {
return text.to_owned();
}
let match_char = first_token_match_char(&chars, terms)
.or_else(|| first_substring_match_char(&chars, terms))
.unwrap_or(0);
window_around(&chars, match_char)
}
fn snippet_around_byte(text: &str, match_start: usize) -> String {
let chars: Vec<char> = text.chars().collect();
if chars.len() <= MAX_SNIPPET_CHARS {
return text.to_owned();
}
let match_char = text[..match_start].chars().count();
window_around(&chars, match_char)
}
fn window_around(chars: &[char], match_char: usize) -> String {
let half = MAX_SNIPPET_CHARS / 2;
let end = (match_char + half).min(chars.len());
let start = end.saturating_sub(MAX_SNIPPET_CHARS);
chars[start..end].iter().collect()
}
fn first_token_match_char(chars: &[char], terms: &[String]) -> Option<usize> {
let mut i = 0;
while i < chars.len() {
if !chars[i].is_alphanumeric() {
i += 1;
continue;
}
let start = i;
let mut token = String::new();
while i < chars.len() && chars[i].is_alphanumeric() {
token.extend(chars[i].to_lowercase());
i += 1;
}
if terms.contains(&token) {
return Some(start);
}
}
None
}
fn first_substring_match_char(chars: &[char], terms: &[String]) -> Option<usize> {
let terms: Vec<Vec<char>> = terms
.iter()
.filter(|t| !t.is_ascii())
.map(|t| t.chars().collect())
.collect();
if terms.is_empty() {
return None;
}
(0..chars.len()).find(|&start| {
terms.iter().any(|term| {
chars[start..]
.iter()
.flat_map(|c| c.to_lowercase())
.take(term.len())
.eq(term.iter().copied())
})
})
}
fn terms_of(s: &str) -> Vec<String> {
s.split(|c: char| !c.is_alphanumeric())
.filter(|t| !t.is_empty())
.map(str::to_lowercase)
.collect()
}
#[must_use]
pub fn distinct_terms_of(query: &str) -> Vec<String> {
let mut terms = terms_of(query);
terms.sort_unstable();
terms.dedup();
terms
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
fn entry(turn_id: &str, position: u64, text: &str) -> HistoryEntry {
HistoryEntry {
turn_id: turn_id.to_owned(),
position,
text: text.to_owned(),
}
}
#[test]
fn search_returns_only_entries_sharing_a_query_term() {
let entries = vec![
entry("t1", 1, "we decided to use BM25 ranking for history"),
entry("t2", 2, "lunch plans for friday afternoon"),
];
let hits = search(&entries, "BM25", 10);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].turn_id, "t1");
assert_eq!(hits[0].position, 1);
}
#[test]
fn search_ranks_more_query_term_overlap_first() {
let entries = vec![
entry("t1", 1, "the deploy pipeline runs on cloud build"),
entry(
"t2",
2,
"the deploy pipeline and the release pipeline both matter",
),
];
let hits = search(&entries, "deploy pipeline", 10);
assert_eq!(hits.len(), 2);
assert_eq!(hits[0].turn_id, "t2", "more overlap ranks first");
assert_eq!(hits[1].turn_id, "t1");
}
#[test]
fn snippet_is_bounded_and_contains_the_match() {
let filler = "padding ".repeat(200); let text = format!("{filler} the keyword quantum appears here {filler}");
let hits = search(&[entry("t1", 1, &text)], "quantum", 10);
assert_eq!(hits.len(), 1);
assert!(
hits[0].snippet.len() <= MAX_SNIPPET_CHARS,
"snippet {} chars exceeds cap",
hits[0].snippet.len()
);
assert!(
hits[0].snippet.to_lowercase().contains("quantum"),
"snippet must show the match: {:?}",
hits[0].snippet
);
}
#[test]
fn snippet_centers_on_whole_token_not_substring() {
let head = "concatenation ".repeat(30); let tail = "padding ".repeat(30);
let text = format!("{head}and then a cat sat over there {tail}");
let hits = search(&[entry("t1", 1, &text)], "cat", 10);
assert_eq!(hits.len(), 1);
assert!(
hits[0].snippet.contains(" cat ")
|| terms_of(&hits[0].snippet).contains(&"cat".to_owned()),
"snippet must contain the whole-token match, not just the \
'concatenation' region: {:?}",
hits[0].snippet
);
}
#[test]
fn search_returns_one_hit_per_turn() {
let entries = vec![
entry("old", 1, "the deploy decision: ship behind a flag"),
entry("noisy", 2, "kicking off the deploy now"),
entry("noisy", 3, "deploy is in progress"),
entry("noisy", 4, "deploy went fine"),
];
let hits = search(&entries, "deploy", 2);
let turn_ids: Vec<&str> = hits.iter().map(|h| h.turn_id.as_str()).collect();
assert_eq!(hits.len(), 2);
assert!(
turn_ids.contains(&"old"),
"old turn crowded out: {turn_ids:?}"
);
assert!(turn_ids.contains(&"noisy"), "{turn_ids:?}");
}
#[test]
fn repeated_query_words_do_not_inflate_rank() {
let entries = vec![
entry("stopword", 1, "the the the"),
entry("real", 2, "we agreed on friday"),
];
let hits = search(&entries, "the plan the agreed", 10);
assert_eq!(hits[0].turn_id, "real", "{hits:?}");
}
#[test]
fn search_matches_unsegmented_scripts_by_substring() {
let entries = vec![
entry("t1", 1, "我们决定了部署计划"),
entry("t2", 2, "lunch plans for friday"),
];
let hits = search(&entries, "部署计划", 10);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].turn_id, "t1");
}
#[test]
fn ascii_terms_never_match_as_substrings() {
let entries = vec![entry("t1", 1, "string concatenation details")];
assert!(search(&entries, "cat", 10).is_empty());
}
#[test]
fn snippet_centers_on_substring_match_for_unsegmented_scripts() {
let head = "padding ".repeat(40); let text = format!("{head}我们决定了部署计划就这样");
let hits = search(&[entry("t1", 1, &text)], "部署计划", 10);
assert_eq!(hits.len(), 1);
assert!(
hits[0].snippet.contains("部署计划"),
"snippet must contain the substring match: {:?}",
hits[0].snippet
);
}
#[test]
fn has_searchable_terms_rejects_symbol_only_queries() {
assert!(!has_searchable_terms("?!… → ---"));
assert!(!has_searchable_terms(" "));
assert!(has_searchable_terms("deploy plan"));
assert!(has_searchable_terms("部署计划"));
}
#[test]
fn snippet_is_unicode_safe_when_case_folding_grows_char_count() {
let prefix = "İ".repeat(20);
let tail = "padding ".repeat(40); let text = format!("{prefix} the marker quantum here {tail}");
let hits = search(&[entry("t1", 1, &text)], "quantum", 10);
assert_eq!(hits.len(), 1);
assert!(
hits[0].snippet.to_lowercase().contains("quantum"),
"unicode snippet must contain the match: {:?}",
hits[0].snippet
);
}
#[test]
fn grep_matches_by_pattern_and_returns_the_turn() {
let entries = vec![
entry("t1", 1, "the incident id was INC-4521 that night"),
entry("t2", 2, "lunch plans for friday afternoon"),
];
let hits = grep(&entries, r"INC-\d+", 10).expect("valid pattern");
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].turn_id, "t1");
assert_eq!(hits[0].position, 1);
assert!(
hits[0].snippet.contains("INC-4521"),
"{:?}",
hits[0].snippet
);
}
#[test]
fn grep_is_case_insensitive_unless_the_pattern_opts_out() {
let entries = vec![entry("t1", 1, "we shipped the Deploy Plan")];
assert_eq!(grep(&entries, "deploy plan", 10).unwrap().len(), 1);
assert!(
grep(&entries, "(?-i)deploy plan", 10).unwrap().is_empty(),
"an inline (?-i) restores case sensitivity"
);
}
#[test]
fn grep_returns_one_hit_per_turn_newest_first() {
let entries = vec![
entry("old", 1, "deploy the flag"),
entry("noisy", 2, "deploy one"),
entry("noisy", 3, "deploy two"),
entry("new", 4, "deploy again"),
];
let hits = grep(&entries, "deploy", 10).unwrap();
let ids: Vec<&str> = hits.iter().map(|h| h.turn_id.as_str()).collect();
assert_eq!(ids, vec!["new", "noisy", "old"], "newest turn first");
assert_eq!(
hits[1].position, 3,
"a turn surfaces once, via its newest matching entry"
);
}
#[test]
fn grep_limit_caps_the_hits() {
let entries = vec![
entry("t1", 1, "deploy a"),
entry("t2", 2, "deploy b"),
entry("t3", 3, "deploy c"),
];
let hits = grep(&entries, "deploy", 2).unwrap();
assert_eq!(hits.len(), 2);
assert_eq!(hits[0].turn_id, "t3", "the newest survive the cap");
}
#[test]
fn grep_snippet_is_bounded_and_contains_the_match() {
let filler = "padding ".repeat(200); let text = format!("{filler}the marker INC-99 appears here {filler}");
let hits = grep(&[entry("t1", 1, &text)], r"INC-\d+", 10).unwrap();
assert_eq!(hits.len(), 1);
assert!(
hits[0].snippet.chars().count() <= MAX_SNIPPET_CHARS,
"snippet {} chars exceeds cap",
hits[0].snippet.chars().count()
);
assert!(
hits[0].snippet.contains("INC-99"),
"snippet must show the match: {:?}",
hits[0].snippet
);
}
#[test]
fn grep_is_unicode_safe_when_windowing() {
let head = "🚀".repeat(500); let text = format!("{head} 部署计划 done");
let hits = grep(&[entry("t1", 1, &text)], "部署计划", 10).unwrap();
assert_eq!(hits.len(), 1);
assert!(
hits[0].snippet.contains("部署计划"),
"snippet must contain the match: {:?}",
hits[0].snippet
);
}
#[test]
fn grep_rejects_an_empty_pattern() {
let entries = vec![entry("t1", 1, "anything")];
assert!(matches!(
grep(&entries, "", 10),
Err(GrepError::EmptyPattern)
));
}
#[test]
fn grep_rejects_an_oversized_pattern() {
let pattern = "a".repeat(MAX_PATTERN_CHARS + 1);
assert!(matches!(
grep(&[], &pattern, 10),
Err(GrepError::PatternTooLong)
));
}
#[test]
fn grep_rejects_a_pattern_that_does_not_compile() {
let err = grep(&[], "[unclosed", 10).unwrap_err();
assert!(
matches!(&err, GrepError::InvalidPattern(msg) if !msg.is_empty()),
"{err:?}"
);
}
#[test]
fn grep_rejects_a_pattern_whose_program_would_balloon() {
let err = grep(&[], "(?:a{1000}){1000}", 10).unwrap_err();
assert!(matches!(err, GrepError::InvalidPattern(_)), "{err:?}");
}
#[test]
fn peek_joins_a_turns_entries_in_order() {
let entries = vec![
entry("t1", 1, "the user asked about deploys"),
entry("t1", 2, "the assistant explained the pipeline"),
entry("t2", 3, "an unrelated later turn"),
];
let peeked = peek(&entries, "t1").expect("t1 present");
assert_eq!(peeked.turn_id, "t1");
assert!(!peeked.truncated);
assert_eq!(
peeked.text,
"the user asked about deploys\nthe assistant explained the pipeline"
);
}
#[test]
fn peek_of_an_unknown_turn_is_none_not_empty() {
let entries = vec![entry("t1", 1, "only turn")];
assert!(
peek(&entries, "does-not-exist").is_none(),
"a peek at a turn that isn't in history must fail loud, not read as empty"
);
}
#[test]
fn peek_middle_elides_an_oversized_turn_keeping_head_and_tail() {
let head = "HEAD ".repeat(1_000); let tail = "TAIL ".repeat(1_000);
let text = format!("{head}MIDDLE-SECRET{tail}");
let peeked = peek(&[entry("t1", 1, &text)], "t1").expect("t1");
assert!(peeked.truncated, "an oversized turn is elided");
assert!(peeked.text.chars().count() <= MAX_PEEK_CHARS);
assert!(peeked.text.starts_with("HEAD "), "head kept");
assert!(peeked.text.trim_end().ends_with("TAIL"), "tail kept");
assert!(peeked.text.contains("elided"), "the cut is marked");
}
#[test]
fn middle_elide_honors_a_cap_smaller_than_the_marker() {
let (out, truncated) = middle_elide("abcdefghijklmnop", 4);
assert!(truncated);
assert_eq!(out.chars().count(), 4);
assert_eq!(out, "abcd");
}
#[test]
fn peek_is_unicode_safe_when_eliding() {
let text = "🚀".repeat(MAX_PEEK_CHARS + 500);
let peeked = peek(&[entry("t1", 1, &text)], "t1").expect("t1");
assert!(peeked.truncated);
assert!(peeked.text.chars().count() <= MAX_PEEK_CHARS);
}
}