From e7953d816432c9c785ebac3e9b5ea9eb5ecc482d Mon Sep 17 00:00:00 2001 From: Roshan Singh Date: Sat, 17 Jan 2026 06:26:27 +0000 Subject: [PATCH] Fix #1056: ignore heavy paths in skills watcher On macOS, watching deep dependency trees can exhaust file descriptors and lead to spawn EBADF failures. The skills watcher only needs to observe skill changes, so ignore dotfiles, node_modules, and dist by default. Adds regression coverage. --- src/agents/skills/refresh.test.ts | 30 ++++++++++++++++++++++++++++++ src/agents/skills/refresh.ts | 7 +++++++ 2 files changed, 37 insertions(+) create mode 100644 src/agents/skills/refresh.test.ts diff --git a/src/agents/skills/refresh.test.ts b/src/agents/skills/refresh.test.ts new file mode 100644 index 000000000..b4091ed76 --- /dev/null +++ b/src/agents/skills/refresh.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it, vi } from "vitest"; + +const watchMock = vi.fn(() => ({ + on: vi.fn(), + close: vi.fn(async () => undefined), +})); + +vi.mock("chokidar", () => { + return { + default: { watch: watchMock }, + }; +}); + +describe("ensureSkillsWatcher", () => { + it("ignores node_modules and dist by default", async () => { + const mod = await import("./refresh.js"); + mod.ensureSkillsWatcher({ workspaceDir: "/tmp/workspace" }); + + expect(watchMock).toHaveBeenCalledTimes(1); + const opts = watchMock.mock.calls[0]?.[1] as { ignored?: unknown }; + + expect(Array.isArray(opts.ignored)).toBe(true); + const ignored = opts.ignored as RegExp[]; + expect(ignored.some((re) => re.test("/tmp/workspace/skills/node_modules/pkg/index.js"))).toBe( + true, + ); + expect(ignored.some((re) => re.test("/tmp/workspace/skills/dist/index.js"))).toBe(true); + expect(ignored.some((re) => re.test("/tmp/workspace/skills/.git/config"))).toBe(true); + }); +}); diff --git a/src/agents/skills/refresh.ts b/src/agents/skills/refresh.ts index f22690b7d..12e0b5f5f 100644 --- a/src/agents/skills/refresh.ts +++ b/src/agents/skills/refresh.ts @@ -125,6 +125,13 @@ export function ensureSkillsWatcher(params: { workspaceDir: string; config?: Cla stabilityThreshold: debounceMs, pollInterval: 100, }, + // Avoid FD exhaustion on macOS when a workspace contains huge trees. + // This watcher only needs to react to skill changes. + ignored: [ + /(^|[\\/])\../, // dotfiles (includes .git) + /(^|[\\/])node_modules([\\/]|$)/, + /(^|[\\/])dist([\\/]|$)/, + ], }); const state: SkillsWatchState = { watcher, pathsKey, debounceMs };