Skip to content

Speed up local mount initialization#103

Merged
willwashburn merged 4 commits into
mainfrom
issue-102-local-mount-init-speed
May 8, 2026
Merged

Speed up local mount initialization#103
willwashburn merged 4 commits into
mainfrom
issue-102-local-mount-init-speed

Conversation

@willwashburn

@willwashburn willwashburn commented May 8, 2026

Copy link
Copy Markdown
Member

Summary

  • trim redundant destination safety work by returning the verified real parent from ensureDirectoryWithinRoot
  • expand default excludes for common build/cache/venv output and add includeDefaultExcludeDirs: false for callers that need those paths
  • split default excludes so generic names like build, env, and coverage only match at the project root
  • apply the same default-exclude filtering during manual syncBack()
  • keep .agentreadonly files as copied 0o444 files so direct mount writes still fail instead of mutating source files
  • document the expanded default exclude list

Refs #102.

Verification

  • npm run test --workspace=packages/local-mount (passed outside sandbox; sandboxed run cannot start macOS FSEvents)
  • one earlier full-suite run hit the existing abort timing flake in syncBack: returns a partial count when aborted mid-walk; rerun passed unchanged
  • npm run typecheck (after npm run build --workspace=packages/core in the fresh worktree)
  • npm run build --workspace=packages/local-mount

No HTTP server changes, so scripts/check-contract-surface.sh was not needed.

@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cd93d977-5472-4b8e-934d-ea1b7e17e1a9

📥 Commits

Reviewing files that changed from the base of the PR and between 7b11430 and 7b5f20b.

📒 Files selected for processing (2)
  • packages/local-mount/src/mount.test.ts
  • packages/local-mount/src/mount.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/local-mount/src/mount.test.ts

📝 Walkthrough

Walkthrough

This PR adds MountOptions.includeDefaultExcludeDirs to opt out of an expanded built-in default exclusion list, splits exclusion rules into any-depth vs root-prefix sets used for watcher globs and tree traversal, refactors the tree walk to track mount paths and return resolved directories, and updates tests and README accordingly.

Changes

Parameterized Default Excludes and Tree Walk Refactor

Layer / File(s) Summary
Public API
packages/local-mount/src/mount.ts, packages/local-mount/src/launch.ts
MountOptions and LaunchOnMountOptions add optional includeDefaultExcludeDirs?: boolean to control whether the expanded built-in exclude list is applied.
Exclusion Rule Representation
packages/local-mount/src/mount.ts, packages/local-mount/src/auto-sync.ts
Default excludes refactored into excludedAnyDepthNames and excludedRootPrefixes; createExcludeRules, addExcludeEntries, and isExcludedPath implement new semantics; buildIgnoreGlobs updated to emit appropriate watcher patterns.
CreateMount Composition
packages/local-mount/src/mount.ts
createMount composes active exclude rules from defaults and caller excludeDirs according to includeDefaultExcludeDirs and includeGit.
Tree Walk & Resolution
packages/local-mount/src/mount.ts
walkProjectTree threads currentMountDir, computes per-entry mountPath, ensureDirectoryWithinRoot now returns `string
Integration
packages/local-mount/src/launch.ts
launchOnMount forwards includeDefaultExcludeDirs into the createMount configuration.
Tests
packages/local-mount/src/mount.test.ts
Default-exclude tests expanded to cover many cache/build/output dirs; test added for includeDefaultExcludeDirs: false; readonly mount assertions extended.
Documentation
packages/local-mount/README.md
Added "Default Excludes" section with full list and opt-out docs; clarified excludeDirs matching semantics; example updated to use vendor-cache.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • AgentWorkforce/relayfile#101: Related prior change that introduced excludedNames and ignore-glob derivation; this PR continues refactoring that area.

Poem

🐰 I nibble paths and skip the heap,

Default excludes tidy where caches sleep,
Mounts map roots and guard the deep,
Tests hop by to prove each sweep,
I bound back home when reviews are complete.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Speed up local mount initialization' accurately reflects the primary optimization goal of returning verified real parent from ensureDirectoryWithinRoot to avoid redundant destination safety work during mount initialization.
Description check ✅ Passed The description is directly related to the changeset, detailing multiple improvements including exclusion list expansion, default exclude filtering, readonly file handling, and documentation updates that align with the code changes across all five modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-102-local-mount-init-speed

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f7260e2aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/local-mount/src/mount.ts Outdated
Comment on lines +584 to +588
if (opts.targetDirectoryReady) {
if (!isPathWithinRoot(parentPath, rootPath)) {
return null;
}
return path.join(parentPath, path.basename(candidatePath));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Revalidate mount parent with realpath before file writes

When targetDirectoryReady is true, this branch returns a path after only a string-prefix check and skips resolving the parent directory with realpathSync. If another process replaces a previously validated mount subdirectory with a symlink between directory creation and file copy, later copyFileSync/linkSync calls can follow that symlink and write outside mountDir. The previous implementation revalidated each target parent via realpath on every file copy, so this change weakens the containment guarantee under concurrent filesystem mutation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7b11430. I removed the targetDirectoryReady fast path, so resolveSafeCopyTarget always goes back through ensureDirectoryWithinRoot and uses the realpath-verified parent before copyFileSync writes.

Comment thread packages/local-mount/src/mount.ts Outdated
Comment on lines +604 to +606
const code = (err as NodeJS.ErrnoException).code;
if (code === 'EXDEV' || code === 'EPERM') {
return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fallback to copy on unsupported hardlink errors

The hardlink fallback currently handles only EXDEV and EPERM; all other link failures are rethrown. On filesystems where hardlinks are unsupported or limited (for example returning ENOTSUP/EOPNOTSUPP/EMLINK), mount creation will now fail for readonly files instead of degrading to the existing copy path. Since this commit advertises hardlinking as an optimization with fallback behavior, these error codes should also trigger the copy fallback to avoid environment-specific breakage.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 1487757 by removing the readonly hardlink optimization entirely. Readonly files are back to copied 0o444 files, so this fallback path no longer exists.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment thread packages/local-mount/src/mount.ts Outdated
Comment on lines +67 to +89
const DEFAULT_EXCLUDED_DIRS = [
'.git',
'node_modules',
'.npm-cache',
'target',
'.next',
'dist',
'build',
'out',
'__pycache__',
'.pytest_cache',
'.mypy_cache',
'.ruff_cache',
'.venv',
'venv',
'env',
'.gradle',
'coverage',
'.nyc_output',
'.turbo',
'.cache',
'.DS_Store',
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Generic default exclude names match at any path depth, silently excluding nested source directories

The newly added default excludes (target, dist, build, out, env, venv, coverage) are matched as bare path segments at any depth by isExcludedPath (packages/local-mount/src/mount.ts:558-566), which checks excludeSet.has(segment) for every segment of a relative path. This means a project with nested source directories like src/build/, packages/env/, lib/out/, or src/target/ would have those entire subtrees silently excluded from the mount. The old defaults (.git, node_modules, .npm-cache) were unambiguous names that never appear as source directories; the new generic names are common in legitimate source paths. The tests only verify root-level exclusion, masking this issue.

Concrete example of silently excluded source files

Running the exclusion check against nested paths:

  • src/build/helper.ts → excluded (segment build matches)
  • packages/env/config.ts → excluded (segment env matches)
  • lib/out/formatter.ts → excluded (segment out matches)
  • src/target/arm64.rs → excluded (segment target matches)
  • test/coverage/reporter.ts → excluded (segment coverage matches)

All of these are legitimate source paths that would be missing from the mount.

Prompt for agents
The DEFAULT_EXCLUDED_DIRS list now contains generic names like 'target', 'dist', 'build', 'out', 'env', 'venv', 'coverage' that are matched at any depth inside isExcludedPath (mount.ts:558-566) because it checks excludeSet.has(segment) for each path segment. This means src/build/helper.ts is excluded because the 'build' segment matches, even though only root-level build/ directories should be excluded.

The fix should make isExcludedPath distinguish between root-level-only and any-depth matching. One approach: entries without a '/' prefix are currently matched at any depth. You could either:

1. Change isExcludedPath to only match the first segment (root-level) for bare names, while keeping prefix-path entries (those with '/') matched as root-anchored prefixes. This would be a breaking change for the existing behavior with node_modules (which should match at any depth in monorepos).

2. Split the exclude set into two categories: names that should match at any depth (like .git, node_modules, .npm-cache, __pycache__) and names that should only match at root level (like build, dist, out, env, target, coverage, venv). The isExcludedPath function would check each category differently.

3. Use root-anchored path-style entries for the generic names in DEFAULT_EXCLUDED_DIRS. E.g. instead of 'build', use 'build/' (or some convention) that isExcludedPath treats as root-only. The buildIgnoreGlobs function in auto-sync.ts would also need updating to match.

Also update the tests to verify that nested paths like src/build/file.ts are NOT excluded.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7b11430. The exclude rules are now split into any-depth names and root-anchored prefixes; generic defaults like build, env, out, target, and coverage are root-only. I added regression coverage for src/build, packages/env, lib/out, src/target, and test/coverage staying mounted.

Comment thread packages/local-mount/src/mount.ts Outdated
Comment on lines +604 to +606
const code = (err as NodeJS.ErrnoException).code;
if (code === 'EXDEV' || code === 'EPERM') {
return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 hardlinkMountedFile re-throws on EMLINK and ENOSYS instead of falling back to copy

hardlinkMountedFile only catches EXDEV (cross-device) and EPERM (permission) errors, re-throwing on all others. If the source inode has reached the filesystem's maximum hardlink count (EMLINK — e.g. 65,000 on ext4), or if the filesystem doesn't support hardlinks at all (ENOSYS/EOPNOTSUPP — some FUSE/network mounts), the function throws instead of returning false to trigger the copy fallback. Since this is called inside copyMountedFile during walkProjectTree, an unhandled throw would abort mount creation entirely.

Suggested change
const code = (err as NodeJS.ErrnoException).code;
if (code === 'EXDEV' || code === 'EPERM') {
return false;
if (code === 'EXDEV' || code === 'EPERM' || code === 'EMLINK' || code === 'ENOSYS' || code === 'EOPNOTSUPP') {
return false;
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 1487757 by removing hardlinkMountedFile and the readonly hardlink path. Readonly files now use the original copied 0o444 behavior again.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/local-mount/src/mount.ts`:
- Around line 574-596: The fast-path that skips realpath resolution when
opts.targetDirectoryReady is true in resolveSafeCopyTarget is unsafe; revert to
always performing the realpath check by removing the early branch that returns
path.join(parentPath, path.basename(candidatePath)) and instead call
ensureDirectoryWithinRoot(rootPath, parentPath) in all cases (preserving the
existing null checks), so the function always uses the resolved realParent
before returning the final path; keep references to isPathWithinRoot,
ensureDirectoryWithinRoot, and the caller expectations around
copyFileSync/linkSync intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3d93eb13-c792-45cf-8b27-d63d4355827f

📥 Commits

Reviewing files that changed from the base of the PR and between 5b427af and 3f7260e.

📒 Files selected for processing (6)
  • packages/local-mount/README.md
  • packages/local-mount/src/auto-sync.test.ts
  • packages/local-mount/src/auto-sync.ts
  • packages/local-mount/src/launch.ts
  • packages/local-mount/src/mount.test.ts
  • packages/local-mount/src/mount.ts

Comment thread packages/local-mount/src/mount.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/local-mount/src/mount.ts`:
- Around line 162-170: syncBack() currently ignores excludeRules and only
respects readonly/ignored/noSyncBack, causing generated dirs (dist/, build/,
node_modules/) to be copied back; update syncBack() to apply the same exclusion
logic used for autosync by reusing the excludeRules-derived helpers (the
isExcludedPath wrapper used in autoSyncContext,
excludedAnyDepthNames/excludedRootPrefixes or the isExcluded function) or accept
the autoSyncContext so syncBack() calls isExcluded(relPosix) alongside the
existing isPathMatched checks
(readonlyMatcher/ignoredMatcher/noSyncBackMatcher), ensuring files matched by
excludeRules are skipped during manual syncBack().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7d4cb5c0-69f7-4aa4-9b24-2b068432d82a

📥 Commits

Reviewing files that changed from the base of the PR and between 1487757 and 7b11430.

📒 Files selected for processing (4)
  • packages/local-mount/README.md
  • packages/local-mount/src/auto-sync.ts
  • packages/local-mount/src/mount.test.ts
  • packages/local-mount/src/mount.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/local-mount/README.md
  • packages/local-mount/src/mount.test.ts

Comment thread packages/local-mount/src/mount.ts
@willwashburn willwashburn merged commit 62f1400 into main May 8, 2026
7 checks passed
@willwashburn willwashburn deleted the issue-102-local-mount-init-speed branch May 8, 2026 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant