From 73a4c6cb0cbcf6fe95b2daf0d3836967ac4a84c5 Mon Sep 17 00:00:00 2001 From: Lukas May Date: Wed, 4 Mar 2026 12:25:34 +0100 Subject: [PATCH] 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. --- apps/server/agent/file-io.ts | 28 ++++++++++++++-------------- apps/server/agent/output-handler.ts | 18 +++++++++--------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/apps/server/agent/file-io.ts b/apps/server/agent/file-io.ts index 6f07b13..211befc 100644 --- a/apps/server/agent/file-io.ts +++ b/apps/server/agent/file-io.ts @@ -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; body: string } | null { +export async function readFrontmatterFile(filePath: string): Promise<{ data: Record; 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, body: parsed.content.trim() }; } catch { @@ -313,19 +313,19 @@ export function readFrontmatterFile(filePath: string): { data: Record( +async function readFrontmatterDir( dirPath: string, mapper: (data: Record, body: string, filename: string) => T | null, -): T[] { +): Promise { 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( return results; } -export function readSummary(agentWorkdir: string): ParsedSummary | null { +export async function readSummary(agentWorkdir: string): Promise { 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 { 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 { 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 { 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 { const dirPath = join(agentWorkdir, '.cw', 'output', 'pages'); return readFrontmatterDir(dirPath, (data, body, filename) => { const pageId = filename.replace(/\.md$/, ''); diff --git a/apps/server/agent/output-handler.ts b/apps/server/agent/output-handler.ts index 2413c17..9c85b2c 100644 --- a/apps/server/agent/output-handler.ts +++ b/apps/server/agent/output-handler.ts @@ -415,14 +415,14 @@ export class OutputHandler { getAgentWorkdir: (alias: string) => string, ): Promise { 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(); @@ -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;