fix: Convert sync file I/O to async in agent spawn path to unblock event loop
writeInputFiles, spawnDetached, and diagnostic writes now use fs/promises (mkdir, writeFile) instead of mkdirSync/writeFileSync. File writes in writeInputFiles are batched with Promise.all. openSync/closeSync for child process stdio FDs remain sync as spawn() requires the FDs immediately.
This commit is contained in:
@@ -8,8 +8,8 @@
|
||||
* Output: .cw/output/ — written by agent during execution
|
||||
*/
|
||||
|
||||
import { mkdirSync, writeFileSync, readdirSync, existsSync } from 'node:fs';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { readdirSync, existsSync, readFileSync } from 'node:fs';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import matter from 'gray-matter';
|
||||
import { nanoid } from 'nanoid';
|
||||
@@ -109,12 +109,12 @@ function formatFrontmatter(data: Record<string, unknown>, body: string = ''): st
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
export function writeInputFiles(options: WriteInputFilesOptions): void {
|
||||
export async function writeInputFiles(options: WriteInputFilesOptions): Promise<void> {
|
||||
const inputDir = join(options.agentWorkdir, '.cw', 'input');
|
||||
mkdirSync(inputDir, { recursive: true });
|
||||
await mkdir(inputDir, { recursive: true });
|
||||
|
||||
// Write expected working directory marker for verification
|
||||
writeFileSync(
|
||||
await writeFile(
|
||||
join(inputDir, '../expected-pwd.txt'),
|
||||
options.agentWorkdir,
|
||||
'utf-8'
|
||||
@@ -122,6 +122,9 @@ export function writeInputFiles(options: WriteInputFilesOptions): void {
|
||||
|
||||
const manifestFiles: string[] = [];
|
||||
|
||||
// Collect all file writes, then flush in parallel
|
||||
const writes: Array<{ path: string; content: string }> = [];
|
||||
|
||||
if (options.initiative) {
|
||||
const ini = options.initiative;
|
||||
const content = formatFrontmatter(
|
||||
@@ -134,13 +137,12 @@ export function writeInputFiles(options: WriteInputFilesOptions): void {
|
||||
},
|
||||
'',
|
||||
);
|
||||
writeFileSync(join(inputDir, 'initiative.md'), content, 'utf-8');
|
||||
writes.push({ path: join(inputDir, 'initiative.md'), content });
|
||||
manifestFiles.push('initiative.md');
|
||||
}
|
||||
|
||||
if (options.pages && options.pages.length > 0) {
|
||||
const pagesDir = join(inputDir, 'pages');
|
||||
mkdirSync(pagesDir, { recursive: true });
|
||||
await mkdir(join(inputDir, 'pages'), { recursive: true });
|
||||
|
||||
for (const page of options.pages) {
|
||||
let bodyMarkdown = '';
|
||||
@@ -162,7 +164,7 @@ export function writeInputFiles(options: WriteInputFilesOptions): void {
|
||||
bodyMarkdown,
|
||||
);
|
||||
const filename = `pages/${page.id}.md`;
|
||||
writeFileSync(join(pagesDir, `${page.id}.md`), content, 'utf-8');
|
||||
writes.push({ path: join(inputDir, 'pages', `${page.id}.md`), content });
|
||||
manifestFiles.push(filename);
|
||||
}
|
||||
}
|
||||
@@ -185,7 +187,7 @@ export function writeInputFiles(options: WriteInputFilesOptions): void {
|
||||
},
|
||||
bodyMarkdown,
|
||||
);
|
||||
writeFileSync(join(inputDir, 'phase.md'), content, 'utf-8');
|
||||
writes.push({ path: join(inputDir, 'phase.md'), content });
|
||||
manifestFiles.push('phase.md');
|
||||
}
|
||||
|
||||
@@ -202,7 +204,7 @@ export function writeInputFiles(options: WriteInputFilesOptions): void {
|
||||
},
|
||||
t.description ?? '',
|
||||
);
|
||||
writeFileSync(join(inputDir, 'task.md'), content, 'utf-8');
|
||||
writes.push({ path: join(inputDir, 'task.md'), content });
|
||||
manifestFiles.push('task.md');
|
||||
}
|
||||
|
||||
@@ -210,8 +212,7 @@ export function writeInputFiles(options: WriteInputFilesOptions): void {
|
||||
const contextFiles: string[] = [];
|
||||
|
||||
if (options.phases && options.phases.length > 0) {
|
||||
const phasesDir = join(inputDir, 'context', 'phases');
|
||||
mkdirSync(phasesDir, { recursive: true });
|
||||
await mkdir(join(inputDir, 'context', 'phases'), { recursive: true });
|
||||
|
||||
for (const ph of options.phases) {
|
||||
let bodyMarkdown = '';
|
||||
@@ -232,14 +233,13 @@ export function writeInputFiles(options: WriteInputFilesOptions): void {
|
||||
bodyMarkdown,
|
||||
);
|
||||
const filename = `context/phases/${ph.id}.md`;
|
||||
writeFileSync(join(phasesDir, `${ph.id}.md`), content, 'utf-8');
|
||||
writes.push({ path: join(inputDir, 'context', 'phases', `${ph.id}.md`), content });
|
||||
contextFiles.push(filename);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.tasks && options.tasks.length > 0) {
|
||||
const tasksDir = join(inputDir, 'context', 'tasks');
|
||||
mkdirSync(tasksDir, { recursive: true });
|
||||
await mkdir(join(inputDir, 'context', 'tasks'), { recursive: true });
|
||||
|
||||
for (const t of options.tasks) {
|
||||
const content = formatFrontmatter(
|
||||
@@ -257,7 +257,7 @@ export function writeInputFiles(options: WriteInputFilesOptions): void {
|
||||
t.description ?? '',
|
||||
);
|
||||
const filename = `context/tasks/${t.id}.md`;
|
||||
writeFileSync(join(tasksDir, `${t.id}.md`), content, 'utf-8');
|
||||
writes.push({ path: join(inputDir, 'context', 'tasks', `${t.id}.md`), content });
|
||||
contextFiles.push(filename);
|
||||
}
|
||||
}
|
||||
@@ -276,17 +276,18 @@ export function writeInputFiles(options: WriteInputFilesOptions): void {
|
||||
status: t.status,
|
||||
});
|
||||
}
|
||||
const contextDir = join(inputDir, 'context');
|
||||
mkdirSync(contextDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(contextDir, 'index.json'),
|
||||
JSON.stringify({ tasksByPhase }, null, 2) + '\n',
|
||||
'utf-8',
|
||||
);
|
||||
await mkdir(join(inputDir, 'context'), { recursive: true });
|
||||
writes.push({
|
||||
path: join(inputDir, 'context', 'index.json'),
|
||||
content: JSON.stringify({ tasksByPhase }, null, 2) + '\n',
|
||||
});
|
||||
}
|
||||
|
||||
// Write manifest listing exactly which files were created
|
||||
writeFileSync(
|
||||
// Flush all file writes in parallel — yields the event loop between I/O ops
|
||||
await Promise.all(writes.map(w => writeFile(w.path, w.content, 'utf-8')));
|
||||
|
||||
// Write manifest last (after all files exist)
|
||||
await writeFile(
|
||||
join(inputDir, 'manifest.json'),
|
||||
JSON.stringify({
|
||||
files: manifestFiles,
|
||||
|
||||
Reference in New Issue
Block a user