Repository navigation
Description
Why
Repos can show up as tracked even though nobody ran a track command for them. The daemon keeps one shared in-memory GlobalConfig, and every index path appends the indexed root to it. That includes roots nobody asked to track, such as GORTEX_AUTOINDEX=1 auto-index and promotion of an implicit checkout. Several daemon writes then dump the whole struct to ~/.gortex/config.yaml. On the next boot, Seed reads the leaked entry back as explicit configuration and creates a manual_config tracking intent for it. After that, nothing distinguishes it from a repo the user configured. The baseline release v0.64.10 has this defect.
The implicit path can even cause its own leak. RecordImplicit calls reconcileFamilyNow, and that saves the config whenever the family has a removed worktree. From then on, every leaked root is indexed on each boot, which costs warmup time, disk and memory. This issue stops new leaks. Cleaning up roots that already leaked is tracked separately.
Current state (origin/main)
internal/indexer/multi.go:TrackRepoCtx(around :3162) andReconcileRepoCtx(around :3434) callmi.configMgr.Global().AddRepo(entry)for every origin. The origin is only used as a log label (WithIndexOrigin).internal/indexer/checkout_lifecycle.go:Register(:729) relies on that append, then callssaveConfig("track")(:782).RecordImplicit(:802) says the list is "deliberately NOT persisted". It still callsreconcileFamilyNow(:1900), which runssaveConfig("reconcile")when a family member was removed (:1914).sweepdumps the config withsaveConfig("janitor")(:1788). The comment above it (:1786) describes an older design.Seeddumps the config withsaveConfig("reconcile")(:4168).
internal/indexer/checkout_overview.go:710has one moresaveConfig("reconcile")dump.internal/indexer/checkout_promote.go: promotion callsTrackRepoCtxand saves (saveConfig("promote"), :231) even whensource == TrackSourceImplicit. The source is already checked for the intent at :115.- Other writers that save the whole struct:
set_active_project(internal/mcp/tools_multi.go:306),(*GlobalConfig).RemoveRepoAndSaveIfPresent(internal/config/global.go:607) andhealDuplicateRepos(cmd/gortex/daemon_state.go:387). - Every config removal the daemon makes already saves itself through
RemoveRepoAndSaveIfPresent. The janitor and reconcile dumps only re-dump memory, and they are the main carriers of the leak. - The existing tests
TestCheckoutLifecycleImplicitRegistrationWritesNoIntentandTestCheckoutLifecycleImplicitRegistrationSurvivesRestartWithoutIntent(internal/indexer/checkout_lifecycle_test.go:1059,:1092) never run a dump between the implicit index and the restart, so they do not catch the leak. ApplyReload(:1578) retires every live root that is not ingc.Repos. Once implicit roots stop going into the config, a reload would retire them unless reload learns to skip them.- Open PR fix(indexer): reject tracking a second repository under a taken prefix #954 (prefix collision) touches
TrackRepoCtxand the CLItrack.go. It is not merged.
What to do
-
Remove the
AddRepocalls fromTrackRepoCtxandReconcileRepoCtx. Return the resolved entry (absolute path plus pinnedName) onIndexResult, for example asIndexResult.Entry config.RepoEntry. -
In
CheckoutLifecycle.Register, callAddRepo(entry)beforesaveConfig("track")for every explicit source:- use
result.Entry; - when
result == nil(the repo is already tracked), use the entry with its pinned name.
This turns "explicitly track a repo that was auto-indexed earlier" into a deliberate adoption. Persist only after the prefix-collision rejection.
- use
-
In promotion, add the entry to the config only when
source != TrackSourceImplicit. -
Delete these four dumps:
saveConfig("janitor")insweep;saveConfig("reconcile")inreconcileFamilyNow;saveConfig("reconcile")inSeed;saveConfig("reconcile")incheckout_overview.go.
Keep
notifyTrackedSetChangedandnotifyFamilyChanged. Fix the stale janitor comment. -
Make
ApplyReloadskip retiring a live root that is known to be implicit. That means a checkout with no active tracking intent, or a root thatRecordImplicitregistered in this process. Without this, the change above would make a reload drop auto-indexed repos. If fix(indexer): reject tracking a second repository under a taken prefix #954 lands first with the same behaviour, reuse it and add only the test. -
Add a static guard test,
TestNoIndexerConfigWrites, built ongo/types. It fails when(*config.GlobalConfig).AddRepois called from anywhere outside this allowlist:CheckoutLifecycle.Register;- promotion;
cmd/gortex/track.go;cmd/gortex/init.go;cmd/gortex/mcp.go(embedded--track, until a later change).
Watcher
AddRepomethods are a different method and must not trip the guard. -
Add a table-driven regression test in
internal/indexerthat usesnewLifecycleFixture:- Write a config with repo A and its linked worktree, a project containing A, a comment and an unknown key.
- Auto-index an untracked repo X: call
TrackRepoCtxwith originmcp_auto_index, thenRecordImplicit. - Run one config writer per case (8 cases): implicit-self (X has a removed worktree), track another repo, janitor sweep, family reconcile, seed reconcile after restart, set active project, untrack another repo, untrack during warmup.
- Assert the file has no entry for X.
- Restart, run
Seed, and assert X has no tracking intent.
-
Add
TestHealDuplicateReposRemovesOnlyDuplicateincmd/gortex. -
If fix(indexer): reject tracking a second repository under a taken prefix #954 merges first, rebase onto it and keep its CLI rollback of a just-added entry.
Out of scope
- Removing entries that already leaked. The doctor listing and the
--applycleanup are separate issues. - Config writes that preserve comments, and a config lock shared across processes.
- Durable provenance ("who tracked this"), and showing it in
repos families/list_checkouts. - A lifecycle-owned implicit set, a full redesign of reload, and no longer persisting embedded
gortex mcp --track. These come in a later release. - Changing the agent-facing "run
gortex track" advice, or whethergortex initpersists.
Done when
- The 8-case regression test fails in all 8 cases on ce04b3e (the merge commit of Improve incremental indexing and resumable contract analysis #806) and passes in all 8 cases on the branch.
-
TestTrackRepoCtxDoesNotMutateConfig,TestRegisterAdoptsImplicitRepo,TestRemovalsPersistWithoutSweepSave,TestNoIndexerConfigWritesandTestHealDuplicateReposRemovesOnlyDuplicateexist and pass undergo test -race. - A test shows that
ApplyReloadkeeps an implicitly indexed root live. - Validation workload: an isolated daemon with 3 repos and 20 worktrees, autostarted from an untracked repo with
GORTEX_AUTOINDEX=1, followed by 15 minutes of worktree add/remove churn and a graceful restart. After the workload and again after the restart:config.yamlcontains 0 implicit roots;- the auto-indexed checkout has 0 tracking intents.
- During that 15-minute churn, the daemon writes
config.yaml0 times with an empty diff: the mtime does not change unless an entry was intentionally added or removed. - No regression:
- entries added by explicit
gortex track, MCP track and reload still persist; - janitor and reconcile removals are still on disk after a restart;
- the existing
internal/indexerandcmd/gortextests pass.
- entries added by explicit
How to verify
- Run
go test -race ./internal/indexer/ -run 'NoIndexerConfigWrites|TrackRepoCtxDoesNotMutateConfig|RegisterAdoptsImplicitRepo|RemovalsPersistWithoutSweepSave|AutoIndexThenDumpWriter|ApplyReload'andgo test -race ./cmd/gortex/ -run HealDuplicateRepos. - To prove the defect, cherry-pick only the regression test onto ce04b3e and confirm that all 8 cases fail.
- Manual run:
- Start an isolated daemon with
GORTEX_AUTOINDEX=1from an untracked repo X that has a removed linked worktree. - Make one tool call, run
gortex trackon another repo, then restart the daemon. - Check that
grep X ~/.gortex/config.yamlprints nothing. - Check that
sqlite3 <store> "select c.root_path, i.source_kind from tracking_intents i join checkouts c using(checkout_id)"returns no row for X.
- Start an isolated daemon with
- During churn, record the mtime with
stat -f %m ~/.gortex/config.yaml(macOS) orstat -c %Y(Linux), and the hash withshasum -a 256, before and after.
Dependencies
Blocked by (must land first):
- Reject a second repository whose folder name is already tracked, on every path (land #954) #1025 — Not persisting implicit roots needs the same-basename collision fix first.
Related issues
- Related: Tracked repo silently dropped from registry on every daemon restart (live worktree admin with unreadable HEAD) #831
- PRs: fix(indexer): reject tracking a second repository under a taken prefix #954
Planning
- Milestone: v0.64.17
- Priority: disk and memory (second priority)
- Estimate: ~3 engineer-days
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsTodo
Activity