feat(timeline): upgrade seed phase to hybrid search (FTS + vector RRF)

Replace the FTS-only seed phase with hybrid search that combines FTS5
full-text matching with vector similarity via Reciprocal Rank Fusion
(RRF), matching the approach already proven in the search command.

Key changes:
- seed_timeline is now async, accepting an optional OllamaClient for
  vector embedding. Gracefully falls back to FTS-only when Ollama is
  unavailable (same pattern as search_hybrid).
- SeedResult now includes search_mode ("hybrid", "lexical", or
  "lexical (hybrid fallback)") for provenance tracking in both human
  and robot output.
- run_timeline and handle_timeline are now async to propagate the
  seed_timeline future.
- Timeline result metadata includes the search mode used.
- Seed retrieval uses 3x oversampling (max_seeds * 3) then deduplicates
  to the requested entity count, improving recall for discussion-heavy
  entities.

Test updates:
- All seed tests updated for the new async + OllamaClient signature
  (client=None exercises the FTS fallback path).
- Pipeline integration tests updated similarly.
- timeline.rs gains #[derive(Debug)] on TimelineResult for test
  assertions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Taylor Eernisse
2026-02-13 12:37:48 -05:00
parent 0034958faf
commit 8dbabe7279
6 changed files with 192 additions and 84 deletions

View File

@@ -179,7 +179,9 @@ async fn main() {
Some(Commands::Search(args)) => {
handle_search(cli.config.as_deref(), args, robot_mode).await
}
Some(Commands::Timeline(args)) => handle_timeline(cli.config.as_deref(), args, robot_mode),
Some(Commands::Timeline(args)) => {
handle_timeline(cli.config.as_deref(), args, robot_mode).await
}
Some(Commands::Who(args)) => handle_who(cli.config.as_deref(), args, robot_mode),
Some(Commands::Drift {
entity_type,
@@ -1763,7 +1765,7 @@ async fn handle_stats(
Ok(())
}
fn handle_timeline(
async fn handle_timeline(
config_override: Option<&str>,
args: TimelineArgs,
robot_mode: bool,
@@ -1784,7 +1786,7 @@ fn handle_timeline(
max_evidence: args.max_evidence,
};
let result = run_timeline(&config, &params)?;
let result = run_timeline(&config, &params).await?;
if robot_mode {
print_timeline_json_with_meta(