* 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)
| 267 | * deadlock the Bun event loop via rapid FSWatcher watch/unwatch churn. |
| 268 | */ |
| 269 | function scheduleReload(changedPath: string): void { |
| 270 | pendingChangedPaths.add(changedPath) |
| 271 | if (reloadTimer) clearTimeout(reloadTimer) |
| 272 | reloadTimer = setTimeout(async () => { |
| 273 | reloadTimer = null |
| 274 | const paths = [...pendingChangedPaths] |
| 275 | pendingChangedPaths.clear() |
| 276 | // Fire ConfigChange hook once for the batch — the hook query is always |
| 277 | // 'skills' so firing per-path (which can be hundreds during a git |
| 278 | // operation) just spams the hook matcher with identical queries. Pass the |
| 279 | // first path as a representative; hooks can inspect all paths via the |
| 280 | // skills directory if they need the full set. |
| 281 | const results = await executeConfigChangeHooks('skills', paths[0]!) |
| 282 | if (hasBlockingResult(results)) { |
| 283 | logForDebugging( |
| 284 | `ConfigChange hook blocked skill reload (${paths.length} paths)`, |
| 285 | ) |
| 286 | return |
| 287 | } |
| 288 | clearSkillCaches() |
| 289 | clearCommandsCache() |
| 290 | resetSentSkillNames() |
| 291 | skillsChanged.emit() |
| 292 | }, testOverrides?.reloadDebounce ?? RELOAD_DEBOUNCE_MS) |
| 293 | } |
| 294 | |
| 295 | /** |
| 296 | * Reset internal state for testing purposes only. |
no test coverage detected