From e9233123ebf195a65f352983fc8b2116b40d14ec Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Wed, 29 Jul 2026 21:12:01 +0800 Subject: [PATCH] fix(skill): ignore unchanged missing-root probes --- packages/skill/skill-local/src/index.ts | 30 ++++++++++++++----- .../tests/skill-local-watcher.spec.ts | 25 ++++++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/packages/skill/skill-local/src/index.ts b/packages/skill/skill-local/src/index.ts index 4ac5f101cd..ed68e84bcb 100644 --- a/packages/skill/skill-local/src/index.ts +++ b/packages/skill/skill-local/src/index.ts @@ -421,7 +421,7 @@ class SkillWatchManager { private openAncestorWatcher(state: RootWatchState, mode: Extract): WatchHandle { const listener = (_current: Stats, _previous: Stats): void => { - this.handleWatchEvent(state, mode, 'change', mode.nextPath) + void this.handleAncestorWatchEvent(state, mode) } watchFile(mode.nextPath, { persistent: false, @@ -435,6 +435,25 @@ class SkillWatchManager { } } + private async handleAncestorWatchEvent( + state: RootWatchState, + mode: Extract, + ): Promise { + let current: RootWatchMode + try { + current = await resolveRootWatchMode(state.root.path) + } catch (error) { + /* v8 ignore start -- Non-absence stat failures need a platform permission or I/O fault. */ + if (!this.closing && state.owners.size > 0) this.handleWatcherError(state, error) + return + /* v8 ignore stop */ + } + if (this.closing || state.owners.size === 0 || sameWatchMode(mode, current)) return + this.queueInvalidation() + state.unhealthy = true + this.scheduleRewatch(state) + } + private async openRootWatcher(state: RootWatchState, mode: Extract): Promise { const watcher = chokidar.watch(mode.anchor, { persistent: false, @@ -481,13 +500,13 @@ class SkillWatchManager { private handleWatchEvent( state: RootWatchState, - mode: RootWatchMode, + mode: Extract, event: SkillWatchEvent, path: string, ): void { if (this.closing || !isRelevantWatchEvent(state.root, mode, event, resolve(path))) return this.queueInvalidation() - if (mode.kind === 'ancestor' || (resolve(path) === state.root.path && event === 'unlinkDir')) { + if (resolve(path) === state.root.path && event === 'unlinkDir') { state.unhealthy = true this.scheduleRewatch(state) } @@ -591,13 +610,10 @@ function sameWatchMode(left: RootWatchMode, right: RootWatchMode): boolean { function isRelevantWatchEvent( root: SkillRoot, - mode: RootWatchMode, + mode: Extract, event: SkillWatchEvent, path: string, ): boolean { - if (mode.kind === 'ancestor') { - return path === mode.nextPath - } const segments = containedSegments(root.path, path) if (segments === undefined) return false if (segments.length === 0) return event === 'addDir' || event === 'unlinkDir' diff --git a/packages/skill/skill-local/tests/skill-local-watcher.spec.ts b/packages/skill/skill-local/tests/skill-local-watcher.spec.ts index 97ab8c9ee7..2a7a3e5f86 100644 --- a/packages/skill/skill-local/tests/skill-local-watcher.spec.ts +++ b/packages/skill/skill-local/tests/skill-local-watcher.spec.ts @@ -92,6 +92,31 @@ beforeEach(() => { }) describe('skill-local watcher failures', () => { + it('ignores missing-path probes until the observed path actually changes', async () => { + const home = await tempDir('skill-watch-missing-stable') + const ctx = new Context() + await ctx.plugin(SkillService) + const fiber = await ctx.plugin(SkillLocal, { + dshHome: join(home, '.dsh'), + agentsHome: join(home, '.agents'), + watch: true, + watchPollIntervalMs: 10, + }) + expect(await ctx.skills.snapshot()).toEqual({ skills: [], complete: true }) + expect(watcherHarness.watchFiles).toHaveLength(2) + let invalidations = 0 + ctx.on('skills/change', () => { invalidations += 1 }) + + for (const control of watcherHarness.watchFiles) { + control.listener({} as Stats, {} as Stats) + } + await settle() + + expect(invalidations).toBe(0) + expect(watcherHarness.watchFiles).toHaveLength(2) + await fiber.dispose() + }) + it('marks a startup failure incomplete and retries discovery without caching it', async () => { const home = await tempDir('skill-watch-start-error') const root = join(home, '.dsh/skills')