feat(app): staleness module + post-process search hits (fb-32)

compute_stale: strict > boundary, threshold=0 disables, future
timestamps treated as fresh (clock skew safety). App::search
re-stamps on cache hit so config threshold changes take effect
without flushing the cache.

Also unblocks the workspace build by plugging placeholder
indexed_at/stale into the two AnswerCitation construction
sites in kebab-rag/pipeline.rs (the score-gate refusal path
forwards from SearchHit; the LLM-citation path uses
UNIX_EPOCH/false until Task 7 wires the real values through
pack_context).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
th-kim0823
2026-05-09 01:30:10 +09:00
parent 8faad2f407
commit dfef65f196
6 changed files with 224 additions and 6 deletions

View File

@@ -190,7 +190,21 @@ impl App {
corpus_revision = key.corpus_revision,
"search served from LRU cache"
);
return Ok(hits.clone());
// p9-fb-32: re-stamp staleness on every cache hit. The cache
// entry was stamped at insert time against an older `now`
// and an older threshold; if either has shifted (config
// reload, time passing) the cached `stale: false` may now
// be wrong. Re-stamping is cheap (per-hit comparison) and
// avoids invalidating the cache on threshold changes.
let mut hits = hits.clone();
drop(guard);
let now = time::OffsetDateTime::now_utc();
crate::staleness::mark_stale_in_place(
&mut hits,
now,
self.config.search.stale_threshold_days,
);
return Ok(hits);
}
// Drop the lock before the (potentially slow) retriever call
// so other in-flight searches can use the cache concurrently.
@@ -205,14 +219,14 @@ impl App {
/// Used by `--no-cache` CLI invocations and by `search` itself
/// on cache miss. Identical behavior to the pre-fb-19 `search`.
pub fn search_uncached(&self, query: SearchQuery) -> Result<Vec<SearchHit>> {
match query.mode {
let mut hits = match query.mode {
SearchMode::Lexical => {
let lex = LexicalRetriever::with_settings(
self.sqlite.clone(),
lexical_index_version(&self.config),
self.config.search.snippet_chars,
);
lex.search(&query)
lex.search(&query)?
}
SearchMode::Vector => {
let (emb, vec_store) = self.require_embeddings()?;
@@ -226,7 +240,7 @@ impl App {
vec_iv,
self.config.search.snippet_chars,
);
retr.search(&query)
retr.search(&query)?
}
SearchMode::Hybrid => {
let lex = Arc::new(LexicalRetriever::with_settings(
@@ -246,9 +260,18 @@ impl App {
self.config.search.snippet_chars,
)) as Arc<dyn Retriever>;
let hybrid = HybridRetriever::new(&self.config, lex, vec_retr);
hybrid.search(&query)
hybrid.search(&query)?
}
}
};
// p9-fb-32: stamp staleness against the freshest possible `now`
// and the current threshold. Cheap (per-hit comparison).
let now = time::OffsetDateTime::now_utc();
crate::staleness::mark_stale_in_place(
&mut hits,
now,
self.config.search.stale_threshold_days,
);
Ok(hits)
}
/// Run a RAG `ask` against the configured retriever + LLM. Reuses