Skip to content

Fix file links failing to open across git worktrees#139

Open
yaacovcorcos wants to merge 2 commits into
mainfrom
agent/fix-file-link-worktree-resolution-20260726
Open

Fix file links failing to open across git worktrees#139
yaacovcorcos wants to merge 2 commits into
mainfrom
agent/fix-file-link-worktree-resolution-20260726

Conversation

@yaacovcorcos

Copy link
Copy Markdown
Contributor

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:

  1. Git worktree duplicates. The filesystem-walk index descended into linked git worktrees, so one logical file was indexed several times — the real checkout plus a copy under each .codex-worktrees/* worktree. The bare/partial-reference resolver saw multiple matches, treated the reference as ambiguous, and gave up; the read then surfaced the raw realpath ENOENT for the literal (non-existent) path the user never typed. The duplicates also inflated the index toward its 25k-entry cap, silently truncating it.
  2. Dishonest ambiguity. On a genuine ambiguity the resolver returned null, which the reader reported as ENOENT — a "file not found" message for a file that exists.

Fix (server-only)

  • Prune linked git worktree subtrees from the index walk. A linked worktree records its .git as a regular file pointer (vs. a primary repo's .git directory) — the reliable discriminator — so the walk stops at that boundary. .git entries 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.
  • Structured resolution. resolveWorkspaceFileBySuffix now returns resolved | ambiguous | unresolved instead of string | 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.
  • Truncation fallback. When the cached index is truncated (and therefore an unreliable source of truth), the resolver re-scans the tree directly with a bounded BFS for a trustworthy candidate set.
  • Honest error. A genuinely ambiguous reference now returns an actionable message listing the competing files and asking for a fuller path, instead of a false ENOENT. This reuses the existing WorkspaceFileSystemError tag, 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.ts and WorkspaceFileSystem.test.ts:

  • Linked git worktree subtrees are pruned from the index.
  • Unique bare basename and unique partial tail → resolved.
  • Basename matching multiple real files → ambiguous (sorted matches).
  • No match → unresolved.
  • Worktree-duplicate scenario resolves to the real file end-to-end.
  • Truncated index triggers a direct disk re-scan.
  • The reader surfaces an honest ambiguity message (asserts it names the competing files and does not contain "ENOENT").

Verification

bun fmt, bun lint, bun typecheck (9/9 packages), and the full apps/server Vitest suite (2370 passing) are green.

🤖 Generated with Claude Code

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>
@github-actions github-actions Bot added size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Jul 26, 2026
… 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant