Skip to content

feat(local-mount): exclude .npm-cache from initial walk by default#101

Merged
willwashburn merged 4 commits into
mainfrom
feat/local-mount-default-exclude-npm-cache
May 8, 2026
Merged

feat(local-mount): exclude .npm-cache from initial walk by default#101
willwashburn merged 4 commits into
mainfrom
feat/local-mount-default-exclude-npm-cache

Conversation

@willwashburn

Copy link
Copy Markdown
Member

Summary

  • Add .npm-cache to DEFAULT_EXCLUDED_DIRS so the initial mount walk skips it the same way it already skips .git and node_modules.
  • Project-local npm caches (created when npm/npx run with npm_config_cache=./.npm-cache, or via tooling like prpm / skill.sh that keep their cache out of ~/.npm) routinely accumulate tens of thousands of files and hundreds of megabytes — none of which belong inside an agent's mount.
  • On a real downstream repo (agentworkforce/workforce), this directory alone held 14,745 files / 399 MB and added ~5s of copyFileSync overhead to every mount creation. Combined with other in-repo caches, the user-visible "starting sandbox mount → installing skills" gap was 30–60s, all of which is wasted work.

Why this is the right default

The excludeDirs option is purely additive — there is no API to opt back in. So adding to the defaults is only safe for directories that are unambiguously cache, never source-of-truth. .npm-cache:

  • is created by package managers, not by humans;
  • is not tracked in version control (it appears in many .gitignores);
  • has no semantic role agents need to interact with — its contents are a content-addressed blob store keyed by hash;
  • already has precedent in this repo: auto-sync.ts lists .git, node_modules, dist, build, .next, .cache as "heavy" dirs the watcher avoids recursing into when they're excluded. .npm-cache fits the same shape.

Callers who specifically need an npm cache visible inside the mount can point npm at a different location (e.g. npm_config_cache=$HOME/.npm — the conventional default) or populate one post-mount from onBeforeLaunch.

Test plan

  • npm test in packages/local-mount — 30/30 pass, including the updated excludes .git, node_modules, and .npm-cache by default regression test.
  • npm run typecheck in packages/local-mount — clean.
  • Verify downstream @agentworkforce/cli mount creation is faster on a repo with .npm-cache populated (after npm publish of the next release).

🤖 Generated with Claude Code

Project-local npm caches (created when `npm`/`npx` run with
`npm_config_cache=./.npm-cache` or via tools like prpm/skill.sh that
keep their cache out of `~/.npm`) routinely accumulate tens of
thousands of files and hundreds of megabytes — none of which belong
inside an agent's mount. On a real workforce repo this directory alone
added ~5s of `copyFileSync` overhead to mount creation per launch.

`DEFAULT_EXCLUDED_DIRS` now lists `.npm-cache` alongside `.git` and
`node_modules`. The auto-sync watcher's existing `candidates` list
already recognized common cache trees (`dist`, `build`, `.next`,
`.cache`) as ones to skip when they're excluded — `.npm-cache` fits
the same shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@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: 684fd3ff-e33b-439b-acf7-d827156fc532

📥 Commits

Reviewing files that changed from the base of the PR and between 511647d and b6f1f17.

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

📝 Walkthrough

Walkthrough

The local-mount package now excludes the .npm-cache directory by default from mounts, in addition to .git and node_modules. Implementation and autosync ignore generation were updated, tests verify the exclusion, and README/CHANGELOG document the change and workarounds.

Changes

npm-cache Default Exclusion

Layer / File(s) Summary
Core Implementation
packages/local-mount/src/mount.ts
DEFAULT_EXCLUDED_DIRS now includes .npm-cache; createMount passes resolved excludedNames into the auto-sync context.
Autosync Ignore List
packages/local-mount/src/auto-sync.ts
AutoSyncContext adds excludedNames; buildIgnoreGlobs generates watcher ignore patterns from ctx.excludedNames rather than a hardcoded probe list; startAutoSync computes globs per subscription root.
Test Coverage
packages/local-mount/src/mount.test.ts
Default-exclusion test updated to create a file under .npm-cache and verify it is not present in the mounted output while .relay remains included.
Documentation & Changelog
packages/local-mount/README.md, packages/local-mount/CHANGELOG.md
README documents .npm-cache as excluded by default; CHANGELOG adds a Changed entry describing the exclusion and autosync behavior and suggests workarounds (use a different npm cache path or populate cache after mounting).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hopped by the cache with a curious sniff,
Skipped .npm-cache where contents are stiff,
Mounts cleaner now, tests clap in cheer,
Docs whisper the reason, changelog is clear—
I nibble a carrot and vanish, adrift.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 clearly and specifically describes the main change: adding .npm-cache to the default excluded directories in the local-mount package, matching the primary objective of the changeset.
Description check ✅ Passed The description is detailed and directly related to the changeset, explaining the rationale, performance impact, and safety of adding .npm-cache to defaults, with test plan confirmation.
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 feat/local-mount-default-exclude-npm-cache

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

@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 1 potential issue.

⚠️ 1 issue in files not directly in the diff

⚠️ .npm-cache missing from buildIgnoreGlobs candidates, so @parcel/watcher still recurses into it (packages/local-mount/src/auto-sync.ts:241)

.npm-cache was added to DEFAULT_EXCLUDED_DIRS in mount.ts:60, but the buildIgnoreGlobs function in auto-sync.ts:241 has a separate hardcoded candidates list that was not updated. This means @parcel/watcher will still subscribe to and recurse into .npm-cache directories, generating events for every file inside them. Those events are filtered by isSyncCandidate, so correctness is preserved, but the entire performance benefit of excluding .npm-cache from the watcher is lost — the PR's own changelog states these directories contain "tens of thousands of files (often hundreds of MB)."

View 2 additional findings in Devin Review.

Open in Devin Review

`buildIgnoreGlobs` keeps its own `candidates` list separate from
`DEFAULT_EXCLUDED_DIRS` so callers can extend exclusions via
`excludeDirs` without forcing introspection of the underlying Set.
The previous commit added `.npm-cache` to the defaults but missed
this list, so `@parcel/watcher` still subscribed to every file
underneath. Correctness was preserved by `isSyncCandidate`, but the
events were being produced and filtered, undoing most of the perf
benefit on real caches with tens of thousands of files.

Add `.npm-cache` to the watcher candidates and document the contract:
anything in `DEFAULT_EXCLUDED_DIRS` MUST appear in this list too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@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.

🧹 Nitpick comments (1)
packages/local-mount/src/auto-sync.ts (1)

241-246: ⚡ Quick win

Use one source of truth for default excluded dirs.

This fixes .npm-cache, but Line 241–246 still leaves room for future drift. Pull DEFAULT_EXCLUDED_DIRS from a shared export and build candidates from it (or move both to a common constants module if importing from mount.ts creates a cycle).

♻️ Proposed refactor
+import { DEFAULT_EXCLUDED_DIRS } from './mount'; // or from shared constants module

 function buildIgnoreGlobs(ctx: AutoSyncContext): string[] {
   const globs: string[] = [];
-  const candidates = ['.git', 'node_modules', '.npm-cache', 'dist', 'build', '.next', '.cache'];
+  const candidates = [...DEFAULT_EXCLUDED_DIRS, 'dist', 'build', '.next', '.cache'];
   for (const name of candidates) {
     if (ctx.isExcluded(name)) {
       globs.push(`**/${name}`, `**/${name}/**`);
     }
   }
   return globs;
 }
🤖 Prompt for 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.

In `@packages/local-mount/src/auto-sync.ts` around lines 241 - 246, The hard-coded
candidates array in auto-sync.ts can drift from the shared
DEFAULT_EXCLUDED_DIRS; replace the literal list by importing
DEFAULT_EXCLUDED_DIRS (or moving both into a shared constants module if import
would create a cycle) and build const candidates =
Array.from(DEFAULT_EXCLUDED_DIRS) (or derive it as needed) so auto-sync.ts uses
the single source of truth; update any references to candidates in functions
like the initial walk/watcher setup to use the derived array.
🤖 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.

Nitpick comments:
In `@packages/local-mount/src/auto-sync.ts`:
- Around line 241-246: The hard-coded candidates array in auto-sync.ts can drift
from the shared DEFAULT_EXCLUDED_DIRS; replace the literal list by importing
DEFAULT_EXCLUDED_DIRS (or moving both into a shared constants module if import
would create a cycle) and build const candidates =
Array.from(DEFAULT_EXCLUDED_DIRS) (or derive it as needed) so auto-sync.ts uses
the single source of truth; update any references to candidates in functions
like the initial walk/watcher setup to use the derived array.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bbe6d393-666d-419d-9355-cc62f22ee086

📥 Commits

Reviewing files that changed from the base of the PR and between 868874d and 43ee972.

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

The previous fix added .npm-cache to a hardcoded `candidates` list in
buildIgnoreGlobs, with a doc-block telling future contributors that
DEFAULT_EXCLUDED_DIRS additions MUST also be mirrored there. That kind
of comment-enforced invariant is exactly what eventually drifts.

Replace the doc-block contract with a structural one: AutoSyncContext
now carries `excludedNames` — the spread of the same Set that drives
`isExcluded` — and `buildIgnoreGlobs` iterates that directly. The
hardcoded probe list goes away entirely, and the two call sites can no
longer disagree because there is only one list.

This also fixes a latent bug: user-supplied `excludeDirs` entries
(e.g. `excludeDirs: ['my-custom-cache']`) are now hinted to
@parcel/watcher in addition to being skipped during the initial walk.
Previously they were skipped from the walk but still subscribed to,
producing events the in-handler filter had to throw away.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@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/auto-sync.ts`:
- Around line 249-251: The watcher ignore globs currently prefix every
ctx.excludedNames entry with "**/" which over-matches path-based excludes
compared to isExcludedPath; update the loop that builds globs so it checks each
name: if the exclude contains a slash (e.g., name.includes('/')) treat it as a
path-based exclude and add root-anchored patterns (name and name + "/**") or
skip the "**/" prefix, otherwise continue to add the "**/" and "**/.../**"
variants for simple directory names; update the code that builds globs in
auto-sync.ts (the loop referencing ctx.excludedNames) to implement this
conditional handling so watcher globs align with isExcludedPath semantics.
🪄 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: f9924b14-4f58-47d0-946b-e21be3ba9c26

📥 Commits

Reviewing files that changed from the base of the PR and between 43ee972 and 511647d.

📒 Files selected for processing (3)
  • packages/local-mount/CHANGELOG.md
  • packages/local-mount/src/auto-sync.ts
  • packages/local-mount/src/mount.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/local-mount/CHANGELOG.md

Comment thread packages/local-mount/src/auto-sync.ts
Switching `buildIgnoreGlobs` to iterate the live `excludeSet` exposed
a semantic gap that was unreachable when the probe list was
hardcoded-bare-names: path-style entries like `build/cache` mean
"only the root-anchored prefix" in `isExcludedPath`, but the loop
was unconditionally prefixing `**/`, producing globs that swallow
deep matches like `src/build/cache/foo.txt`. The watcher would drop
those events even though the in-handler filter would have synced
them — a real correctness regression for callers whose excludeDirs
include slashes.

Make `buildIgnoreGlobs` per-root and emit the shape that matches the
canonical predicate:
  - bare names → `**/<name>` + `**/<name>/**` (any depth)
  - path-style → `<watchRoot>/<name>` (literal absolute → ignorePaths)
                + `<watchRoot>/<name>/**` (anchored descendant glob)

Library defaults (`.git`, `node_modules`, `.npm-cache`) are all bare
names, so released behavior is unchanged. The fix is required because
the API allows path-style entries and `isExcludedPath` honors them,
so the watcher hint must agree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@willwashburn willwashburn merged commit 415457a into main May 8, 2026
7 checks passed
@willwashburn willwashburn deleted the feat/local-mount-default-exclude-npm-cache branch May 8, 2026 12:40
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