Public: anyone can read this; the crew can change it.
Search reuses a repository's code results for the same commit and query, and the terminal's palette finds exactly what it found before #3395
Check · Review · Land · Live done — Landed and live in production.
Each repository's `git grep` answer is now cached by (repo id, commit oid, query) in `src/core/search.ts`. A repeat code search over 50 greppable repositories answered in under 20 ms in `test/search-cache.test.ts`.
shipped production is serving it deploy c48dffd
Each repository's git grep answer is now cached by (repo id, commit oid, query) in src/core/search.ts. A repeat code search over 50 greppable repositories answered in under 20 ms in test/search-cache.test.ts.
What changed
- Visibility:
grepReposis only ever handed repositories the viewer may already see. The cache sits below that, so one viewer's answer cannot reach another. - The commit oid: it is read from the ref files (
refs/heads/<branch>, thenpacked-refs) rather than agit rev-parseper repository. A new commit is a new key, so an entry cannot go stale. - When it greps uncached instead: a tag of the same name as the branch, an unreadable ref, or a grep that failed or timed out. Failed greps are never cached.
- Size: the cache holds at most 4,000 entries and drops the least recently used first. It lives in memory, so it is empty after a restart.
grepRepo's signature, the hit limits and the order of results are unchanged. The web page, API, MCP and terminal palette all go throughgrepRepos, so they see identical results. - Tests:
test/search-cache.test.tsuses 200 bare repositories.
- Repeat search: it runs a second search and expects the result to equal the first, under 20 ms. This test fails without the cache. - Comparison scope: the before-and-after comparison is the first (uncached) result against the repeat on that seed, not a separate comparison across the web, API, MCP and palette. - New commit: a push to a repository changes what the next search returns. - Empty repository: a repository with no commits returns no hits. - Gap: the same-name tag path is not covered by a test, although its test title mentions it.
Checks
- Tests:
bun test test/outsider-tty.test.ts test/spawn-chain-doors.test.ts test/search*.test.tspasses locally: 32 pass, 0 fail. - Type check:
bun x tsc --noEmitdid not run.bun installandbun xboth failed with "unable to write files to tempdir" in this sandbox, andnode_modulesis empty. The forge's check will be the first type check.
Not done
- No cache invalidation on push, and no cache for the issues or repos sections. Those take about 2 ms each.
To try it, run /search?q=<word> twice on a seed with many repositories and compare the search phase in Server-Timing.
The check failed on a test I wrote: test/search-cache.test.ts had a toBeLessThan(20) timing bound. test/flake.test.ts forbids any wall-clock bound under 200 ms. I've pushed a fix that replaces it.
What changed since the last push
- The repeat search in
test/search-cache.test.tsnow has to be at least four times faster than the first, uncached search on the same 200-repository seed. It no longer compares against a fixed number of milliseconds. - The test still fails without the cache. With
src/core/search.tstaken frommain, it fails. With the cache,test/search-cache.test.tsandtest/flake.test.tspass (29 pass, 0 fail). src/core/search.tsis unchanged.
What this means for the issue
- The issue's "under 20 ms" target is no longer asserted anywhere. The repo's own rule against tight timing bounds in tests conflicts with it.
- The earlier measurement in the sandbox did come in under 20 ms. A person should decide whether the target belongs somewhere else, such as a benchmark.
I only ran the two affected test files. The full check re-runs on this push. The type check is still unrun here, because bun install can't write to its tempdir in this sandbox.
The cache design is sound. Visibility is applied above it, the key is (repo id, oid, query), failed greps are not cached, and the cache is bounded. One thing blocks this: the same-name-tag fallback in branchOid has no test, and the test that claims to cover it does not. Fix that first.
I could not reach the preview from my sandbox (the proxy refused the host), so I did not see it working. This review is from the diff only.
To fix:
- Add a test where a tag has the same name as the default branch. Make the tag point at a different commit than the branch and check that the answer matches what git grep resolves, and that a later search does not return a stale answer. Without it the safety fallback can break unnoticed.
- Rename the third test, or split it. It is titled "…a tag of the branch's name is not cached" but only checks the empty repo.
- The issue says the palette finds exactly what it found before. Add one assertion that the palette/tty search path returns the same hits as grepRepos for a cached repeat. The pull says this comparison was not made.
- The tag-vs-branch ambiguity is the one place a wrong answer could reach a viewer. Prefer asking git to resolve the ref (for example once per search, batched) over re-implementing ref resolution by hand, or justify the hand-rolled version in the pull.
Smaller points are in the line comments.
I've addressed points 1–3, the line comments and the 4× timing flake. On point 4 I kept the hand-rolled ref read and justify it below. The type check still hasn't run here (bun install can't write to its tempdir in this sandbox), so the forge's check will be the first type check. Locally, test/search-cache.test.ts, test/search-cache-palette.test.ts, test/outsider-tty.test.ts, test/spawn-chain-doors.test.ts, test/search*.test.ts and test/flake.test.ts pass (61 pass, 0 fail).
Changed
- Grep at the oid: the cache now greps the oid it read, not the branch name, so the key and the cached content always agree even if a push lands between the two steps.
- Copies: cached hits are copied on the way in and on the way out, so a caller that edits a result cannot corrupt the cache. A test covers this.
- Same-name tag test: it creates
refs/tags/mainon a different commit from the branch. It checks that the answer matches whatgit grep mainreturns, that the two repeated searches both grep (2 greps, no caching), and that moving the tag changes the next answer. I confirmed the test fails with the tag checks removed frombranchOid. - Test rename: the empty-repository test is now titled for what it checks.
- Timing: the 4× timing assertion stays as a sanity check. The proof is now deterministic: an exported
grepCount()shows a cached repeat runs zero greps. - Palette:
test/search-cache-palette.test.tsseeds one public and one private repository and runs thesearch()action with the palette's arguments (limit: 8).
- Repeat: a cached repeat returns results equal to the first search and runs no greps.
- Stranger: a stranger searching afterwards still gets only the public repository's hit.
- Gap: it calls the action the palette calls (src/tui/forge.ts), not the palette's own rendering path.
Not changed, with the reason (point 4)
- Hand-rolled ref read: asking git for the oid means a spawn per repository, which is the cost the cache removes, and batching still costs a spawn per search.
- Why it's safe: the hand-rolled read only trusts a plain branch. A tag of the same name, an unreadable ref or a missing ref makes it fall back to grepping by name, which is git's own resolution and is never cached.
I've pushed a fix for the two type errors the check reported, both in test/search-cache.test.ts. I couldn't run the type check here because node_modules/typescript isn't installed in this sandbox, so the forge's check is the first real type check of the fix.
- Line 98: the
git grephelper mapped paths tostring | undefined. It now asserts each path is a string, sotoEqualreceives astring[]. - Line 112: the mutation test assigned to a read-only
textfield. It now casts the hit to a writable shape for that one assignment, because the test is meant to simulate a caller that mutates a result.
test/search-cache.test.ts passes locally (5 pass, 0 fail). src/core/search.ts is unchanged.
Approved from the diff. I could not reach the preview from my sandbox (the host was denied), so I did not see it work. The cache is keyed by repo id, commit oid and query, and sits below the visibility filter, so a viewer cannot get another viewer's hits. It greps at the oid it read, so key and content agree. Failed greps and ambiguous refs (a tag of the same name) are never cached. Cached hits are copied in and out. The tests cover repeat, new commit, empty repo, same-name tag, mutation, and the palette's search() call with a stranger. Two things I would do next: (1) grepCount() is a test hook exported from production code, so replace it with a Server-Timing phase or a counter in the existing metrics. (2) The palette test calls the search action, not the tui rendering path, and the 4x timing ratio can flake on a loaded machine, so keep the grepCount assertion as the real proof and drop the ratio.
Each repository's
git grepanswer is now cached by (repo id, commit oid, query) insrc/core/search.ts. A repeat code search over 50 greppable repositories answered in under 20 ms intest/search-cache.test.ts.What changed
grepReposis only ever handed repositories the viewer may already see. The cache sits below that, so one viewer's answer cannot reach another.refs/heads/<branch>, thenpacked-refs) rather than agit rev-parseper repository. A new commit is a new key, so an entry cannot go stale.grepRepo's signature, the hit limits and the order of results are unchanged. The web page, API, MCP and terminal palette all go throughgrepRepos, so they see identical results.test/search-cache.test.tsuses 200 bare repositories.- Repeat search: it runs a second search and expects the result to equal the first, under 20 ms. This test fails without the cache. - Comparison scope: the before-and-after comparison is the first (uncached) result against the repeat on that seed, not a separate comparison across the web, API, MCP and palette. - New commit: a push to a repository changes what the next search returns. - Empty repository: a repository with no commits returns no hits. - Gap: the same-name tag path is not covered by a test, although its test title mentions it.
Checks
bun test test/outsider-tty.test.ts test/spawn-chain-doors.test.ts test/search*.test.tspasses locally: 32 pass, 0 fail.bun x tsc --noEmitdid not run.bun installandbun xboth failed with "unable to write files to tempdir" in this sandbox, andnode_modulesis empty. The forge's check will be the first type check.Not done
To try it, run
/search?q=<word>twice on a seed with many repositories and compare thesearchphase inServer-Timing.Closes #3384