fix: stop repeated file-watcher errors on Windows drive-root and UNC workspaces (#2876)

This commit is contained in:
7Sageer 2026-08-13 14:35:23 +08:00 committed by GitHub
parent f8a88c1bd9
commit 5912d4c7d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 118 additions and 24 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
Fix repeated file-watcher errors on Windows when the workspace is a drive root (such as `E:\`) or a UNC network share.

View file

@ -1,14 +1,40 @@
/**
* `_base/utils/paths` (cross-cutting) pure path-filter predicates.
* `_base/utils/paths` (cross-cutting) pure path predicates and directory
* walks.
*
* Constrains filesystem watches to selected subtrees and scanner-visible
* entries.
* entries, and walks host directory chains with platform-native path
* semantics so drive-letter / UNC roots keep their host form.
*/
import nodePath from 'node:path';
function normalizeSlashes(p: string): string {
return p.replaceAll('\\', '/');
}
export interface UpwardRootPathApi {
resolve(dir: string): string;
dirname(dir: string): string;
join(...segments: string[]): string;
}
export async function findUpwardRoot(
workDir: string,
markerName: string,
hasMarker: (markerPath: string) => Promise<boolean>,
pathApi: UpwardRootPathApi = nodePath,
): Promise<string> {
const start = pathApi.resolve(workDir);
let current = start;
while (true) {
if (await hasMarker(pathApi.join(current, markerName))) return normalizeSlashes(current);
const parent = pathApi.dirname(current);
if (parent === current) return normalizeSlashes(start);
current = parent;
}
}
export interface SubtreeWatchFilterOptions {
readonly maxDepth?: number;
readonly skipEntry?: (entryName: string) => boolean;

View file

@ -11,6 +11,8 @@
import { promises as fs } from 'node:fs';
import path from 'pathe';
import { findUpwardRoot } from '#/_base/utils/paths';
import type { SkillRoot, SkillSource } from './types';
const USER_BRAND_DIRS = ['skills'] as const;
@ -78,14 +80,7 @@ export async function configuredRoots(
}
async function findProjectRoot(workDir: string): Promise<string> {
const start = path.resolve(workDir);
let current = start;
while (true) {
if (await exists(path.join(current, '.git'))) return current;
const parent = path.dirname(current);
if (parent === current) return start;
current = parent;
}
return findUpwardRoot(workDir, '.git', exists);
}
async function pushFirstExisting(

View file

@ -5,8 +5,9 @@
* filesystem boundary. Pure path probes; no scoped state.
*/
import { dirname, join, resolve } from 'pathe';
import { join } from 'pathe';
import { findUpwardRoot } from '#/_base/utils/paths';
import type { IHostFileSystem } from '#/os/interface/hostFileSystem';
import { HostFsError, OsFsErrors } from '#/os/interface/hostFsErrors';
@ -86,20 +87,15 @@ async function findProjectRoot(
workDir: string,
warn?: AgentRootWarn,
): Promise<string> {
const start = resolve(workDir);
let current = start;
while (true) {
const marker = join(current, '.git');
return findUpwardRoot(workDir, '.git', async (marker) => {
try {
if (await pathExists(fs, marker)) return current;
return await pathExists(fs, marker);
} catch (error) {
if (isUnavailable(error)) throw error;
warn?.(`Skipping unreadable project marker ${marker}: ${errorMessage(error)}`, error);
return false;
}
const parent = dirname(current);
if (parent === current) return start;
current = parent;
}
});
}
async function pushFirstExisting(

View file

@ -1,14 +1,19 @@
/**
* Scenario: recursive watches constrained to selected candidate subtrees.
* Responsibilities: candidate ancestry, scan-depth bounds, and excluded-entry
* probing. Wiring: pure path predicates with no external collaborators.
* Responsibilities: candidate ancestry, scan-depth bounds, excluded-entry
* probing, and the marker-based upward root walk. Wiring: pure path
* predicates and walks with no external collaborators.
* Run: `pnpm --filter @moonshot-ai/agent-core-v2 exec vitest run
* test/_base/utils/paths.test.ts`.
*/
import { describe, expect, it } from 'vitest';
import { mkdtemp, mkdir, rm, stat } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import nodePath, { win32 } from 'node:path';
import { subtreeWatchFilter } from '#/_base/utils/paths';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { findUpwardRoot, subtreeWatchFilter } from '#/_base/utils/paths';
describe('subtree watch filtering', () => {
const root = '/repo';
@ -105,3 +110,70 @@ describe('subtree watch filtering', () => {
expect(ignored('/repo/.agents/skills/parent/child/runtime')).toBe(true);
});
});
describe('findUpwardRoot', () => {
const noMarker = async () => false;
describe('with host-default path semantics', () => {
let root: string;
beforeEach(async () => {
root = await mkdtemp(nodePath.join(tmpdir(), 'upward-root-'));
});
afterEach(async () => {
await rm(root, { recursive: true, force: true });
});
const hasMarker = async (markerPath: string): Promise<boolean> => {
try {
await stat(markerPath);
return true;
} catch {
return false;
}
};
it('stops at the nearest ancestor holding the marker', async () => {
await mkdir(nodePath.join(root, '.git'));
const child = nodePath.join(root, 'src', 'pkg');
await mkdir(child, { recursive: true });
const found = await findUpwardRoot(child, '.git', hasMarker);
expect(found).toBe(root.replaceAll('\\', '/'));
});
it('falls back to the working directory when no ancestor holds the marker', async () => {
const child = nodePath.join(root, 'src', 'pkg');
await mkdir(child, { recursive: true });
const found = await findUpwardRoot(child, '.git', hasMarker);
expect(found).toBe(child.replaceAll('\\', '/'));
});
});
it('keeps a Windows drive-root working directory in host form', async () => {
const found = await findUpwardRoot('E:\\', '.git', noMarker, win32);
expect(found).toBe('E:/');
});
it('keeps a Windows UNC working directory in host form', async () => {
const found = await findUpwardRoot('\\\\fs1\\share\\dir', '.git', noMarker, win32);
expect(found).toBe('//fs1/share/dir');
});
it('stops at the nearest Windows ancestor holding the marker', async () => {
const found = await findUpwardRoot(
'E:\\repo\\src',
'.git',
async (markerPath) => markerPath === 'E:\\repo\\.git',
win32,
);
expect(found).toBe('E:/repo');
});
});