Finding what I wrote before

2026-09-27work in progress

After reading about Jev, I wanted to know if small AI models running on my own hardware could do useful work on my Git forge. I kept settling questions there, forgetting I had, and asking them again weeks later, so the first test was whether a model could remind me. So far, a fifty-year-old way of counting words does it better than any of them.

The tool I ended up with works like this. I describe a new problem in one line, and it lists the old issues I should read first. Here the problem was that after a reboot, one of my servers sent its whole log history again.

$ issue-related "systemd-journal-upload cursor lost after reboot, journal replays"

  #956   0.14  hypervisor: no journal entries reached VictoriaLogs in 15m
  #968   0.10  checks: service-state cannot see a StateDirectory from a packaged unit

ranked by term overlap, then by links from the top hits; it misses about
a third of real links, so an absent issue here is not a clearance

Both were the right things to read, and I’d forgotten both. It finds the issue I actually needed in its top five about two times in three. Showing the five newest issues instead manages about one in four, and picking at random almost never.

That also means it cannot be read as a clearance. Checking it, seeing nothing, and concluding the question is new converts I didn’t look into I looked and it was clear, which is worse than never having looked. So it says so on every run.

Underneath is TF-IDF. It counts the words in every document, weights rare words above common ones, and calls two documents similar when they share unusual vocabulary. Two additions carry most of its strength. It reads the comments as well as the issue text, which are twice the volume and worth about seven points. And it lifts whatever its top three hits already link to.

How I graded it

Grading needed an answer key, and the forge already had one. Whenever I mention an old issue in a new one, I’m recording that the two are related. So the test hides that mention, gives the system only the new issue’s text, and lets it search only the issues that existed when the new one was written. If the issue I actually linked comes back in the first five, that counts as a hit.

The models I tried are all small enough to run on a laptop. Most turn each issue into a position in space, so that similar issues land near each other, and suggest the nearest ones. The chart shows each model on its own, and again as a second pass over what counting words found.

no model neural model baseline to beat
Counting words, then a second pass
+ links already there
Issues the top three hits already link to are moved up.
67.2%
65.1%
64.0%
Counting words
Counting words + commentsWith comments
Counting words over each issue plus all its comments.
63.8%
Stemmed
Counting words with comments, after cutting words to their stem so restart, restarts and restarted match (a light Porter stemmer).
63.8%
BM25 + comments, tunedBM25, tuned
A refined version of counting words that stops a repeated word from dominating and evens out long and short issues (BM25F). Titles weighted up and comments down.
62.8%
Query likelihood
Asks how likely each old issue's words are to have produced the new one's, comments included (a Dirichlet-smoothed language model).
62.5%
Identifiers kept wholeWhole identifiers
Counting words with comments, but names like checks/service-state.nix stay one word instead of four.
61.9%
Counting words
Scores the words two issues share, with rare words counting more than common ones (TF-IDF). Issue text only, no comments.
57.0%
Counting words + topicsWith topics
Counting words, blended with a model of which words tend to appear together (LSA).
56.3%
Character patterns
Counts short runs of letters instead of whole words, so partial matches count (character n-grams).
55.0%
BM25, untuned
A refined version of counting words, with its textbook settings (BM25, k1 = 1.2 and b = 0.75).
47.6%
Neural models on their own
56.7%
gte-small, by passage
Each issue and its comments split into passages, and the issue scored by its best one.
51.1%
46.0%
Controls
Five most recent
Always the five newest issues, which is what a plain issue list shows.
23.1%
Random
Five issues at random.
2.6%
How often the issue someone linked lands in the first five suggestions.

Counting words beat every model. The best one on its own, SPLADE, was seven points behind, and blending any of them into counting words added a point or two at most. What did help was structure rather than language. A decision, its follow-ups and the fix that later undid it tend to link to each other, so one good hit pulls in related issues that share few words with the new one.

The likeliest reason is what my issues are made of. They’re full of exact names, like StateDirectory, checks/service-state.nix and the names of my machines. Matching a rare string exactly is what counting words does best, and what a model trained on ordinary prose does worst.

checks: service-state cannot see a StateDirectory from a packaged unit
checks/service-state.nix cannot see a StateDirectory= that comes from a PACKAGED unit file, so the rowless-unit census added asks its question of fewer units than the host has.
in up to 20 issues 21 to 150 more than 150
Rarity is counted across the issues on this forge, so a plain word like "fewer" comes out rare.

Where a method had settings to tune, I tuned them on older links and checked them on newer ones, so the results aren’t just fitted to the answers. The answer key only counts links someone bothered to write, so a useful suggestion can still score as a miss, and the real numbers are probably a little higher. Most of those links were written by the AI coding agents I run, which search by keyword and might favour counting words, but the models lost on the links I wrote myself too.

The forge is small and I didn’t try the largest models, so none of this says a big model would fail.

What else I tried

Most of what I tried was aimed at giving the models a fairer chance. Letting them read the whole issue in passages, instead of only its start, lifted gte-small five points. Rewriting checks/service-state.nix into plain words changed nothing. Fine-tuning a model on my own linked pairs moved it three points, inside the noise. Qwen3-Embedding-0.6B, newer and larger, was the best single-vector model, and the rerankers, which read the new issue and each candidate together, made the order worse. None of it closed the gap.

To check the result wasn’t peculiar to this forge, I ran the same comparison on my notes vault, graded against its wikilinks. Counting words won there too, in the same order. I’d expected prose to favour the models, but my notes are short and full of names, much like the forge.

Before any of this I aimed a model at my labels, predicting who should act on an issue and escalating when unsure. Four issues in five are labelled within a minute of being filed, so there was no decision left for a model to take. Five minutes of counting would have told me that before I built anything.

So for this job the models came close but didn’t earn their place, and they cost far more. Counting words runs the whole test in under a minute with nothing to download. The small models needed a few minutes to process the forge, and the largest took about seven and a half hours on my server’s processor, reading each issue in passages. Most of that was the half-precision weights it ships, which my processor has to emulate: over each issue’s first 512 tokens the same model takes 55 minutes, and under eight minutes in single precision. Here the cheapest method is also the best one.

The models aren’t bad at language; my issues just aren’t much language. A model trained on code is the one I didn’t run. It would have to beat counting words by a margin no model here came within ten points of, so I left it there.