fix: Convert sync file I/O to async in read path to unblock event loop

readFrontmatterFile, readFrontmatterDir, readSummary, readPhaseFiles,
readTaskFiles, readDecisionFiles, and readPageFiles all used readFileSync
and readdirSync which block the Node.js event loop during agent completion
handling. Converted to async using readFile/readdir from fs/promises and
added await at all call sites in output-handler.ts.
This commit is contained in:
Lukas May
2026-03-04 12:25:34 +01:00
parent a2afc2e1fd
commit 73a4c6cb0c
2 changed files with 23 additions and 23 deletions

View File

@@ -8,8 +8,8 @@
* Output: .cw/output/ — written by agent during execution
*/
import { readdirSync, existsSync, readFileSync } from 'node:fs';
import { mkdir, writeFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { mkdir, writeFile, readFile, readdir } from 'node:fs/promises';
import { join } from 'node:path';
import matter from 'gray-matter';
import { nanoid } from 'nanoid';
@@ -303,9 +303,9 @@ export async function writeInputFiles(options: WriteInputFilesOptions): Promise<
// OUTPUT FILE READING
// =============================================================================
export function readFrontmatterFile(filePath: string): { data: Record<string, unknown>; body: string } | null {
export async function readFrontmatterFile(filePath: string): Promise<{ data: Record<string, unknown>; body: string } | null> {
try {
const raw = readFileSync(filePath, 'utf-8');
const raw = await readFile(filePath, 'utf-8');
const parsed = matter(raw);
return { data: parsed.data as Record<string, unknown>, body: parsed.content.trim() };
} catch {
@@ -313,19 +313,19 @@ export function readFrontmatterFile(filePath: string): { data: Record<string, un
}
}
function readFrontmatterDir<T>(
async function readFrontmatterDir<T>(
dirPath: string,
mapper: (data: Record<string, unknown>, body: string, filename: string) => T | null,
): T[] {
): Promise<T[]> {
if (!existsSync(dirPath)) return [];
const results: T[] = [];
try {
const entries = readdirSync(dirPath);
const entries = await readdir(dirPath);
for (const entry of entries) {
if (!entry.endsWith('.md')) continue;
const filePath = join(dirPath, entry);
const parsed = readFrontmatterFile(filePath);
const parsed = await readFrontmatterFile(filePath);
if (!parsed) continue;
const mapped = mapper(parsed.data, parsed.body, entry);
if (mapped) results.push(mapped);
@@ -336,9 +336,9 @@ function readFrontmatterDir<T>(
return results;
}
export function readSummary(agentWorkdir: string): ParsedSummary | null {
export async function readSummary(agentWorkdir: string): Promise<ParsedSummary | null> {
const filePath = join(agentWorkdir, '.cw', 'output', 'SUMMARY.md');
const parsed = readFrontmatterFile(filePath);
const parsed = await readFrontmatterFile(filePath);
if (!parsed) return null;
const filesModified = parsed.data.files_modified;
@@ -348,7 +348,7 @@ export function readSummary(agentWorkdir: string): ParsedSummary | null {
};
}
export function readPhaseFiles(agentWorkdir: string): ParsedPhaseFile[] {
export async function readPhaseFiles(agentWorkdir: string): Promise<ParsedPhaseFile[]> {
const dirPath = join(agentWorkdir, '.cw', 'output', 'phases');
return readFrontmatterDir(dirPath, (data, body, filename) => {
const id = filename.replace(/\.md$/, '');
@@ -364,7 +364,7 @@ export function readPhaseFiles(agentWorkdir: string): ParsedPhaseFile[] {
});
}
export function readTaskFiles(agentWorkdir: string): ParsedTaskFile[] {
export async function readTaskFiles(agentWorkdir: string): Promise<ParsedTaskFile[]> {
const dirPath = join(agentWorkdir, '.cw', 'output', 'tasks');
return readFrontmatterDir(dirPath, (data, body, filename) => {
const id = filename.replace(/\.md$/, '');
@@ -384,7 +384,7 @@ export function readTaskFiles(agentWorkdir: string): ParsedTaskFile[] {
});
}
export function readDecisionFiles(agentWorkdir: string): ParsedDecisionFile[] {
export async function readDecisionFiles(agentWorkdir: string): Promise<ParsedDecisionFile[]> {
const dirPath = join(agentWorkdir, '.cw', 'output', 'decisions');
return readFrontmatterDir(dirPath, (data, body, filename) => {
const id = filename.replace(/\.md$/, '');
@@ -398,7 +398,7 @@ export function readDecisionFiles(agentWorkdir: string): ParsedDecisionFile[] {
});
}
export function readPageFiles(agentWorkdir: string): ParsedPageFile[] {
export async function readPageFiles(agentWorkdir: string): Promise<ParsedPageFile[]> {
const dirPath = join(agentWorkdir, '.cw', 'output', 'pages');
return readFrontmatterDir(dirPath, (data, body, filename) => {
const pageId = filename.replace(/\.md$/, '');

View File

@@ -415,14 +415,14 @@ export class OutputHandler {
getAgentWorkdir: (alias: string) => string,
): Promise<void> {
const agentWorkdir = getAgentWorkdir(agent.worktreeId);
const summary = readSummary(agentWorkdir);
const summary = await readSummary(agentWorkdir);
const initiativeId = agent.initiativeId;
const canWriteChangeSets = this.changeSetRepository && initiativeId;
let resultMessage = summary?.body ?? 'Task completed';
switch (mode) {
case 'plan': {
const phases = readPhaseFiles(agentWorkdir);
const phases = await readPhaseFiles(agentWorkdir);
if (canWriteChangeSets && this.phaseRepository && phases.length > 0) {
const entries: CreateChangeSetEntryData[] = [];
@@ -499,9 +499,9 @@ export class OutputHandler {
break;
}
case 'detail': {
const tasks = readTaskFiles(agentWorkdir);
const tasks = await readTaskFiles(agentWorkdir);
if (canWriteChangeSets && this.taskRepository && tasks.length > 0) {
const phaseInput = readFrontmatterFile(join(agentWorkdir, '.cw', 'input', 'phase.md'));
const phaseInput = await readFrontmatterFile(join(agentWorkdir, '.cw', 'input', 'phase.md'));
const phaseId = (phaseInput?.data?.id as string) ?? null;
const entries: CreateChangeSetEntryData[] = [];
const fileIdToDbId = new Map<string, string>();
@@ -595,12 +595,12 @@ export class OutputHandler {
break;
}
case 'discuss': {
const decisions = readDecisionFiles(agentWorkdir);
const decisions = await readDecisionFiles(agentWorkdir);
resultMessage = JSON.stringify({ summary: summary?.body, decisions });
break;
}
case 'refine': {
const pages = readPageFiles(agentWorkdir);
const pages = await readPageFiles(agentWorkdir);
if (canWriteChangeSets && this.pageRepository && pages.length > 0) {
const entries: CreateChangeSetEntryData[] = [];
@@ -662,9 +662,9 @@ export class OutputHandler {
break;
}
case 'chat': {
const chatPhases = readPhaseFiles(agentWorkdir);
const chatTasks = readTaskFiles(agentWorkdir);
const chatPages = readPageFiles(agentWorkdir);
const chatPhases = await readPhaseFiles(agentWorkdir);
const chatTasks = await readTaskFiles(agentWorkdir);
const chatPages = await readPageFiles(agentWorkdir);
if (canWriteChangeSets) {
const entries: CreateChangeSetEntryData[] = [];
let sortOrd = 0;