From 0dbc989b8515cb42fbb9e77b45c952431480da8b Mon Sep 17 00:00:00 2001 From: root Date: Thu, 29 Jan 2026 13:42:33 +0800 Subject: [PATCH] fix: exclude Python virtual environments from skill scanner - Add .venv, venv, env to ignored patterns - Add __pycache__, .pytest_cache, .tox, .egg-info to ignored patterns - Prevents FD exhaustion when skills contain Python virtual environments - Fixes #3708: spawn EBADF errors from file descriptor leak The skill scanner was opening thousands of files in Python virtual environments without closing them, causing immediate FD exhaustion (~11,000 open FDs). This change adds common Python dependency directories to the ignore list, similar to how node_modules and .git are already ignored. Added comprehensive test cases to verify all Python-related directories are properly ignored. --- src/agents/skills/refresh.test.ts | 41 +++++++++++++++++++++++++++++++ src/agents/skills/refresh.ts | 13 ++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/agents/skills/refresh.test.ts b/src/agents/skills/refresh.test.ts index 51b86e7f7..cc2ce326f 100644 --- a/src/agents/skills/refresh.test.ts +++ b/src/agents/skills/refresh.test.ts @@ -28,4 +28,45 @@ describe("ensureSkillsWatcher", () => { expect(ignored.some((re) => re.test("/tmp/workspace/skills/.git/config"))).toBe(true); expect(ignored.some((re) => re.test("/tmp/.hidden/skills/index.md"))).toBe(false); }); + + it("ignores Python virtual environments and cache directories", async () => { + const mod = await import("./refresh.js"); + const ignored = mod.DEFAULT_SKILLS_WATCH_IGNORED; + + // Python virtual environments + expect( + ignored.some((re) => re.test("/tmp/workspace/skills/.venv/lib/python3.9/site-packages/")), + ).toBe(true); + expect( + ignored.some((re) => re.test("/tmp/workspace/skills/venv/lib/python3.9/site-packages/")), + ).toBe(true); + expect(ignored.some((re) => re.test("/tmp/workspace/skills/env/bin/python"))).toBe(true); + + // Python cache directories + expect( + ignored.some((re) => re.test("/tmp/workspace/skills/__pycache__/module.cpython-39.pyc")), + ).toBe(true); + expect(ignored.some((re) => re.test("/tmp/workspace/skills/.pytest_cache/"))).toBe(true); + expect(ignored.some((re) => re.test("/tmp/workspace/skills/.tox/py39/lib/"))).toBe(true); + expect(ignored.some((re) => re.test("/tmp/workspace/skills/package.egg-info/PKG-INFO"))).toBe( + true, + ); + + // Should not ignore files that just contain these names + expect(ignored.some((re) => re.test("/tmp/workspace/skills/my_venv_config.json"))).toBe(false); + expect(ignored.some((re) => re.test("/tmp/workspace/skills/pytest.ini"))).toBe(false); + }); + + it("ignores build artifacts and cache directories", async () => { + const mod = await import("./refresh.js"); + const ignored = mod.DEFAULT_SKILLS_WATCH_IGNORED; + + // Build artifacts + expect(ignored.some((re) => re.test("/tmp/workspace/skills/build/output.js"))).toBe(true); + expect(ignored.some((re) => re.test("/tmp/workspace/skills/.cache/data.json"))).toBe(true); + + // Should not ignore normal skill files + expect(ignored.some((re) => re.test("/tmp/workspace/skills/my-skill/build.md"))).toBe(false); + expect(ignored.some((re) => re.test("/tmp/workspace/skills/my-skill/SKILL.md"))).toBe(false); + }); }); diff --git a/src/agents/skills/refresh.ts b/src/agents/skills/refresh.ts index 489fd2e63..e4c66f04d 100644 --- a/src/agents/skills/refresh.ts +++ b/src/agents/skills/refresh.ts @@ -31,6 +31,19 @@ export const DEFAULT_SKILLS_WATCH_IGNORED: RegExp[] = [ /(^|[\\/])\.git([\\/]|$)/, /(^|[\\/])node_modules([\\/]|$)/, /(^|[\\/])dist([\\/]|$)/, + // Python virtual environments + /(^|[\\/])\.venv([\\/]|$)/, + /(^|[\\/])venv([\\/]|$)/, + /(^|[\\/])env([\\/]|$)/, + // Python cache directories + /(^|[\\/])__pycache__([\\/]|$)/, + /(^|[\\/])\.pytest_cache([\\/]|$)/, + /(^|[\\/])\.mypy_cache([\\/]|$)/, + /(^|[\\/])\.tox([\\/]|$)/, + /(^|[\\/])[^/\\]*\.egg-info([\\/]|$)/, + // Build artifacts and caches + /(^|[\\/])build([\\/]|$)/, + /(^|[\\/])\.cache([\\/]|$)/, ]; function bumpVersion(current: number): number {