Releases: dualeai/seek
Release list
v0.14.0
seek gc gains a --sort flag and hardened table rendering. seek gc --dry-run --sort=size shows what is eating your disk.
Upgrade. No re-index, no cache-layout change, no removed flags. Automatic cache GC is unchanged — sorting is manual-only.
What changes for you
- Sortable GC table.
seek gc --sort=name|age|size:name(default, hash order),age(oldest.usedfirst — the eviction order),size(largest first). Invalid--sortexits non-zero (was a silent no-op); shell completion lists the values. - Disk audit in one command.
seek gc --dry-run --sort=sizeprints the plan by size, mutates nothing. Add--forceto skip the daily throttle,--allto evict every corpus not in use. - Terabyte sizes. The SIZE column now renders TB caches (was GB-capped), still within its 6-char width.
- Sane ages under clock skew. A future
.used(NTP step,cp -prestore) renders0s, not a negative raw-second blob. - Safe table cells. Control characters and tabs are stripped from displayed paths; left-truncation lands on rune boundaries, so multibyte paths never render orphan continuation bytes.
- Documented maintenance. New README "Cache maintenance" section: auto-GC (14d TTL, once per day, off on NFS), the
SEEK_GC_MAX_AGE/SEEK_GC_INTERVALknobs, manual commands.
Dev/test
- Dry-run and live GC paths unified so the tables cannot drift: shared
gcTableStats,emitGCRow,predictGCAction/evictIfExpired,sweepCorpusOrphans,buildGCRows/sortGCRows.--sortvalues come from onegcSortValueslist feeding both the parse error and completion. - Sorted views (
age/size) measure all rows before rendering;nameand the silent opportunistic path stream incrementally, keeping Ctrl-C prefix semantics. releaseLockis now nil-safe, replacing hand-rolled unlock+close inrunGCandevictCorpus.- Coverage:
gc_sort_test.go(sort keys, tiebreaks, cancel),gc_format_test.go(TB, sanitize, rune boundaries, negative duration), a guard that the empty-cache live run still stamps.last-gc, andBenchmarkGC_RunGC_StreamingSorted_N100. TODO(gc-size-cap): futureSEEK_GC_MAX_TOTAL_SIZE(journalctl --vacuum-sizestyle, LRU-by-age).
Full changelog: v0.13.2...v0.14.0
v0.13.2
Indexing no longer blocks searches, and scoped Git searches reuse the whole-repo index instead of building one per scope.
No flag changes. First search after upgrade may rebuild the index once; later searches reuse it.
What changed
- Lock-free builds. Builds run in a temp dir, validate HEAD, then publish atomically under a brief lock (
.build.lockbuild,.lockpublish). Readers hold a shared lock across glob+open — never blocked by a build, never see a torn shard set. A.swappingmarker recovers a crash mid-swap. - Scopes share one index. A scoped search (
seek 'x' ./cmd/seek) reuses the combined whole-repo index and filters at search time — no per-scope layers, less rebuild churn. - Over-cap fallback. If the whole repo exceeds index caps, a scoped search builds a per-scope index instead, so large siblings can't cap small scopes. A
.git_cap_exceededmarker (HEAD + cap limits) caches the verdict; HEAD or cap-limit change re-evaluates. - Faster file scopes. ≤64 file operands compile to trigram-assisted anchored regexps instead of an O(docs) scan; above 64 falls back to set lookup.
- GC hardening. Sweeps orphaned temp build dirs, skips eviction during a build, excludes temp dirs from reported size.
Dev/test
- New
swap.go(temp build, atomic publish, crash recovery); reworkedlock.go(acquireBuildLock/acquirePublishLock/acquireReadLock). - Scoped path collapsed onto the shared index in
corpus.go/git_scope.go/main.go; per-scope index kept only as the over-cap fallback. RemovedgitRepoStateInScopeand the committed/dirty/shared-committed layers. - Coverage: build/search concurrency, mid-swap recovery, orphan-dir GC, shared-index reuse, over-cap fallback, file-operand regexp vs set.
v0.13.1
Scoped Git searches now share one whole-repo committed index instead of building a separate one per scope. Clean trees skip the dirty layer.
No flag changes. First scoped search after upgrade may rebuild the index once; later scopes reuse it.
What changed
- Scopes share one committed index. A scoped search (
seek 'needle' ./cmd/seek) reuses the repo's whole-repo index and filters by scope at search time — less rebuild churn across subtrees of the same repo. - Clean trees do no dirty work. A clean in-scope tree skips the dirty layer — no dir, no state writes.
- Over-cap repos fall back. When the whole repo exceeds the index caps, search uses the per-scope committed layer instead. A
.git_cap_exceededmarker (keyed by HEAD + cap limits) caches that verdict so the budget isn't re-scanned every search; a HEAD or cap change re-evaluates.
Dev/test
- Shared
committed_sharedlayer on the plan; per-scopecommitted*kept as over-cap fallback.resolveCommittedLayerpicks the layer;prepareAndSearchCorpusOncerepointsplan.committed*and zerosplan.dirty*on a clean tree. - Cap marker:
read/write/removeGitCapMarker+gitCapMarkerValue(treeish + cap limits), best-effort (write failure only warns). - Coverage: shared reuse across scopes, over-cap fallback, clean-tree elision, cap-marker invalidation, state validation skipping the elided dirty layer.
v0.13.0
File operands inside a Git worktree now route through the repo's scoped Git index instead of a separate per-file corpus. seek 'needle' ./cmd/seek/searcher.go keeps Git ignore and local-change behavior, reuses the repo's index, and reads back as the file you selected.
Upgrade. No flag changes. Exact files inside a Git repo now route to the repo's Git corpus (scoped to your selection), not a standalone single-file corpus — files and directories take the same routing decision.
.gitignore-excluded files and folders, and anything outside a worktree, fall back to a plain file/folder corpus. The Git corpus identity is unchanged; a file newly routed to it may build a scoped committed/dirty layer on first use, and an ignored fallback may build a folder/file corpus.
What changes for you
- In-repo files reuse the Git index. A tracked or untracked-visible file routes to its worktree's Git corpus, scoped to the selection — no per-file corpus, consistent ignore + dirty-layer behavior.
- Ignored folders are searchable. An exact
.gitignore-excluded file or folder now searches via a fallback corpus. Previously only ignored files. - Selected files read as what you picked. Single-corpus mode shows the basename; multi-corpus mode keeps the absolute, openable path.
- Files follow nested-Git ownership. A file routes through its parent dir: into the inner repo when nested, collapsed to one plan when multiple files share a repo or a selected directory already covers them.
- Predictable at boundaries.
git check-ignoreruns index-aware (tracked-but-ignored stays tracked); submodule-interior paths fall back per path; case/normalization-insensitive filesystems are corrected to the real on-disk name so a mistyped-case operand isn't silently missed. - Pathspec-safe operands. Paths are fed as literals (
./-prefixed), so a:-leading name isn't misparsed as pathspec magic; a symlink to a gitignored in-worktree file falls back instead of vanishing.
Dev/test
- 3-phase operand router (resolve → classify visibility → route), one
git check-ignorebatch per repo root,core.fsmonitor=falseto skip the fsmonitor IPC tax. - Extracted
git_scope.go,git_visibility.go,corpus_id.go. realCaseWithincorrects case/NFC-NFD names byos.SameFileidentity before they drive the byte-exact Git scope.- Regression coverage: classify gap (tracked/untracked/ignored/nested/collapse), visibility matrix (submodule-interior, leading-colon, all-prefiltered), case-mismatch, symlink-to-ignored, operand-order independence, frozen golden guard on the dirty-scope cache key. Refreshed README and CLI help.
Full changelog: v0.12.0...v0.13.0
v0.12.0
Git directory operands now use scoped Git indexes. seek 'needle' ./platform keeps Git ignore and local-change behavior, but no longer lets unrelated repo files drive indexing, caps, or results.
Upgrade. No flag changes. Compared with v0.11.1, directories inside a Git repo now use Git rules instead of the literal folder pipeline. Exact files remain literal, folders outside Git remain folder corpora, and root/file/outside-folder cache identities are unchanged. Git directory operands may build new scoped committed/dirty layers on first use.
What changes for you
- Scoped Git directories. Selected Git directories get committed and dirty layers for that subtree, so large ignored artifacts, dirty siblings, or tracked siblings outside the path no longer trip Git file/byte caps.
- Nested Git handled once. No-path searches, Git-root operands, and selected Git directories discover visible nested repos, submodules, gitlinks, and linked worktrees, while parents exclude them to avoid duplicate results and ignored-content leaks.
- Ignore and explicit operands stay predictable. Ignored directories inside the selected repo stay ignored; exact ignored files and explicit nested Git roots still search what you asked for.
- Fewer Git edge-case misses. Seek forces
--untracked-files=all, treats pathspec metacharacters literally, strips pathspec environment overrides, and can reuse scoped committed layers after commits outside the selected path.
Dev/test
- Added scoped committed/dirty Git layers with separate caches, locks, empty-layer markers, state validation, and
git ls-tree/git cat-filecommitted indexing. - Recursed nested-Git discovery through visible repos, submodules, gitlinks, and linked worktrees; removed the old fixed discovery cap.
- Hardened Git boundary detection for symlink escapes, common-dir escapes, and linked-worktree backrefs.
- Added regression coverage for scoped budgets, ignored nested Git, pathspec handling, nested untracked files, layer drift, exact-file ownership, and refreshed README/benchmark docs.
Full changelog: v0.11.1...v0.12.0
v0.11.1
Small follow-up to v0.11.0. Path operands now do what they say: exact files search only that file, plain folders search that folder, nested Git roots stay separate, and local Git repos without remotes index reliably.
Upgrade. No flag changes and no cache-layout change. Existing Git indexes stay valid; explicit file and non-Git-root directory operands may build a new literal corpus on first use.
What changes for you
- File operands are exact.
seek 'TODO' ./src/foo.gosearches only that file, even inside a Git worktree. Result context now uses the file's parent directory. - Plain folder operands stay plain. Passing a non-Git-root directory searches that folder literally, including ignored files when passed directly. For Git-aware subtree search, search the worktree root and filter inside the query, for example
seek 'needle file:src'. - Git roots keep Git behavior. Worktree-root operands keep Git ignore handling and
[uncommitted]labels. - Nested Git roots stay separate. Discovered nested Git roots get their own Git corpus. If discovery is disabled or capped,
seekfalls back to parent-folder traversal. - Mixed parent/child operands deduplicate. A Git parent plus explicit child operand no longer returns child results twice.
- Symlink operands are consistent. Direct symlink operands resolve to their target; broken symlinks error; symlinks found during folder walks remain skipped.
- No-remote Git repos index reliably. Local Git repos without a remote use a stable opaque fallback repository name, including repos named
uncommitted. - Cold-cache failures report the real cause. Git indexing failures now return the indexer error instead of
no index shards.
Dev/test
- Shared Go setup action with Ubuntu 26.04 linker workaround.
- CodSpeed action updated to
v4.17.6. - Go directive updated to
1.26.4. - Delta pressure test now uses channel synchronization.
- Regression coverage for exact files, ignored directories, nested Git roots, no-remote repos, symlink operands, cold-cache errors, and duplicate suppression.
Full changelog: v0.11.0...v0.11.1
v0.11.0
Output rendering pass: long lines window around the match, color on a terminal and plain when piped, control bytes and Trojan-Source overrides stripped, truncation now states what it hid.
Upgrade. No re-index, no flag changes, no cache-layout change. Text output changed: denser gutter and a truncation notice — re-check anything that parses seek's output.
What changes for you
- Color, auto-gated. File cyan, line numbers dim, matches bold red — terminal only. Piped output (agents, CI,
| cat) stays plain, zero config. Precedence:NO_COLOR(non-empty) off →CLICOLOR_FORCEon (even piped) → else on whenTERMset/≠dumband stdout is a TTY. - Long lines windowed. Match line over 1024 B → cropped to ±512 B around its matches (never bisects a match),
…+N bytesmarkers for dropped head/tail. Context lines over the cap tail-trimmed. Cuts on rune boundaries. No new flags. - Control-char & Trojan-Source sanitisation. C0/C1/DEL (tab kept) and the bidi / line-paragraph-separator code points behind CVE-2021-42574 stripped from output. Zero-width joiners kept (emoji, scripts).
- Truncation announced. File cap →
… N more files (showing X of Y); per-file match cap →… N more matches in this file. Was silent. - Denser gutter. Dropped line-number padding and two-space indent — now
<lineNo> <content>. Fewer tokens per line. - Directly-openable multi-corpus headers. Header carries the absolute path (
displayRoot+ file name); tag collapses to bare[git]/[folder], was[git: /abs/root].
Breaking
- Text output format changed — denser gutter, announced truncation, reshaped multi-corpus headers. Re-check output-scraping tooling. CLI flags, subcommands, and cache layout unchanged.
Dev/test
- Deps:
golang.org/x/termadded;golang.org/x/sys→ v0.46.0;cobra/pflagnow direct. - Tests:
color_gate_test.go,formatter_color_test.go(~490 lines), pluscli_root_test.go/main_test.goadditions. cicd/bench-field.sh: newTok(content)/Tok(sym)columns — piped-output token count (tiktokeno200k_baseviauv).- CI:
ubuntu-26.04runners (matrix expanded to 24.04/26.04 + macOS), coverage upload,govulncheckSARIF scan, dependency submission; test job timeout 25 min,make test-unitcoverage profile under 18m.
Full changelog: v0.10.0...v0.11.0
v0.10.0
Search a parent dir holding many repos: each indexes as its own corpus. Multi-GiB folder corpora no longer wedge. CLI errors actually tell you what went wrong. seek gc shows you what it evicted.
Upgrade. Folder corpora indexed under v0.9.x re-index once on first search (cache layout v2 → v3). Git corpora untouched. CLI flags and on-disk paths unchanged.
What changes for you
- Multi-repo parent folders.
seek 'foo' ~/devnow discovers nested git repos inside the parent and indexes each one separately — gitignore honored per repo, results disambiguated by corpus. Capped at 64 nested repos per query (Warn on cap). - No more wedged folder searches. The deadlock that hung indefinitely on multi-GiB folder corpora is fixed. Per-corpus content cap raised 5 GiB → 10 GiB; peak RSS still bounded.
- Multi-corpus searches run in parallel. Up to 4 corpora index concurrently. Same physical repo reached via symlinks or multiple operands indexes once.
- CLI errors that help.
seek gcc→unknown subcommand "gcc" (did you mean "gc"?)seek --verbos foo→unknown flag: --verbos (did you mean --verbose?)seek -file:test(single-dash Zoekt query) still passes through unchanged.
seek gc --forceis no longer silent. Live banner + per-corpus table + summary, same shape as--dry-run. ACTION column:evicted/kept/locked/gone/trashed: <err>/failed: <err>.- Wedged-indexer signal. A stuck indexer used to silently degrade every search to stale shards forever. Now you get a Warn naming the lock so you can find and kill it.
seek gcsubcommand. Promoted to a real subcommand with--force,--dry-run,--all. Alias:garbage-collect.
Performance
Microbenchmarks (Apple M1 Max, benchstat over 5 samples):
| Benchmark | Δ ns/op |
|---|---|
FolderCorpus_ColdIndex |
−41.0% |
FolderCorpus_WarmSearch |
−20.1% |
SmallRepo executeParsedSearchScoped |
−52.7% |
SmallRepo planCurrentGitCorpus |
−21.3% |
SmallRepo ensureUntrackedCache |
−31.6% |
SmallRepo indexUncommitted_1file |
−12.1% |
SmallRepo postVerify_restat |
−21.5% |
PlannedGitCorpus_DirtyReindex |
−9.2% |
| geomean (16 benches) | −18.4% |
Field benchmarks — git + non-git, PR-scale dirty re-index at 1% and 10%:
| Kind | Workload | Files | Cold index | Warm search | Dirty 1% | Dirty 10% |
|---|---|---|---|---|---|---|
| git | spf13/cobra | 66 | 470ms | 80ms | 120ms | 130ms |
| git | prometheus/prometheus | 1,635 | 1.8s | 90ms | 150ms | 380ms |
| git | kubernetes/kubernetes | 30,507 | 11.0s | 180ms | 700ms | 4.3s |
| folder | synthetic-10k | 10,000 | 6.0s | 100ms | 250ms | 750ms |
| folder | synthetic-100k | 100,000 | 33.5s | 250ms | 1.3s | 7.8s |
Reproduce: ./cicd/bench-field.sh [--no-linux] [--keep].
Breaking
- Cache layout
v2 → v3— folder corpora re-index once on first search. - Internal Go API:
streamFilesrequirescontext.Context;acquireSearchLockrequiresindexDir string;fileContent.weightRelease contract (see docstring);gitPaths.ExcludePathremoved.
Dev/test
- 19 new test files: cancellation with goleak, pressure (4 MiB swap), property/fuzz, deadlock guard, discovery fixtures, git-boundary detection, Cobra CLI surface, soak (gated
//go:build soak+SEEK_SOAK=1). cicd/bench-field.sh— self-contained field-benchmark harness, sandboxed viaSEEK_CACHE_DIR.make test-bench-comparewrapsbenchstat.- New deps: cobra, pflag, goleak (test-only).
Full changelog: v0.9.0...v0.10.0
v0.9.0
Per-file indexing cap 10 MiB → 100 MiB. New byte-weighted read semaphore caps reader-buffer RAM at ~600 MiB. Drop-in upgrade from v0.8.0 — no CLI or cache-layout changes.
Highlights
maxIndexedDocumentBytes10 MiB → 100 MiB. Vendored libraries, minified bundles, generated JSON/CSV, lockfiles, large protobufs now indexable. Overflow files skipped withslog.Warn.- Bounded in-flight memory. A new byte-weighted semaphore caps total bytes resident in reader buffers across all workers at ~600 MiB, so the larger per-file cap does not balloon RSS under bursts of large files.
- Auto-scaling ceiling.
maxInFlightBytes = inFlightHeadroomFiles (6) × maxIndexedDocumentBytes. Bumping the per-file cap scales the in-flight budget for free. TuneinFlightHeadroomFilesfor tighter or looser ceilings.
Development
- New
cmd/seek/read_semaphore.goplus lifecycle tests covering leak-free drain on builder errors, panic safety, and ctx cancellation mid-read. - Reader weight is released by the consumer after
builder.Finish()returns, because Zoekt's shard writers retainContentreferences until then. - New dep:
golang.org/x/sync v0.20.0.
Notes
- Synchronous folder-delta reads stay unbounded by the semaphore (documented as future work).
Full changelog: v0.8.0...v0.9.0
v0.8.0
Two big wins for steady-state cost. Zoekt delta indexing replaces full shard rebuilds, and a new cache GC keeps ~/Library/Caches/seek (or ${XDG_CACHE_HOME}/seek) bounded over time.
seek gc # opportunistic eviction (default)
seek gc --dry-run # report only
seek gc --force # ignore throttle gate
seek gc --full # evict everythingHighlights
- Zoekt
IsDeltaenabled for committed and uncommitted shards. Only changed blobs are touched per cycle; renames and removals land as tombstones in the.metasidecar. - Committed indexer flips to delta with a 64-shard fallback threshold.
- Uncommitted indexer mirrors the folder pattern: per-cycle manifest (path + size + mtime + inode) tagged with state hash, sorted-merge diff against working tree, tombstones on rename/delete.
- New
seek gcsubcommand with--force,--dry-run,--full. - Opportunistic GC runs after each successful search, gated by throttle and NFS detection (skips network filesystems).
- Suffix-only shard cleanup preserves Zoekt's contiguous shard numbering (fixes a latent folder-corpus bug too).
stateVersionbumpsv5 → v6to force a one-time clean re-index for upgraded binaries.
Performance
Measured on a real kubernetes/kubernetes clone (~17k Go files, 1.6 GB), M1 Max, n=3 medians, via SEEK_BENCH_REPO-gated benches:
| Bench | Before | After | Delta |
|---|---|---|---|
LargeRepo_CommittedAdvance wall |
8.97 s | 2.43 s | 3.7× faster |
LargeRepo_CommittedAdvance RAM |
~3.2 GB | ~250 MB | ~13× less |
LargeRepo_CommittedAdvance allocs |
~19 M | ~320 k | ~60× fewer |
LargeRepo_UncommittedRealistic |
97.3 ms | 104.1 ms | +7% (noise) |
| Cold index (warm page cache) | 10.04 s | 9.21 s | −8% |
Small-fixture benches (1-file, M1 Max, n=5 medians):
- Win:
PlannedGitCorpus_ColdIndex312 → 272 ms (−13%),PlannedGitCorpus_WarmSearch28.0 → 25.6 ms (−9%). - Regression:
PlannedGitCorpus_DirtyReindex53.6 → 62.0 ms (+16%),indexCommitted_incremental306 → 364 µs (+19%),indexUncommitted_1file26.3 → 27.8 ms (+6%),FolderCorpus_DirtyReindex_1File30.1 → 39.6 ms (+32%). - New steady-state:
GitCommitted_1CommitAhead96 ms,GitUncommitted_RapidEdits_N16912 ms (~57 ms/cycle),GitBranch_Switch_Unrelated79 ms (80% churn),SearchTombstoneCost859 µs over 16 delta shards.
Uncommitted is neutral on real repos: rebuilding one dirty file is dominated by ctags + Zoekt builder init (~30 ms), so manifest + per-shard probe overhead cancels the savings. Delta path still helps by keeping each shard small and bounding tombstone growth.
Development
- New
delta.goshared helpers,uncommitted_manifest.gofor state hashing. - 18 correctness tests covering modify/delete/rename tombstoning, HEAD rewind, rebase, branch switch, threshold fallback, manifest corruption, dirty-to-committed transitions, drift mid-build, concurrent search.
- 5 delta-focused benches plus
LargeRepo_CommittedAdvanceandLargeRepo_UncommittedRealistic(gated bySEEK_BENCH_REPO). gc.go,gc_cmd.go,gc_test.go(1217 lines test coverage), platform-specific NFS detection (nfs_darwin.go,nfs_linux.go,nfs_other.go).
Notes
- An earlier draft claimed "order-of-magnitude" committed-advance gain from extrapolation; actual measured speedup on kubernetes is 3.7×. A 20× number from prior runs came from a reflog oscillation bug (HEAD@{1} alternating between two distant positions).
LargeRepo_CommittedAdvancenow usesgit rev-list --reverse HEAD~10..HEADand asserts shard count grows underIsDeltaso silent fallback cannot pass.
Full changelog: v0.7.1...v0.8.0