Search

One field over every message you have ever sent or received, every memory a project keeps, and the name of every file a session left on disk.

The screen

$ braidsthen / and a word
braids: the search screen

/ opens it from any screen. Results come back as you type, in a millisecond or two. The TYPE column says what each hit is, because the three kinds want different things done to them, and ↵ opens the screen that can do it: a conversation opens its spine at that turn, a memory opens the reader, a work product opens the browser at that directory.

KeyWhat it does
typethe query, live
tabnarrow to this conversation, or widen back to everything
↑ / ↓move through the hits
↵open the hit
escback where you came from

From the command line

braids search "lock across a network call"
braids search worktree --limit 5
braids search "index" --type memory,artifact
braids search deploy --project braids --since 30d
braids search "rate limit" --since 2026-08-01 --until 2026-08-31
braids search "timed out" --kind tool_result --lane 46360382
braids search "fork" --json
FlagWhat it does
--lane IDonly this conversation, by ID prefix
--project NAMEonly this project, matched without regard to case
--since WHENa date or an age: 2026-08-01, 30d, 6w, 12h
--until WHENthe same, as an upper bound. A bare date means all of that day
--type LISTconversation, memory, artifact. All three unless you narrow it
--kind LISTtext, thinking, tool_use, tool_result
--limit Nhow many hits, default 20
--jsonmachine readable, with whole IDs

The query syntax

Search is SQLite's FTS5 over what was written, and its whole query syntax works. The one worth knowing first is that bare words are and: payment retry asks for both, which is narrower than either word alone and is the usual reason a search comes back empty about a conversation that is really there.

QueryMatches
a bboth, in any order
a OR beither. This is how you cover a synonym
a NOT ba, excluding anything that also has b
"a b"that phrase, in that order
a*prefix: deploy* matches deployed and deployment
NEAR(a b, 10)both, within ten words of each other

Anything with an operator, a quote, a bracket or a star is handed to FTS5 as written. Everything else is treated as words to find, so a plain question needs no escaping.

Raise --limit when you use OR. The limit is shared across the whole query, so a rare word gets crowded out by a common one: on a real history of 28 conversations, five alternatives found fifteen of them at --limit 500 and seven at the default of twenty.

Reading what a hit points at

A hit is a pointer, not an answer. It names a conversation, the turn the match was on, and about twelve words either side of it, which is enough to recognise the right thread and far too little to tell a proposal from a conclusion. The sentence that settles it is usually the one after the one that matched.

braids show --lane 9419fd9c --at 22403                # six turns either side
braids show --lane 9419fd9c --at 22403 --kind text    # only what was said
braids show --lane 9419fd9c --from 100 --to 140       # a range
braids show --lane 9419fd9c --plain                   # without terminal colour
braids show --lane 9419fd9c                           # how it ended

Two thirds of a real conversation is tool calls and their results, so --kind text is usually what you want; leave it off when the thing you are after is what a command returned. Each block is cut at --chars characters and says how many it dropped, because a single tool result can be a whole file and arriving whole is rarely a kindness.

A tool that found a terminal on the other end wrote colour codes, and braids stores what was written, so they come back with it. --plain takes them out. It is off by default because editing a transcript to make it read better is the one thing braids does not do, and on request because those bytes sit inside the words: a test run that printed 1 failed in red holds eleven escape bytes in the middle of that phrase, so searching the output for it finds nothing. Stripping happens before --chars, so asking for it also buys more of the words.

This is the command the map has always had as a screen and the command line did not. A search could tell an agent where an answer was and give it no way to read the answer, which made the honest advice "quote the evidence" impossible to follow.

The same filters, typed

Flags are right for a script and wrong for a person mid-sentence, so the filters can also be typed into the query itself, which is what the search screen takes:

braids search "project:braids since:30d lock"

project:, lane:, since: (or after:), until: (or before:), kind: and type:. Press / in braids and the same words work there, with the header saying what it narrowed to. A flag wins where both are given.

Pressing / with nothing typed lists them, which is where anyone actually finds out they exist, and the results carry the date each thing happened so a bound by date can be checked rather than taken on trust. On a narrow terminal the date is the first column to go, because a result whose text you cannot read is no result at all.

A word that looks like a filter but cannot be read as one stays part of the query. That matters because the field searches on every keystroke: halfway through typing since:30d you have since:3, and a parser that failed there would blank the screen while you type.

How it ranks, and why that took two goes

The index is SQLite FTS5 and the ranking is bm25, which prefers short documents. A filename is three words and a turn is several hundred, so ranked in one pool the filenames won everything and buried the conversations. Each kind is now queried and ranked separately, then merged by interleaving.

The first attempt at that shared one query across memories and work-product names, which starved the memories: a search for a word appearing in four memories returned none of them. Each kind gets its own query now.

Work products are matched by name, not by contents. Indexing the contents of 3.3 GB of scratch files would cost more than it is worth, and the name is what you remember.