Skip to content

Put -n on the ranker the list already uses - #52

Open
devcodes9 wants to merge 1 commit into
mainfrom
feat/n-ranked-sessions
Open

Put -n on the ranker the list already uses#52
devcodes9 wants to merge 1 commit into
mainfrom
feat/n-ranked-sessions

Conversation

@devcodes9

Copy link
Copy Markdown
Owner

The bug behind the bug

Step 2 started as "add a session id to `-n` and stop printing ANSI into a pipe". Both are real, but they are symptoms. -n and the interactive list were two different search paths:

list -n (before)
ranking rank_sessions: BM25 over title / first prompt / body, recency and resume-count boost rank_matches: all(t in line.lower()), sorted by date
stemming yes (migrationmigrate) no
typos subsequence tier for rare terms no
unit one row per session one row per message
SDK-spawned runs demoted below your own sessions not distinguished

Demonstrating it on my own corpus:

$ agsearch -n "heldout evluation"
No matches.

$ agsearch _filter 0 "heldout evluation"     # what the TUI runs
ef702a40…  2026-08-24  cc  1/2  agsearch  Agsearch held-out eval blind spot

The typo tolerance the README's demo GIF sells does not exist on -n.

This is not a corner. uvx agsearch -n "stripe tax id" is the README's quick start, the first command a new user runs. print_matches is also the fallback when fzf is missing. So the surface most people meet first was the unranked one, and every ranking improvement the eval measures never reached it.

A test pinned the old behaviour as intentional (test_n_mode_keeps_raw_substring_and), so this was a deliberate simplification once, not drift. It stopped being defensible when -n became the surface scripts and coding agents use.

Change

-n calls rank_sessions. rank_matches and fmt_row are deleted rather than left for the next caller to pick up by accident.

Each result is one session, led by its id, with the matching line indented under it:

ef702a40-7ed3-429c-92a4-959a19859545  2026-08-24  cc   2/2  agsearch   Agsearch held-out eval blind spot
    …65% of the held-out queries target auto sessions that can never rank 1…

The id leads because it is the only field another program needs. A hit you cannot open is a hit you cannot use, which is why the two additions below come with it:

  • agsearch read <session-id> prints a whole conversation without resuming it. The code was already there as the TUI's Ctrl-O, reachable only as an internal subcommand.
  • Colour is chosen at the edge, in one _emit seam. A terminal gets colour; a pipe, or NO_COLOR, gets text. That includes escape sequences quoted inside a transcript, and ones a snippet cut in half. The fzf paths pass color=True explicitly, because their pager is a pipe but their reader is human.

Cost

-n now builds the same per-session index the list does: ~0.7s per run on a 727-session corpus, up from ~0.25s. Accepted rather than optimised. The TUI pays the same cost at startup, it is a one-shot CLI call, and caching sessions.tsv against the index would be cache-invalidation work this change does not need.

Verification

Ranking is untouched, and the eval confirms it:

                         gold@1  gold-tol  goldMRR |   held@1  heldMRR |  sec/query
main                      0.733     0.800    0.795 |    0.490    0.618 |     0.0727
candidate                 0.733     0.800    0.795 |    0.490    0.618 |     0.0749

103 tests pass. Also checked by hand: _filter still emits its 4-field fzf rows, _transcript keeps colour through the pager, NO_COLOR is honoured on a tty, and read exits 1 on an unknown id.

New tests in tests/test_noninteractive_rows.py pin the contract: one entry per session however many messages match, the id leads the line, no ANSI in a pipe, a typo still finds the session.

Why now

Groundwork for #46. A skill built on the old -n would have inherited the weaker ranker and had no way to open what it found.

-n was a second search path: an AND of raw substrings over message rows, sorted by
date. No BM25, no stemming, no typo tier, no demotion of SDK-spawned runs, and one
session repeated once per matching message. `agsearch -n "heldout evluation"` returned
"No matches" while the same query put the right session first in the TUI, so the typo
tolerance the README demos did not exist on the surface it demos it from.

It matters more than an -n flag sounds like. `uvx agsearch -n "stripe tax id"` is the
README's zero-install first command, and print_matches is also what runs when fzf is
missing. The path most new users met was the unranked one, and every ranking change we
measure in the eval never reached it.

So -n calls rank_sessions now, and rank_matches/fmt_row are gone rather than left for
the next caller to pick up by accident. Output is one entry per session led by its
session id, with the matching line indented under it. The id is the point: a hit you
cannot open is a hit you cannot use.

Also here, because they are the same seam:

- `agsearch read <sid>` prints a whole conversation without resuming it. The code was
  already there as the TUI's ctrl-o, reachable only as an internal subcommand.
- Colour is chosen at the edge, in _emit. A terminal gets it; a pipe or NO_COLOR gets
  text, including for escapes quoted inside a transcript. The fzf paths pass color=True
  explicitly, since their pager is a pipe but their reader is human.

Ranking is untouched: gold 0.733/0.800/0.795 and held-out 0.490/0.618, identical before
and after. -n costs ~0.7s per run against 400 sessions, up from ~0.25s, because it now
builds the same per-session index the list does. 103 tests pass.
@devcodes9 devcodes9 closed this Aug 26, 2026
@devcodes9 devcodes9 reopened this Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant