How we rebuilt supermemory's best idea for the Mac — local-only, two primitives, and every mistake we made getting there.
A few weeks ago we shipped thought search inside Apprentice, with a simple bet: you should be able to open any file on your Mac from a vague memory of what it's about. Not its name. Not its folder. A thought.
"Find that essay I wrote about calculating the punch force that could snap a jaw."
That query has no filename in it. No date, no folder, no extension. Every search box on your computer will shrug at it. Ours opens the right PDF.
TLDR: We've open-sourced the whole layer — code, tests, the agent prompt, the war stories in the comments — at github.com/Reppin123/thought-search.
If you haven't been following along, here's what shipped:
find_file (name + kind + time, straight off Spotlight) and semantic_grep (content by meaning, from a local embedding index).path:line to start reading from, never a chunk dump.It's been running default-on for our users since v0.3.13. Time to write down how it works — and what it cost us to learn.
Two halves, each broken on its own.
Search by name is what your Mac already does. Spotlight is genuinely fast — it's a pre-built index, and scoped queries return in tens of milliseconds. But it's literal. It finds fermi-estimates-draft.pdf when you type "fermi." You didn't remember "fermi." You remembered punch force and jaw. Name search finds what you can name, and the file you're looking for is precisely the one you can't.
Agentic search is what every coding agent does: list a directory, open a few files, grep for a string, reason, repeat. It's powerful because the agent stays in control. It's also blind. Our agent's world was effectively ls ~/Downloads — anything in Documents, on the Desktop, in a project folder, might as well not exist. And when the model got ambitious, it reached for unscoped mdfind and find /: a 30-to-60-second walk of the disk, burning the context window on wrong turns the whole way down.
Semantic retrieval, RAG-style, should fix exactly this. One query pulls content by meaning, surfacing passages no filename could signpost. But it hands back top-K chunks ripped out of their files: excerpts with no neighbors, no surrounding page, no thread to pull. The agent gets an answer fragment and nowhere to stand.
Fast but literal. Smart but blind. Meaningful but severed. Pick one and you've picked your failure mode.
The whole point is: you shouldn't have to pick.
The supermemory team put numbers on the right design in their SMFS technical report, and the numbers are worth staring at. On their benchmark, a filesystem-only agent's per-question token cost grows ~7× as the corpus scales from 5 to ~10,000 files, and accuracy collapses to 69%. Their hybrid holds 81% — while spending half the tokens. (Their launch post is a five-minute read and worth all five.)
The mechanism is the part we fell in love with: semantic search lands on a path. Not an answer. Not a chunk. A path, with a line number. The agent opens the file, reads around the line, greps the subtree — agentically, from a starting point it actually trusts. The reach of semantic search and the control of agentic search, in the same motion.
That's the retrieval model. We stole it without apology.
Here's where we split from SMFS, and the split is the product.
SMFS mounts a cloud memory backend as a filesystem. Which means half their system — the genuinely hard half — is a sync engine: push queues, delta pulls, watermarks, dirty-bits, FUSE on Linux and NFS on macOS. Beautiful engineering. All of it exists because the files live somewhere else.
We inverted the premise. Your Mac is already the filesystem. The files are already local, already canonical, already yours. Nothing needs mounting, because nothing ever left. What's missing isn't a mount — it's the semantic layer over what's already there.
So thought search has no daemon, no sync loop, no reconciliation, no account. It has a SQLite file, a matrix in RAM, and two tools. Freshness isn't a distributed-systems problem; it's a content hash. A file changed if its bytes changed. A file moved if its bytes showed up at a new path — in which case its vectors move with it, for free, and nothing re-embeds.
The filesystem sees nothing new. The agent sees two more tools. The user sees a question answered. Nobody sees a server, because there isn't one.
find_file is the lexical leg. Name, kind, time, folder — straight off the OS index, always scoped, never a walk. Multi-word queries are tokenized so "financial memo" matches Acme Financial - One Page Memo.pptx, words apart, separators be damned. Temporal queries key on when you last opened the file, not when some background process touched it — so "the deck I worked on yesterday" means what you meant. On a miss it escalates: work folders, then your whole home directory, then external drives. It never dead-ends silently. It tells you where it looked.
semantic_grep is the semantic leg. Contents chunked and embedded locally — ONNX, no PyTorch, ~15MB of runtime — searched by cosine over an in-memory matrix. A hit is path:line (score) — excerpt. A launch point.
And the router between them? There isn't one. No intent classifier, no keyword table, no pipeline. The model decomposes "the pdf about pricing from last week" into parameters, picks the leg, and fuses results by reading them — the same way you choose between grep and grep -r without a flowchart. We learned this the hard way: every deterministic gate we built caged the model into dead ends the old free-form exploration would have escaped. Speed belongs in the index. Smarts belong in the model. Don't confuse the two.
This is the section I wish more engineering blogs had, so here's ours. Five real failures, in shipping order.
The index that lied for 26 minutes. First live test: the background crawl ran 26 minutes, the write-ahead log swelled to 71MB, and every single search said "index empty." We committed at the end of the crawl. Now we commit every 20 files — partial results are searchable during the first build, and a killed process keeps its progress.
The 373% CPU crawl. That same first crawl pegged four cores while I was trying to work. An index that spins the fans is an index that gets uninstalled. Now: lowered OS priority plus a per-embed sleep. Background means background.
The 5.8-second search. At 252,000 chunks, every query was reloading a 388MB vector matrix from SQLite. Caching it in RAM took search from 5.8s to ~40ms. Then the cache broke anyway — we keyed it on SQLite's data_version, which turns out to be per-connection, so a fresh connection per query meant a full 388MB rebuild on every single search. The fix is embarrassingly small: key on chunk count. The lesson is not small: measure the warm path with the real access pattern, not the bench harness.
The 73-second warm-up. Loading the cache built six 252,000-element Python lists of snippets and paths. The matmul was never the cost — object churn was. The cache now holds the matrix and small integer arrays, nothing else; snippets are fetched lazily for the ten winners. 73s → 6.7s.
The filter that ate the right answer. We let the model pass a file-type filter to semantic search. It guessed kind='document' for a query about a PDF; 'document' didn't include PDF; the exact file the index contained got silently excluded — and the agent thrashed for eight turns looking for a file we had. An inferred filter is a silent excluder. Semantic search now returns every type and shows the extension. Choosing is the model's job.
We also evaluated bf16 vector storage — halves the RAM! — and rejected it, because numpy has no BLAS f16 matmul, so you pay a 2-3 second upcast per query or upcast at load and the RAM comes right back. The dead end stays documented in the comments so nobody walks it twice.
On the machine it shipped on: 7,207 files, 280,098 chunks, 1.2GB of index. Warm semantic search benches ~40ms at 252k chunks. The lexical leg answers in 0.09–1.24s live. Vague, name-less, memory-shaped queries — a college essay by its topic, a workout PDF by a concept the filename never mentions — land on the right file with the right line to start reading from.
One honesty note, because SMFS set the bar here and we intend to meet it: their numbers come from a 440-trial public benchmark. Ours come from one founder's Mac, measured in live use. We're building toward the same discipline — tokens and tool-calls per task, not just latency — and the eval harness is public in the repo.
The index refreshes on crawl, not yet on a live file-watcher. The embedder is small on purpose and the seam to swap it is two constants wide. Images, audio and video want transcription siblings — talk.mp3 → talk.mp3.transcript.md, source canonical, sidecar greppable — which is designed and not yet built. And the endgame is that a thought should find anything you've touched: files today; everything you've seen and said, next.
Your files were the memory all along. We just gave them retrieval.
That's the bet. That's the roadmap.
Code: github.com/Reppin123/thought-search · Product: withapprentice.com · The idea's origin: SMFS, by supermemory