* Debounce rapid skill changes into a single reload. When many skill files * change at once (e.g. auto-update installs a new binary and a new session * touches skill directories), each file fires its own chokidar event. Without * debouncing, each event triggers clearSkillCaches() + clearCommandsC
(changedPath: string)
| 253 | * deadlock the Bun event loop via rapid FSWatcher watch/unwatch churn. |
| 254 | */ |
| 255 | function scheduleReload(changedPath: string): void { |
| 256 | pendingChangedPaths.add(changedPath) |
| 257 | if (reloadTimer) clearTimeout(reloadTimer) |
| 258 | reloadTimer = setTimeout(async () => { |
| 259 | reloadTimer = null |
| 260 | const paths = [...pendingChangedPaths] |
| 261 | pendingChangedPaths.clear() |
| 262 | // Fire ConfigChange hook once for the batch — the hook query is always |
| 263 | // 'skills' so firing per-path (which can be hundreds during a git |
| 264 | // operation) just spams the hook matcher with identical queries. Pass the |
| 265 | // first path as a representative; hooks can inspect all paths via the |
| 266 | // skills directory if they need the full set. |
| 267 | const results = await executeConfigChangeHooks('skills', paths[0]!) |
| 268 | if (hasBlockingResult(results)) { |
| 269 | logForDebugging( |
| 270 | `ConfigChange hook blocked skill reload (${paths.length} paths)`, |
| 271 | ) |
| 272 | return |
| 273 | } |
| 274 | clearSkillCaches() |
| 275 | clearCommandsCache() |
| 276 | resetSentSkillNames() |
| 277 | skillsChanged.emit() |
| 278 | }, testOverrides?.reloadDebounce ?? RELOAD_DEBOUNCE_MS) |
| 279 | } |
| 280 | |
| 281 | /** |
| 282 | * Reset internal state for testing purposes only. |
no test coverage detected