diff --git a/.changeset/fix-windows-unc-watch.md b/.changeset/fix-windows-unc-watch.md new file mode 100644 index 0000000000..50f5e882e1 --- /dev/null +++ b/.changeset/fix-windows-unc-watch.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix startup on Windows network shares by normalizing UNC paths before opening native filesystem watchers. diff --git a/packages/agent-core-v2/src/os/backends/node-local/hostFsWatchService.ts b/packages/agent-core-v2/src/os/backends/node-local/hostFsWatchService.ts index 333f631498..43396ca8b7 100644 --- a/packages/agent-core-v2/src/os/backends/node-local/hostFsWatchService.ts +++ b/packages/agent-core-v2/src/os/backends/node-local/hostFsWatchService.ts @@ -6,7 +6,7 @@ */ import { watch as fsWatch } from 'node:fs'; -import { basename, isAbsolute, join, relative } from 'node:path'; +import { basename, isAbsolute, join, relative, win32 } from 'node:path'; import { FSWatcher } from 'chokidar'; @@ -159,13 +159,16 @@ class SignalWatchHandle implements IHostFsWatchHandle { private startNativeLeg(): void { if (this.disposed) return; try { - const watcher = this.runtime.watchNative(this.root, (_eventType, filename) => { - if (this.disposed) return; - this.retryAttempts = 0; - const absPath = resolveNativeSignalPath(this.root, filename); - if (absPath !== this.root && this.ignored(absPath)) return; - this.fireInvalidation(); - }); + const watcher = this.runtime.watchNative( + toNativeWatchRoot(this.root, this.runtime.platform), + (_eventType, filename) => { + if (this.disposed) return; + this.retryAttempts = 0; + const absPath = resolveNativeSignalPath(this.root, filename); + if (absPath !== this.root && this.ignored(absPath)) return; + this.fireInvalidation(); + }, + ); watcher.on('error', (error: NodeJS.ErrnoException) => { this.onNativeError(watcher, error); }); @@ -255,6 +258,10 @@ function useNativeRecursive( ); } +function toNativeWatchRoot(root: string, platform: NodeJS.Platform): string { + return platform === 'win32' ? win32.normalize(root) : root; +} + function resolveNativeSignalPath(root: string, filename: string | null): string { if (filename === null || filename === '' || filename === basename(root)) return root; return clampToRoot(root, isAbsolute(filename) ? filename : join(root, filename)); diff --git a/packages/agent-core-v2/test/os/backends/node-local/hostFsWatchService.test.ts b/packages/agent-core-v2/test/os/backends/node-local/hostFsWatchService.test.ts index 95e9bb9d45..25f8d697d0 100644 --- a/packages/agent-core-v2/test/os/backends/node-local/hostFsWatchService.test.ts +++ b/packages/agent-core-v2/test/os/backends/node-local/hostFsWatchService.test.ts @@ -48,6 +48,7 @@ class TestNativeWatcher { } interface TestNativeAttempt { + readonly root: string; readonly watcher: TestNativeWatcher; emit(filename: string | null): void; } @@ -58,7 +59,10 @@ interface TestRetry { run(): void; } -function signalRig(options?: { readonly synchronousFailures?: number }): { +function signalRig(options?: { + readonly platform?: NodeJS.Platform; + readonly synchronousFailures?: number; +}): { readonly service: IHostFsWatchService; readonly attempts: TestNativeAttempt[]; readonly retries: TestRetry[]; @@ -69,14 +73,15 @@ function signalRig(options?: { readonly synchronousFailures?: number }): { const retries: TestRetry[] = []; let synchronousFailures = options?.synchronousFailures ?? 0; const runtime: HostFsWatchRuntime = { - platform: 'darwin', - watchNative: (_root, listener) => { + platform: options?.platform ?? 'darwin', + watchNative: (root, listener) => { if (synchronousFailures > 0) { synchronousFailures -= 1; throw Object.assign(new Error('native watch creation failed'), { code: 'EIO' }); } const watcher = new TestNativeWatcher(); attempts.push({ + root, watcher, emit: (filename) => { listener('rename', filename); @@ -164,6 +169,13 @@ describe('host filesystem change notifications', () => { expect(events).toEqual([{ path: '/repo', action: 'modified', kind: 'directory' }]); }); + it('passes Windows UNC roots to the native watcher with native separators', () => { + const rig = signalRig({ platform: 'win32' }); + handle = rig.service.watch('//server/share/repo', { signal: true }); + + expect(rig.attempt(0).root).toBe('\\\\server\\share\\repo'); + }); + it('does not invalidate when a native signal path is ignored', () => { const rig = signalRig(); const events: HostFsChange[] = [];