Finding related work
Kairos can tell an agent what a piece of work is probably related to. This page is about why it is shaped the way it is, because the shape is counter-intuitive: a good answer here often looks like a weak one.
The problem is not search
Section titled “The problem is not search”Search finds what you ask for. The expensive failure in a work-management system is different: it is the ticket nobody knew to look for. Two teams solving the same problem in different words. A dependency real enough to break a release and written down nowhere. A question somebody answered eighteen months ago and put away.
A person half-remembers “didn’t we look at this?”. An agent does not, and will cheerfully re-solve last quarter’s problem with complete confidence.
The claim is a disagreement
Section titled “The claim is a disagreement”Similarity on its own says nothing worth telling anyone. Two tickets about billing are supposed to look alike — they are both about billing.
What is worth telling is where similarity and the graph disagree:
| similar, and… | what that suggests |
|---|---|
| nothing joins them | a dependency nobody drew |
| they share a parent | probably the same work written down twice |
| the other is finished or put away | prior art — somebody has been here |
This is why Kairos can do something a general-purpose search engine cannot. Its
graph is authored: a human drew every parent and blocks edge on purpose.
Most systems that do this have to extract a graph from prose first and inherit
every mistake that makes. Here the expensive half is already done, and the
communities already have names — an initiative and its children, a team’s board,
a repository’s tasks.
Why you get proposals, not answers
Section titled “Why you get proposals, not answers”Every result says possible. That is not hedging, it is the measurement.
We took every pair of 4,927 real work documents from nineteen repositories and compared them:
| average similarity | |
|---|---|
| pairs the graph says are related | 0.80 |
| pairs with no relation at all | 0.68 |
Related work does score higher. But unrelated pairs reach 0.82 at the top of their range — higher than the average related pair. The two distributions overlap across their entire useful span. Repeating it on Kairos’s own seeded data gave the same answer: related 0.71–0.73, unrelated 0.58, and unrelated pairs reaching 0.75 while genuinely linked pairs fell to 0.57.
There is no number you could put between them. Not a carefully tuned one, not a per-tenant one. At the very top of the range, where the signal is strongest, about half the pairs are genuinely related and about half are not.
So an agent told flatly “this blocks you” would be wrong about half the time, and would act on it — which is worse than being told nothing. Everything here is phrased as a suggestion because a suggestion is what the evidence supports.
What this means for you
Section titled “What this means for you”Read the reasoning, not the ranking. Each proposal says what matched, which section it matched in when one section decided the match, and what the graph did or did not know. That sentence is the useful part; the order is a hint.
Look past your own project. Items of the same project share its words, so they usually take the first places. Prior art from another repository can come lower in the list, and it is often the most useful proposal.
“Nothing joins them” is weaker than it sounds. It means no direct edge and no shared parent — not that a path was searched for and missed. Work graphs are sparse, so absence of a link is mostly absence of evidence.
A short answer is not a broken one. Results are bounded to a handful on purpose. Forty related items is a research project, not an answer.
Sometimes it will say it only searched the text. That happens when a deployment has no vectors yet, or has just changed models. The answer is real but thinner — it will have missed work phrased differently — and it tells you so rather than letting you assume otherwise.
A text-only answer is also shorter. The rank of a text match is not enough, because common words match most items. So each word of the title weighs by how rare it is. The tool proposes an item only when it matches a large share of that weight. It gives 3 proposals at most, and often none. “None found” from text is a fair answer. Five matches on common words look like findings, but they are not.
How the text half is stored
Section titled “How the text half is stored”The text half of retrieval is PostgreSQL full-text search. The q of
search uses it too. Each item has one row in the table item_search. The
row holds a weighted vector of the words of the item. The title has weight A
and the content has weight B, so a title match ranks above a body match.
Triggers on the five item tables write the row. They write it in the same transaction as the change, so the vector is never out of date. A create writes the row, and a purge deletes it. A change to the title or the content writes a new vector. A transition, an archive or a restore does not write it.
A GIN index on the stored vector finds the matches. The rank (ts_rank_cd)
reads the stored vector too. So a search does not read the content of an
item again. Before this table, the index held no vector. Each search read
the content of each match two times, once to match and once to rank. On a
test tenant with 900 matching tasks, a search of 100 tasks took 1.45 s. Now
it takes 87 ms.
An edit of the content also costs less. The item tables have no index on the content now. So PostgreSQL can often write an edit as a HOT update, which adds no index entry.
Confirming makes it better
Section titled “Confirming makes it better”When a suggestion is right, confirming it draws the edge for real. That is not bookkeeping: graph distance is half the signal above, so every confirmation makes the next answer better. The system improves by being used.
A suggestion is not an edge until someone confirms it. A wrong parent edge
re-parents work onto a board that then reports the wrong thing to the wrong
people, and nobody re-reads an edge once it exists. A proposal costs a click; a
wrong edge costs a conversation.
A grant decides who confirms: approve_proposals, on the board of one of the
two items. An organization admin can also decide. An organization chooses who
holds the grant. It can be a person, the agent key of that person, or a service
account that automates the approval. The holder can also decide their own
proposal. So a team of one person can work with its agents, and a larger team
can ask a second person to check each edge.
The grant is the rule because a confirm is not a vote on a suggestion: it
writes the edge. If the confirm asked for nothing, a member who can edit
neither item could draw a parent edge in two steps, a proposal and a confirm.
The server refuses that edge as one step. A proposal must not be a way around a
rule. So each member can suggest, and the holders on the two boards decide. A
refused confirm leaves the proposal where it was, for one of them. See
The decision of an edge proposal.
Rejections are kept, too. A pair that keeps being suggested and keeps being rejected is the clearest evidence available that the retrieval is wrong about something.
Related reading
Section titled “Related reading”- Connect over MCP — the
related_workandpropose_edgetools an agent uses - Configure semantic retrieval — models, endpoints, and the backfill
- Archiving — why finished work stays findable, which is what makes prior art possible
- Repositories as execution scope — why a shared repository is a strong hint of real coupling