Skip to content

fix(agent-core-v2): bound the project skill-root watch fd footprint - #2612

Open
7Sageer wants to merge 2 commits into
mainfrom
fix/skill-watch-fd-exhaustion
Open

fix(agent-core-v2): bound the project skill-root watch fd footprint#2612
7Sageer wants to merge 2 commits into
mainfrom
fix/skill-watch-fd-exhaustion

Conversation

@7Sageer

@7Sageer 7Sageer commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

Resolve #2542

Problem

On macOS, 0.31.1 can exhaust the process file-descriptor budget when a skill folder contains a very large file tree (e.g. a skill bundling a Python runtime): afterwards every spawn fails with EBADF and all tools (Bash, Grep, …) stop working. 0.29.1 did not have this problem.

Root cause: the Workspace-domain refactor (#2366, first released in 0.31.1) made the workspace skill-root source recursively watch the project skill-root candidates (.kimi-code/skills, .agents/skills, anchored at the project root so not-yet-existing roots are detected). chokidar v4 watches every node with its own fs.watch; on macOS each watched file or directory holds one kqueue fd, and the watch filter pruned nothing inside the skill subtrees — not even node_modules or dot directories, which the scanner itself never descends into. A 10k-file skill tree therefore pinned 10k+ fds for the lifetime of the workspace handler (which is never closed).

What changed

Two complementary fixes that keep skill hot-reload working:

  • The watch now mirrors the scanner's pruning. subtreeWatchFilter gains maxDepth / skipEntry options, the scanner exports its exclusion rule and depth cap, and the skill-root source watches with those rules (depth cap + 2 segments for the skill directory and its SKILL.md). Subtrees the scanner would never read no longer get watched — this bounds fd and event volume on every platform.
  • New signal mode in hostFsWatch. Callers that consume events as a mere "something changed" signal (the skill source and similar rescan-style watchers) opt in; on darwin/win32 a signal-mode recursive watch then uses ONE native recursive fs.watch (FSEvents / ReadDirectoryChangesW) whose fd footprint is constant in the subtree size, instead of per-node watchers. Events in this mode may be coarse (a deleted entry of unknown kind is reported as 'file'; FSEvents may coalesce a burst into an ancestor-directory event), which is safe for rescan-style consumers and documented on the interface. The workspaceFs watch bridge, whose subscribers receive precise action/kind, stays on chokidar unchanged.

Tests: unit tests for the new filter options; signal-mode integration tests (create/modify/delete mapping, ignored predicate); a darwin-only regression test asserting the fd footprint stays bounded over a 1200-file subtree; a catalog scenario test proving pruned writes do not change the catalog while real skill edits still hot-reload. Full agent-core-v2 suite passes (296 files / 4620 tests), plus typecheck and lint:imports.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

The workspace skill-root source recursively watches the skill-root
candidates with chokidar, which holds one fs.watch fd per file and per
directory on macOS. A skill bundling a large runtime tree can exhaust
the process fd budget and break every subsequent spawn (EBADF).

Mirror the scanner's own pruning (node_modules / dot entries, scan
depth cap) in the watch filter, and add a signal mode to hostFsWatch:
rescan-style consumers get ONE native recursive fs.watch on
darwin/win32, whose fd footprint is constant in the subtree size.
@changeset-bot

changeset-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b7af1d1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Aug 4, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@b7af1d1
npx https://pkg.pr.new/@moonshot-ai/kimi-code@b7af1d1

commit: b7af1d1

…harden signal mode

Review follow-up:

- The scanner probes every entry's direct SKILL.md before gating
  recursion, so the watch filter now keeps an excluded entry itself and
  its direct SKILL.md (keepEntryFile) instead of pruning them — skills
  under node_modules / dot directories keep their hot reload.
- The signal-mode native leg now owns its recovery: a native watch
  error fires one root invalidation and re-arms with capped exponential
  backoff; chokidar is used only where recursive fs.watch is
  unavailable, so a transient failure can neither silently end hot
  reload nor downgrade to the per-node watcher.
- Native event path resolution handles absolute filenames and the
  root-basename case, clamping out-of-root events to a root
  invalidation instead of dropping them.
- The event mapping is extracted into NativeSignalMapper with the stat
  call injected, so the native-branch decisions are unit-tested on any
  platform.
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.

fix(agent-core): macOS file descriptor exhaustion in workspace scanner

1 participant