Fix file links failing to open across git worktrees#139
Open
yaacovcorcos wants to merge 2 commits into
Open
Conversation
Bare/partial file references (e.g. a chat link to `kanbanDispatch.ts`) could fail to open with a misleading "no such file" error even though the file plainly exists. Two root causes: 1. The filesystem-walk index descended into linked git worktrees, so a single logical file was indexed multiple times (the real checkout plus each worktree copy). The suffix resolver treats multiple matches as ambiguous and gave up, and the read then surfaced the realpath ENOENT for the literal (non-existent) path. Indexing the duplicates also inflated the index toward its 25k-entry cap, truncating it. 2. On a genuine ambiguity the resolver returned null, which the reader reported as ENOENT -- a "file not found" message for a file that exists. Changes (server-only): - Prune linked git worktree subtrees from the index walk (a worktree records `.git` as a regular file pointer, the reliable discriminator), and skip `.git` entries whether file or directory. Extract the shared worktree/skip predicates. - Rework resolveWorkspaceFileBySuffix to return a structured result (resolved | ambiguous | unresolved). Resolve only on a single distinct match; report the competing paths on a true ambiguity instead of guessing between different files. - When the cached index is truncated (unreliable), re-scan the tree directly (bounded) for a trustworthy candidate set. - Surface an honest, actionable error listing the competing files instead of a false ENOENT. Reuses the existing WorkspaceFileSystemError tag, so no contract/client change. Tests: worktree pruning, unique/ambiguous/unresolved resolution, the worktree-duplicate scenario end-to-end, and the truncated-index disk re-scan. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… scan Follow-up correctness fixes to the worktree-aware file-link resolution. 1. Submodule false-prune. The worktree detector treated *any* directory whose `.git` is a regular file as a linked worktree and pruned it. Git submodules also store `.git` as a file, so their subtrees were pruned too -- silently hiding submodule files from search and link resolution. Discriminate by the `gitdir:` pointer target: prune only when it points into `.git/worktrees/<name>` (a linked worktree), never when it points into `.git/modules/<name>` (a submodule). Reading the pointer fails open (do not prune) so an unreadable `.git` can never hide real files. 2. Incomplete-scan false-unique. The bounded disk re-scan (used when the index is truncated) returned whatever it had found when it hit the scanned-directory cap. A single match found before the cap was then reported as a unique "resolved" even though a second match could sit in the unscanned remainder -- risking opening the wrong file. The scan now reports whether it completed, and classification fails closed to a new "indeterminate" outcome when an incomplete scan saw fewer than two matches. The reader surfaces an honest "too large to resolve, use a fuller path" error instead of guessing or falsely claiming ENOENT. Tests: submodule files stay resolvable and searchable (not pruned); the disk scan reports incomplete at its directory cap; classification fails closed (single match from an incomplete scan -> indeterminate, two distinct matches -> ambiguous even when incomplete, empty complete scan -> unresolved). The existing linked-worktree fixtures now use realistic `.git/worktrees/<name>` pointer contents. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Clicking a file link in chat (e.g. a bare reference like
kanbanDispatch.ts) could fail to open with a misleading "no such file" error even though the file plainly exists in the workspace. Two independent root causes combined to produce this:.codex-worktrees/*worktree. The bare/partial-reference resolver saw multiple matches, treated the reference as ambiguous, and gave up; the read then surfaced the rawrealpathENOENT for the literal (non-existent) path the user never typed. The duplicates also inflated the index toward its 25k-entry cap, silently truncating it.null, which the reader reported as ENOENT — a "file not found" message for a file that exists.Fix (server-only)
.gitas a regular file pointer (vs. a primary repo's.gitdirectory) — the reliable discriminator — so the walk stops at that boundary..gitentries are skipped whether file or directory. The worktree/skip predicates are extracted into shared helpers used by both the index build and the direct disk scan.resolveWorkspaceFileBySuffixnow returnsresolved | ambiguous | unresolvedinstead ofstring | null. It resolves only on a single distinct match and reports the competing paths on a true ambiguity — it never silently guesses between two different files that happen to share a basename.WorkspaceFileSystemErrortag, so no contract or client change is required.Why not "rank and pick the best match"
Deliberately avoided: silently choosing one of several distinct real files with the same basename would open the wrong file and hide the ambiguity. Reporting it is correct and predictable; the worktree pruning removes the false duplicates that caused the vast majority of these collisions in the first place.
Tests
Added coverage in
workspaceEntries.test.tsandWorkspaceFileSystem.test.ts:resolved.ambiguous(sorted matches).unresolved.Verification
bun fmt,bun lint,bun typecheck(9/9 packages), and the fullapps/serverVitest suite (2370 passing) are green.🤖 Generated with Claude Code