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:
Lukas May
2026-03-04 12:15:31 +01:00
parent 70fd996fa1
commit bd0aec4499
7 changed files with 87 additions and 79 deletions

View File

@@ -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,