Repository navigation
Description
Why
A repository's prefix is its folder name unless name is set. The prefix is the first segment of every node id. It is also the key of the live registry, the watcher map, the per-repo config cache and the resolver helper registry, and it is the input to the dedicated graph id. Nothing reserves a prefix. On main, every "already tracked?" check treats "this prefix is taken" as "this root is tracked" and never compares the roots. With /a/src tracked, running gortex track /b/src on main:
- reports success, and
--waittimes out; - replaces the owner's cached
.gortex.yaml, or deletes it if/b/srchas none; - can move the owner's checkout binding (owner checkout, family, primary slot) to
/b/srcwhen the owner has no worktree readers; - logs only
watcher already exists for repo: src.
After a restart, both entries race for src on parallel warmup workers. In one 30-restart review run, the wrong repo won 14 times. When it wins, the owner's corpus holds both trees' files. The loser also schedules a base publication for the owner's prefix. The obvious workaround, untracking the second path, can evict the owner's corpus. This is the likely cause of the "silently dropped" and "already tracked but not tracked" reports from people with two clones of one project. It also hits monorepos with many src, app or api folders. This is a safety fix and is never cut from the release.
Current state
Verified on origin/main:
internal/indexer/multi.goprepareTrackRepo:if _, exists := mi.repos[prefix]; exists { … return nil, nil // already tracked }. The same pattern appears in the in-lane recheck intrackPreparedRepoSourceCtxand at both checks inReconcileRepoCtx.internal/indexer/checkout_lifecycle.goRegister: whenTrackRepoCtxreturns nil, it falls back tol.ResolvePrefix(absPath)and then toconfig.ResolvePrefix(entry)(the folder name). It then callsrecordCheckoutand attaches the watcher for a root it never indexed.internal/indexer/checkout_promote.go: the error arms, including theUpsertDedicatedGraphfailure, callrollbackPromotionOrLog(ctx, out.Prefix, …), which purges whatever is registered under that prefix. Success is checked asresult == nil && l.mi.GetMetadata(prefix) == nil, so another root's metadata counts as success.cmd/gortex/daemon_state.go: a loser's nil reconcile falls back to the folder-name prefix and reachesstate.basePublisher.Schedule(indexed).cmd/gortex/daemon_controller.goTrackholdsc.mufor the whole track.internal/mcp/tools_multi.goanswers{status:"accepted"}near its deadline, so a collision found later only goes to the log.internal/indexer/repo_load_state.goalready reports a warmup job that ended unregistered asprefix_taken: … already registered to <owner>. That is only a status reason. It does not prevent the takeover.internal/indexer/multi_watcher.goproduces thewatcher already exists for repo: %serror.
Open PR #954 does most of the core. The maintainer approved it as a partial solution, to be merged once its merge conflict is resolved. It contains:
PrefixCollisionError(withOwnerMissing) andtrackedLocked, returned at both track checks;- resolution of live and configured owners before warmup. A skipped configured entry ends as failed, not untracked;
- a promotion guard: a collision leaves the transition pending and never purges the other root, and the next sweep retries under a suffixed prefix;
bindDedicatedGraphrefusing to move a binding owned by another checkout or family unless the binding is unpublished and abandoned. It also adds metadata-only repair of wrong legacy unpublished bindings;- workspace and gitignore reads moved outside
mi.mu; - a CLI non-zero exit with or without
--wait, which removes only the config entry this call added; - a config-only untrack for a root that is configured but not loaded;
--name/namerefusing to rename a path that is already configured;- MCP messages that name the
nameparameter; - the
prefix_collisioncode ininternal/daemon/proto.goandserver.go, and docs indocs/cli.mdanddocs/multi-repo.md.
What #954 does not cover:
- owner-root stats bounded and done outside
mi.muon the track path; - removal of
Register's folder-name fallback; - the typed warmup skip, so the loser no longer counts as changed and no longer schedules a base publication;
- a collision check that runs before the controller lock is taken;
- a stored outcome for deferred MCP tracks;
- a structured MCP error code;
- the probe arm.
What to do
- Land fix(indexer): reject tracking a second repository under a taken prefix #954. Rebase it on main, resolve the conflict and merge it. The PR measured +12% on reconcile at 1,000 repositories. Keep that within the no-regression target below.
- Follow-up PR with the remaining items:
- Stats outside
mi.mu. Stat the candidate root and the owner root before takingmi.mu(intrackedLocked, the same-directory identity check andPrefixCollisionErrorconstruction), with a 2 s bound. A timeout counts as "cannot stat". Under the lock, only re-check that the owner entry is unchanged, and retry once if it changed.PrefixCollisionError.Error()does no I/O. - Register. Never fall back to the folder name for a root that is not registered. If the track result is nil and no registered entry has the same root identity, return
PrefixCollisionErrorbeforerecordCheckout, the watcher attach and the config save. - Promotion. Count "registered under this prefix" as success only when the root is the same one. Apply the no-purge guard to every failure arm, including
UpsertDedicatedGraph. - Warmup loser (typed skip). At the reconcile sites, return a typed skip, not a bare nil. Warmup treats it as unchanged: nothing is added to
changedReposorscopeUnknown, the delta frontier is not invalidated, andbasePublisher.Scheduleis never called for the owner's prefix. The job is recorded asfailed{prefix_collision}. This covers both the warm reconcile arm and the cold track arm. - Control socket. Add a cheap collision pre-check before
c.mu.Lock()inrealController.Track, and before the goroutine starts inhandleTrackRepository. Use a registry snapshot, with the stats done outside any lock. - MCP deferred path. Keep each track goroutine's final outcome for 10 minutes, keyed by canonical root. A follow-up
track_repositoryfor the same root returns that outcome instead of running the track again. Collisions return a structured error witherror_code: prefix_collision,prefix,owner_pathandowner_state. - Auto-index.
internal/mcp/auto_index.gonever records an implicit checkout for a root whose prefix belongs to another root. - Messages. Log one warn line per collision, naming both roots and the action taken. No message recommends
untrackfor the colliding path. Suggest untracking the owner only when the owner root is missing. - Probe. A configured root whose prefix is registered to a different root answers
prefix_collision. It does not answerrepo_loading,repo_unavailableorrepo_not_tracked, and a re-track of that root does not answeralready_configured. - Keep fix(indexer): reject tracking a second repository under a taken prefix #954's same-directory no-ops: a different case or Unicode spelling, a symlink alias with the same folder name, and a directory already tracked under another name.
- Stats outside
Out of scope
- Automatic parent-qualified names such as
b-src. Collisions are rejected for now. - A full ownership plan with a persistent
conflictprobe state, and repair of corpora already mixed by earlier takeovers, beyond the metadata repair fix(indexer): reject tracking a second repository under a taken prefix #954 ships. - Renaming an already indexed corpus.
- Symlink aliases with a different folder name. These make a duplicate corpus, not a collision.
projects:-only entries.
Done when
Fixture: two git repos /T/a/src and /T/b/src. Each has a .gortex.yaml declaring a distinct workspace and exclude. /T/a/src has 2 linked worktrees.
- fix(indexer): reject tracking a second repository under a taken prefix #954 is merged to main (PR fix(indexer): reject tracking a second repository under a taken prefix #954, merge
1e6605c0cf7b9fad7bda3aa43b9c7eeb62dc7753, 2026-10-25). - With
/T/a/srctracked,gortex track /T/b/srcexits 1 within 1 s, with and without--wait. This holds on an idle controller and while another track holds the controller lock. Config is unchanged, and the error names/T/a/src. - After that track, and again with
/T/b/srchaving no.gortex.yaml, a restart, a config reload and 3 janitor sweeps, these owner properties are unchanged:- node and edge counts;
GetRepoConfig("src")(non-nil and unchanged) and its gitignore root;- the watcher root and the LSP helper root;
- the base-publication schedule;
- the dedicated graph binding: owner checkout, family, primary flag, active generation.
- Each collision produces exactly 1 warn line naming both roots, and 0
watcher already existslines. - With a stat hook that blocks forever on the owner root, the track returns within 2.5 s, and concurrent
GetMetadatap99 is at most 10 ms. - On a warm restart over the fixture, the loser adds nothing to
changedReposorscopeUnknown, never triggersbasePublisher.Schedule("src"), and is reported asfailed{prefix_collision}. Warmup takes the fast path when nothing else changed. - A promotion whose derived prefix is taken mid-build leaves the other corpus's node count unchanged.
- A second MCP
track_repositoryfor the same colliding root within 10 minutes returns the storedprefix_collisionoutcome, witherror_code,prefix,owner_pathandowner_state, and starts no new track. - These tests exist and pass under
go test -race, alongside fix(indexer): reject tracking a second repository under a taken prefix #954's tests:TestWarmupCollisionTypedSkipTestPromotionCollisionKeepsOtherCorpusTestRegisterCollisionNoCatalogNoWatcherNoSaveTestBusyControllerCollisionWithin1sTestMCPDeferredCollisionOutcomeTestHungOwnerStatBoundedTestAutoIndexCollisionNoRecordImplicit
- Re-tracking the same directory under another spelling is still a silent no-op, and the existing tests stay green.
- No regression against main, measured on a CI arm64 runner with 6 runs per side:
- the reconcile benchmark at 1,000 repositories is at most 15% slower;
- daemon warmup time and call-changing save time at 200 and 1,000 repositories show no statistically significant slowdown (benchstat p < 0.05).
-
docs/cli.md,docs/multi-repo.mdanddocs/mcp.mddescribe the unique-prefix rule, theprefix_collisionerror,--name/name, and the deferred MCP outcome.
How to verify
- Run
go test -race ./internal/indexer/... ./cmd/gortex/... ./internal/mcp/... ./internal/daemon/.... - Reproduce by hand on an isolated daemon:
- Run
git init /T/a/src /T/b/src, thengortex track /T/a/src --wait, thengortex track /T/b/src --wait. - Expect exit 1 within 1 s.
daemon.loghas one collision warn line and nowatcher already exists. - Restart the daemon 5 times. Each time,
search_textfor a marker string unique to each tree finds only/T/a/src's marker undersrc/.
- Run
- Compare the reconcile, warmup and call-changing save benchmarks against main with
benchstat, 6 runs per side.
Dependencies
Blocks:
Related issues
- Addresses: Tracked repo silently dropped from registry on every daemon restart (live worktree admin with unreadable HEAD) #831 (close those when the targets below pass and the reporters confirm)
- Related: Catalog primary graph keeps a stale repo_prefix: worktrees never get a coordinator, untrack fails forever with "cannot serve checkout … yet" #898
- PRs: fix(indexer): reject tracking a second repository under a taken prefix #954
Planning
- Milestone: v0.64.12
- Priority: release gate
- Estimate: ~4 engineer-days
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsDone
Activity
zzet commentedon Oct 28, 2026
Implementation is available in draft PR #1164, stacked on #1162. The final pushed commit passed the prescribed pinned lint/format/CGO/workflow/tidy checks, and all 116 retained focused controls have passing evidence with per-commit attribution; ownership repeats and final changed-expression checks also passed. The work remains In Progress: hosted CI, the isolated runtime scenarios and the six-run ARM64 performance comparisons are still pending.
The existing manual CI and skill-drift workflows were dispatched once because automatic PR workflows only target main. Manual CI falls back to full tests. Security/normal PR checks must also run after retargeting to main; no acceptance or merge is claimed.
zzet commentedon Oct 28, 2026
CI at the first draft head found regressions and fixture assumptions beyond the original focused checks. They are preserved in run 40678839397. The superseded manual run was canceled after its failed-job logs were saved.
PR #1164 now contains seven atomic corrections at
bc2a927fdc8059a2c0bc7bbbb9d43d7d923cecce: preserve missing-path errors; avoid reading workspace configuration during canonical membership lookup; register promotion publication with output-generation authority; avoid unnecessary naming discovery when a prefix is explicit; compare decoded Windows paths; distinguish collision from genuinely unavailable roots; and finish admitted cleanup before the replacement-owner fixture proceeds.All 38 focused race regression/neighbor tests passed locally, with zero skips, followed by the full prescribed pinned lint, formatting, CGO/workflow, tidy and module checks. Fresh hosted CI is running. Reference runtime and performance acceptance remain outstanding; this issue stays In Progress.
zzet commentedon Oct 28, 2026
The corrected head
bc2a927fdc8059a2c0bc7bbbb9d43d7d923ceccepassed the full hosted CI run, including every Linux, macOS and Windows test group and parser stress. Skill-drift checks also passed. The original failures remain recorded; this is a new validated head.Local validation at this head passed 38 focused race tests and all prescribed pinned lint, formatting, CGO/workflow, tidy and module checks. Runtime and performance acceptance, dependency integration and the eventual main-target PR checks remain open, so #1025 stays In Progress and PR #1164 stays draft.
zzet commentedon Nov 1, 2026
The code in #1164 is merged with exact-head correctness/security checks and a merge tree equal to the locally tested tree. GitHub automatically closed this issue through the PR link; reopening it because the recorded R3 and native/ARM64 runtime comparisons remain open. This merge is source readiness, and does not claim those measurements passed.
zzet commentedon Nov 3, 2026
Qualified performance acceptance is recorded following the maintainer’s explicit decision to accept the measured +3.32% (p=.002) and +1.61% (p=.026) call-changing save-time results with a profiling follow-up before v0.65.0.
The follow-up is #1301. It links the exact source pair, all 96 observations, build/host/tool pins and retained artifact from run 41579513305, and preserves the allocation observations and the unproven-cause limitation. The four warmup timing cells and the 1,000-repository reconcile bound passed separately.
This decision applies to those two measured save-time results only. It does not change unrelated criteria or establish an allocation threshold waiver. The original failed comparison and the significant results remain retained; no third cloud run was dispatched. Verification of the remaining functional, documentation and race criteria is being attached before issue closure. #831 still requires reporter confirmation separately.
zzet commentedon Nov 3, 2026
Closing #1025 with acceptance evidence complete in the selected scopes.
watcher already existsmessages.The qualified ARM64 comparison contains all 96 samples, six per side for each benchmark. Reconcile at 1,000 repositories stayed within the 15% limit, and all four warmup cells met the original significance rule. The user accepted only the two save-time regressions: +3.32% (p=.002) and +1.61% (p=.026). Their profiling follow-up, #1301, remains required before the v0.65.0 cut. Every allocation delta remains separately reported; the timing decision does not waive allocations or another criterion.
This closeout applies to #1025 only. It does not close #831 or declare the whole release accepted. Native raw evidence and provenance/cleanup receipts remain private.
The final native public case passed in 3.67 seconds and the private correctness supplement in 2.78 seconds, with no skipped tests or forced cleanup. Earlier controller, catalog and log-sink failures remain preserved; none is relabeled as a pass.