You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Windows: host process spins at ~200% of one core when a session's skill root is deleted (skill-filesystem rewatch has no backoff)
#11815
On Windows, the DSH desktop host process (dsh-desktop-host, the Node side-agent process) can sustain ~200% of one core indefinitely while completely idle ??no model call, no network egress, no disk I/O. Traced to @deepseek-ai/dsh-skill-filesystem: when a project/skill root path disappears (workspace directory deleted or renamed while the session still references it), the rewatch path has no backoff and no attempt cap, so it re-opens a watcher, fails, and re-scans the directory in a tight loop. The process also leaks native FSWatcher handles that keep receiving events after being logically closed.
This is adjacent to but distinct from #6099 (rename-event storm on a custom Codex skills root) and #11479 (per-session 100ms skill watcher). #6099 needs a high event rate on an existing root; this one needs only a root that no longer exists and reproduces with zero filesystem activity.
Start a session whose working directory / project root is an existing directory, e.g. G:\zimg, and let the skill provider observe the roots.
Delete or rename that directory (G:\ still exists, G:\zimg no longer does).
Leave the app idle.
Result: the host process climbs to ~200% of one core and stays there. In my case it had accumulated 2824 CPU-seconds in ~25 minutes of idling, and the session's last model request had completed 24 minutes earlier.
Evidence
Process state while spinning
198% of one core sustained over 6s samples, flat (not bursts).
Handle count 352 -> 351, thread count fixed at 15 ??nothing is being created or torn down.
IO counters (Win32_PerfRawData_PerfProc_Process): IOReadBytes / IOWriteBytes / IOOtherBytes deltas all 0 over 4s.
TCP: loopback only, no egress. Last API call 2026-11-01T21:30:16Z; session file session.v4.jsonl.zstd last written 21:30:42Z; spin continued past 21:45.
One thread (Id 99968) accounts for ~1010s of the process's 2824s, state Running.
CPU profile (3s, Profiler.setSamplingInterval 200, 3953 samples, taken over the app's own inspector on 127.0.0.1:9230, which is the dsh-experimental-inspector target for the host realm):
Each of the 15 exposes keys ['_events','_eventsCount','_maxListeners','_handle','_ignoreMatcher'] and no _watched / _watchedDirs ??i.e. chokidar considers them logically empty/closed, but the libuv _handle is still alive and still delivering onchange events. So 15 zombie native watchers keep feeding the loop while the owning state has already been replaced.
scheduleRewatch() (~:583) awaits the current opening then calls ensureWatcher() immediately; failure is swallowed:
try{awaitthis.ensureWatcher(state)}catch{// Watch startup logged the retry failure; the next incomplete discovery retries it again.return}
There is no delay, no attempt counter, no maxRewatch, and no backoff anywhere in the file.
handleWatcherError() (:575), handleAncestorWatchEvent() (:493), and the unlinkDir branch of handleWatchEvent() (~:570) each set state.unhealthy = true and call scheduleRewatch() ??so any error or disappearing-anchor event immediately re-enters the open path.
openStableWatcher() (~:446) loops while (!this.closing && state.owners.size > 0) between two resolveRootWatchMode() probes, closing and re-opening the watcher whenever the mode differs. When the root is permanently absent, the ancestor/root mode resolution and the chokidar open both race the deletion, so the loop can keep re-opening.
Suggested direction (not prescriptive): add exponential backoff + a cap to scheduleRewatch, skip/close cleanly when the anchor is absent instead of re-opening, and verify that closing the handle actually closes the libuv handle (the 15 stale FSWatchers suggest close() resolves before the native handle is gone).
Expected behavior
Idle host process should sit near 0% CPU with an absent session root, and should log one warning rather than re-opening a watcher in a loop.
Additional observation
Killing the host process dropped total machine CPU from 184% (one core) to 0.9% immediately, and the main Electron process did not respawn it ??the inspector endpoint on 9230 stayed down while the window and renderer lived on, so the UI kept accepting input with a dead agent backend. If that is not intended, it may deserve its own item.
Workarounds
Kill the stuck host process and restart the desktop app.
Do not leave sessions pointed at deleted/renamed working directories.
Setting CHOKIDAR_USEPOLLING=0 / lowering pollIntervalMs does not help, because the loop is driven by open failures, not by polling.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Summary
On Windows, the DSH desktop host process (
dsh-desktop-host, the Node side-agent process) can sustain ~200% of one core indefinitely while completely idle ??no model call, no network egress, no disk I/O. Traced to@deepseek-ai/dsh-skill-filesystem: when a project/skill root path disappears (workspace directory deleted or renamed while the session still references it), the rewatch path has no backoff and no attempt cap, so it re-opens a watcher, fails, and re-scans the directory in a tight loop. The process also leaks nativeFSWatcherhandles that keep receiving events after being logically closed.This is adjacent to but distinct from #6099 (rename-event storm on a custom Codex skills root) and #11479 (per-session 100ms skill watcher). #6099 needs a high event rate on an existing root; this one needs only a root that no longer exists and reproduces with zero filesystem activity.
Environment
0.2.1-rc.5,dshBuildCommit d4db512a969fccf0c5cc7ec8fe38e9c1d27af9165.0.0(bundled)packages/skill/skill-filesystemReproduction (observed, not yet minimized)
G:\zimg, and let the skill provider observe the roots.G:\still exists,G:\zimgno longer does).Result: the host process climbs to ~200% of one core and stays there. In my case it had accumulated 2824 CPU-seconds in ~25 minutes of idling, and the session's last model request had completed 24 minutes earlier.
Evidence
Process state while spinning
198%of one core sustained over 6s samples, flat (not bursts).352 -> 351, thread count fixed at15??nothing is being created or torn down.Win32_PerfRawData_PerfProc_Process):IOReadBytes/IOWriteBytes/IOOtherBytesdeltas all 0 over 4s.2026-11-01T21:30:16Z; session filesession.v4.jsonl.zstdlast written21:30:42Z; spin continued past21:45.Id 99968) accounts for ~1010s of the process's 2824s, stateRunning.CPU profile (3s,
Profiler.setSamplingInterval 200, 3953 samples, taken over the app's own inspector on127.0.0.1:9230, which is thedsh-experimental-inspectortarget for the host realm):Everything hot is chokidar's readdir + PathNormalize path.
_handleReadre-scanning a directory that does not exist is the direct cost.Paused stack (
Debugger.pause):Leaked native watchers
Each of the 15 exposes keys
['_events','_eventsCount','_maxListeners','_handle','_ignoreMatcher']and no_watched/_watchedDirs??i.e. chokidar considers them logically empty/closed, but the libuv_handleis still alive and still deliveringonchangeevents. So 15 zombie native watchers keep feeding the loop while the owning state has already been replaced.Suspected source
packages/skill/skill-filesystem/src/index.ts(master):scheduleRewatch()(~:583) awaits the current opening then callsensureWatcher()immediately; failure is swallowed:maxRewatch, and no backoff anywhere in the file.handleWatcherError()(:575),:493), and thehandleAncestorWatchEvent()(unlinkDirbranch ofhandleWatchEvent()(~:570) each setstate.unhealthy = trueand callscheduleRewatch()??so any error or disappearing-anchor event immediately re-enters the open path.openStableWatcher()(~:446) loopswhile (!this.closing && state.owners.size > 0)between tworesolveRootWatchMode()probes, closing and re-opening the watcher whenever the mode differs. When the root is permanently absent, the ancestor/root mode resolution and the chokidar open both race the deletion, so the loop can keep re-opening.openRootWatcher()(~:488) still callschokidar.watch(mode.anchor, { depth: 1, atomic: true, awaitWriteFinish: {...} })with noignoredmatcher, which is the same surface [Bug][Windows] skill-filesystem watcher can hit a high-CPU rename-event storm on a custom Codex skills root #6099 reported.Suggested direction (not prescriptive): add exponential backoff + a cap to
scheduleRewatch, skip/close cleanly when the anchor is absent instead of re-opening, and verify that closing the handle actually closes the libuv handle (the 15 staleFSWatchers suggestclose()resolves before the native handle is gone).Expected behavior
Idle host process should sit near 0% CPU with an absent session root, and should log one warning rather than re-opening a watcher in a loop.
Additional observation
Killing the host process dropped total machine CPU from
184%(one core) to0.9%immediately, and the main Electron process did not respawn it ??the inspector endpoint on 9230 stayed down while the window and renderer lived on, so the UI kept accepting input with a dead agent backend. If that is not intended, it may deserve its own item.Workarounds
CHOKIDAR_USEPOLLING=0/ loweringpollIntervalMsdoes not help, because the loop is driven by open failures, not by polling.All reactions