History search: rank on history priors, not fuzzy score alone (APP-5650) - #15591
Draft
warp-agent-staging[bot] wants to merge 3 commits into
Draft
History search: rank on history priors, not fuzzy score alone (APP-5650)#15591warp-agent-staging[bot] wants to merge 3 commits into
warp-agent-staging[bot] wants to merge 3 commits into
Conversation
- Replace HistorySearchItem::score's raw SkimMatcherV2 score with a two-stage design: fuzzy match as a gate producing normalized match-quality features (exact/skim/consecutive/tightness), then a rank combining that quality with history priors (recency, frequency, session, cwd, exit status). - Whitespace-tokenize the query for fzf-style space-AND matching (warp#4174), add a consecutive-substring boost to fix boundary bias (warp#1810), and add a match-quality score floor to filter loose scattered matches. - Order via a packed (exact_line, match_band, final_score, age) sort key so history priors can only break ties within a match-quality tier, never let a fresh weak match outrank an older exact one (warp#6126, #3430, #5588, #6344). - Thread through the frequency count already computed by CommandHistorySummary via a new History::command_execution_count accessor, and add a list-position age fallback for history-file entries with no sqlite-backed timestamp. - Add golden fixture tests covering all three failure modes plus guard cases for exact-match priority and the score floor.
Contributor
Author
|
This PR was generated with Warp. Comment |
Contributor
Author
There was a problem hiding this comment.
Overview
Replaces Ctrl+R history ranking with a normalized match-quality gate plus history priors, keeping SkimMatcherV2. One open policy question needs a human decision; the other review findings are already back with the author.
Concerns
- The packed history sort key changes the score scale the shared mixer compares across sources, without isolating history from them. Command Search registers history alongside workflows, notebooks, env-var collections and AI results (
app/src/search/command_search/view.rs:222-331), all at defaultpriority_tier0, andSearchMixercompares rawscore()across sources (crates/warp_search_core/src/mixer.rs:459) — non-whole-line history now tops out near 13 and whole-line history starts near 100, while the untouched sources still return raw Skim scores in the tens to hundreds. Decide the intended policy (preserve history's former cross-source position, give history a distinct priority tier, or normalize every participating source), implement it, and cover it with a mixer test using genuinely heterogeneous result types, since the two updatedsearcher_tests.rsassertions use two history sources and so cannot validate cross-source interleaving.
Verdict
Checks: build pass, tests not independently verified, CI red (required jobs reported skipped; no code-test failure exposed), visual proof missing
Found: 0 critical, 1 important, 0 suggestions, 0 nits
Responding as wilson: Open session · View in factory
- rank(): bypass MATCH_SCORE_FLOOR for a blank query. SearchMixer intentionally invokes history with an empty query for the zero state (run_in_zero_state: true), where match quality is always 0, so the floor was dropping every candidate. Ranking now falls back to history priors for the zero state instead of an empty list. - current_cwd was being derived by picking the most recent history entry's pwd, but HistoryEntry::pwd is captured at command *start* (for_session_command), so it goes stale immediately after a cd until the next command runs. Thread the session's live-tracked working directory (TerminalView::current_working_directory, backed by ActiveSession) through view.rs -> CommandSearchView::reset_state -> the history data source instead. - Add end-to-end mixer tests for both: blank-query zero-state history presence, and the live-cwd prior winning over a stale last-entry pwd. - Add a rank()-level unit test for the blank-query floor bypass.
… floor rank() now returns a constant score for a blank query instead of computing one from priors, so the mixer's stable sort preserves History::commands_shared()'s chronological order (matching pre-ranking behavior, where Skim scored every candidate 0 for an empty pattern) instead of letting frequency/cwd/session/exit priors reorder the zero state. Replaced the single-entry rank() test with one asserting two candidates with wildly different priors score identically for a blank query, and replaced the single-entry mixer test with a two-entry regression test whose session-prior difference would otherwise reorder it. Verified both tests fail against the prior (floor-only-bypass) revision and pass against this one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Description
HistorySearchItem::scorereturned Skim's raw fuzzy-match score and nothing else, so Ctrl+Rresults were ranked purely on fuzzy alignment quality — recency, frequency, session and cwd
existed on
HistoryEntrybut were display-only. This produced three measured failure modes(warp#6126, #3430, #5588, #6344, #4174, #1810):
make *candidate scored identically regardless of recency, so afresh command could get buried behind older ones with the same shape of match.
like
cd hi ormmatched nothing even when every term was present in the command.match (
txjs-cli pushfor querytcp) outscore a tight, contiguous one (adb tcpip 5000) —the opposite of what real fzf does.
We kept
SkimMatcherV2(same Smith–Waterman family as fzf v2, so swapping matchers would be alateral move) and fixed the ranking instead.
The new score
Two stages: fuzzy match as a gate that produces normalized match-quality features, then a
rank that combines that quality with history priors.
exactis 1.0 for a whole-line match, 0.85 for any literal substring (anywhere — this is whatfixes failure mode 1, since
OS=linux make barnow gets the same exact-tier credit asmake bar), 0.55 when the fuzzy alignment starts at the very first character, else 0.skimis Skim's raw score, normalized by query length and soft-capped viax / (x + 30)so anunbounded, length-scaled Skim score can't swamp the priors.
consecutiveandtightnessare computed directly from the matched character indices(independent of Skim's internal bonuses), which is what fixes failure mode 3.
recencyisexp(-ln2 · age_days / 3);freqisln(1+count)/ln(21)clamped to 1.Query text is whitespace-tokenized and every term is matched as an independent fuzzy subsequence,
ANDed together (fzf-style), which fixes failure mode 2. A minimum match-quality floor filters out
matches that are technically non-empty under Skim's DP but are visual noise (a few scattered
characters in an unrelated command).
The zero state (blank query, shown before the user types) is a special case: it bypasses ranking
entirely and gives every candidate the same score, so the mixer's stable sort preserves
History::commands_shared()'s established chronological order — the same effect Skim's uniform0 score for an empty pattern had before this PR. Priors only apply once there's an actual query.
Ordering, not just weights
The actual sort key packs
(exact_line, match_band = floor(match/0.25), final_score, age)intoone
f64, withexact_line/match_bandgiven enough headroom (100.0 and 3.0 respectively) thatthey always dominate
final_score(bounded to ±1). This means an older whole-line exact matchalways outranks a fresher weak/scattered match — that guarantee is structural, not a side effect
of the weights, per the golden fixture
older_exact_match_outranks_fresher_weak_match.Ordering direction is unchanged: higher score is still better, consistent with the existing Skim
convention,
SearchMixer's ascending(priority_tier, score, source_order)sort, andSearchResultOrdering::BottomUp("the highest ranked result [is] ordered last", i.e. closest tothe input box). Neither
warp_search_core's sort norcrates/fuzzy_match(shared by every othersearch surface) were touched.
Data plumbing
CommandHistorySummary.count(app/src/terminal/history.rs) already counted executions but wasdiscarded when joining history-file commands to their sqlite metadata. Added a small
History::command_execution_count(session_id, command)accessor instead of wideningHistoryEntry(whoseEq/Hashare used elsewhere).HistoryEntry::command_only) have notimestamp. These fall back to a synthetic age based on how many newer candidates exist in the
chronologically-ordered list, reusing the same 3-day half-life, so they decay gracefully instead
of reading as infinitely old.
cwdprior uses the session's live-tracked working directory(
TerminalView::current_working_directory, backed byActiveSession), threaded throughworkspace/view.rs::show_command_search→CommandSearchView::reset_state→ the history datasource. It does not use the most recent history entry's
pwd: that field is captured when acommand starts (
HistoryEntry::for_session_command), so it's stale immediately after acduntil the next command runs.
What a user will notice
match the query equally well (previously ties were broken by data-source registration order).
cd hi ormnow match commands containing all three terms anywhere, notjust as one literal phrase.
tcpipfortcp) will rank above a scattered one spanning wordboundaries.
consistently rank at the very top, regardless of age.
Coordination with APP-5564 / APP-4542 (unbounded SkimMatcherV2 allocation)
Whitespace tokenization means a multi-word query now calls
SkimMatcherV2once per term percandidate instead of once, which multiplies (bounded by the number of terms a user types, typically
2-4) rather than fixes the allocation concern those tickets track. To avoid making it worse: each
term is matched independently against the same candidate list size (no new collection or
candidate-set growth), and a single-word query — the common case — is unaffected (exactly one
fuzzy_indicescall per candidate, same as before). The score floor also reduces the number ofitems that make it into
HistorySearchItem/rendering, but doesn't reduce the per-candidatematching cost itself; the underlying allocation fix is left to that other work.
Decisions not settled by the issue
position back in the chronological list as roughly one synthetic day of age. This is a
judgment call the issue didn't pin down a formula for.
SKIM_SOFT_CAP = 30.0andMATCH_SCORE_FLOOR = 0.12: chosen so the per-character rawSkim scores in the issue's concrete examples (~20-23) land mid-curve; validated against the
golden fixtures rather than a specific formula in the issue.
itself is proof of at least one execution.
compared across sources in the shared mixer (Command Search registers history alongside
workflows, notebooks, env-var collections and AI results, all at the default
priority_tier).This is an open policy question the orchestrator has put to the requester; not addressed in this
revision pending that decision.
Linked Issue
Linear: APP-5650
Origin: warp#6126, #4174, #3430, #5588, #6344, #1810
Testing
rank_tests.rs) against a fixed clock, covering allthree failure modes plus guard cases (older exact match beats a fresh weak match, frequency,
session, cwd, exit-status penalty, missing-timestamp fallback, the score floor, and the blank
query bypassing priors entirely rather than just the floor).
searcher_tests.rs) for: exact-match-over-substring ranking; blankquery preserving chronological zero-state order despite differing priors (verified this
actually fails against a floor-only-bypass revision, and passes against this one); and the live
cwd prior winning over a stale last-entry
pwd.cargo test -p warp --lib search::command_search(24 passed) andcargo test -p warp --lib terminal::history(20 passed)../script/formatandcargo clippy -p warp --all-targets --tests -- -D warnings(clean), plusthe workspace-wide
cargo clippy --workspace --exclude warp_completer --all-targets --tests -- -D warningsfromscript/presubmit(clean).cargo run --bin warp) fromthis branch, seeded real shell history, and confirmed both multi-word (space-AND) matching and
recency-based prior ranking on the running Ctrl+R panel. See recording below.
Recording
Multi-word matching (
hi ormmatching acd .../history_ormcommand) and recency-based ordering(
makequery ranking the freshest of three similar matches closest to the input):Ctrl+R history search demo (video)
Screenshot: multi-word match
Screenshot: recency-based ordering
I have manually tested my changes locally with
./script/runAgent Mode