Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions src/core/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1340,12 +1340,8 @@ export class Agent {
};
outcome = await this.hooks.runPostToolUse(outcome, context);

if (toolUse.name === 'fs_read' && toolUse.input?.path) {
await this.filePool.recordRead(toolUse.input.path);
}
if ((toolUse.name === 'fs_write' || toolUse.name === 'fs_edit' || toolUse.name === 'fs_multi_edit') && toolUse.input?.path) {
await this.filePool.recordEdit(toolUse.input.path);
}
// NOTE: recordRead/recordEdit 已在各工具内部调用,此处不再重复调用
// 参考: fs_read/index.ts:25, fs_write/index.ts:26, fs_edit/index.ts:29,53, fs_multi_edit/index.ts:60,92

const success = outcome.ok !== false;
const duration = Date.now() - (this.toolRecords.get(record.id)?.startedAt ?? Date.now());
Expand Down
27 changes: 26 additions & 1 deletion src/core/file-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface FilePoolOptions {
export class FilePool {
private records = new Map<string, FileRecord>();
private watchers = new Map<string, string>();
private watchPending = new Map<string, Promise<void>>(); // per-path 锁,防止并发创建 watcher
private readonly watchEnabled: boolean;
private readonly onChange?: (event: { path: string; mtime: number }) => void;

Expand Down Expand Up @@ -106,10 +107,34 @@ export class FilePool {
return Array.from(this.records.keys());
}

private async ensureWatch(path: string) {
private async ensureWatch(path: string): Promise<void> {
if (!this.watchEnabled) return;
if (!this.sandbox.watchFiles) return;
if (this.watchers.has(path)) return;

// 检查是否有正在进行的 watch 操作(per-path 锁)
const pending = this.watchPending.get(path);
if (pending) {
await pending; // 等待已有操作完成
return;
}

// 创建 watch 操作并存储 Promise
const watchPromise = this.doWatch(path);
this.watchPending.set(path, watchPromise);

try {
await watchPromise;
} finally {
this.watchPending.delete(path);
}
}

private async doWatch(path: string): Promise<void> {
// 再次检查(可能在等待期间已被设置)
if (this.watchers.has(path)) return;
if (!this.sandbox.watchFiles) return;

try {
const id = await this.sandbox.watchFiles([path], (event) => {
const record = this.records.get(path);
Expand Down
56 changes: 56 additions & 0 deletions tests/unit/core/file-pool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,62 @@ runner

const status = await pool.checkFreshness('missing.txt');
expect.toEqual(status.isFresh, false);
})

.test('并发 recordEdit 不会创建重复 watcher', async () => {
const dir = createTempDir('concurrent');
const filePath = path.join(dir, 'test.txt');
fs.writeFileSync(filePath, 'content');

const sandbox = new LocalSandbox({ workDir: dir, enforceBoundary: true, watchFiles: false });

// 追踪 watchFiles 调用次数
const watchCalls: string[] = [];
(sandbox as any).watchFiles = async (paths: string[]) => {
watchCalls.push(paths[0]);
await new Promise(r => setTimeout(r, 50)); // 模拟异步延迟
return `watch-${watchCalls.length}`;
};

const pool = new FilePool(sandbox, { watch: true });

// 并发调用 3 次 recordEdit
await Promise.all([
pool.recordEdit('test.txt'),
pool.recordEdit('test.txt'),
pool.recordEdit('test.txt'),
]);

// 验证只创建了 1 个 watcher(per-path 锁生效)
expect.toEqual(watchCalls.length, 1);
})

.test('不同文件的并发 recordEdit 各自创建 watcher', async () => {
const dir = createTempDir('concurrent-multi');
fs.writeFileSync(path.join(dir, 'a.txt'), 'a');
fs.writeFileSync(path.join(dir, 'b.txt'), 'b');
fs.writeFileSync(path.join(dir, 'c.txt'), 'c');

const sandbox = new LocalSandbox({ workDir: dir, enforceBoundary: true, watchFiles: false });

const watchCalls: string[] = [];
(sandbox as any).watchFiles = async (paths: string[]) => {
watchCalls.push(paths[0]);
await new Promise(r => setTimeout(r, 30));
return `watch-${watchCalls.length}`;
};

const pool = new FilePool(sandbox, { watch: true });

// 并发操作 3 个不同文件
await Promise.all([
pool.recordEdit('a.txt'),
pool.recordEdit('b.txt'),
pool.recordEdit('c.txt'),
]);

// 每个文件各创建 1 个 watcher
expect.toEqual(watchCalls.length, 3);
});

export async function run() {
Expand Down