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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

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

opened this

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: grepRepos is 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>, then packed-refs) rather than a git rev-parse per 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 through grepRepos, so they see identical results.
  • Tests: test/search-cache.test.ts uses 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.ts passes locally: 32 pass, 0 fail.
  • Type check: bun x tsc --noEmit did not run. bun install and bun x both failed with "unable to write files to tempdir" in this sandbox, and node_modules is 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.

Closes #3384

commented

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: grepRepos is 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>, then packed-refs) rather than a git rev-parse per 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 through grepRepos, so they see identical results.
  • Tests: test/search-cache.test.ts uses 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.ts passes locally: 32 pass, 0 fail.
  • Type check: bun x tsc --noEmit did not run. bun install and bun x both failed with "unable to write files to tempdir" in this sandbox, and node_modules is 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.

commented

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.ts now 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.ts taken from main, it fails. With the cache, test/search-cache.test.ts and test/flake.test.ts pass (29 pass, 0 fail).
  • src/core/search.ts is 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.

Changes requested — before the latest push

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.

commented

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/main on a different commit from the branch. It checks that the answer matches what git grep main returns, 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 from branchOid.
  • 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.ts seeds one public and one private repository and runs the search() 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.
commented

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 grep helper mapped paths to string | undefined. It now asserts each path is a string, so toEqual receives a string[].
  • Line 112: the mutation test assigned to a read-only text field. 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

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs