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

@@ -57,7 +57,7 @@ export class CassetteProcessManager extends ProcessManager {
this.replayWorkerPath = new URL('./replay-worker.mjs', import.meta.url).pathname;
}
override spawnDetached(
override async spawnDetached(
agentId: string,
agentName: string,
command: string,
@@ -68,7 +68,7 @@ export class CassetteProcessManager extends ProcessManager {
prompt?: string,
onEvent?: (event: StreamEvent) => void,
onRawContent?: (content: string) => void,
): { pid: number; outputFilePath: string; tailer: FileTailer } {
): Promise<{ pid: number; outputFilePath: string; tailer: FileTailer }> {
const key: CassetteKey = {
normalizedPrompt: normalizePrompt(prompt ?? '', this._workspaceRoot),
providerName,
@@ -80,7 +80,7 @@ export class CassetteProcessManager extends ProcessManager {
const existing = this.cassetteMode !== 'record' ? this.store.find(key) : null;
if (existing) {
const result = this.replayFromCassette(agentId, agentName, cwd, env, providerName, existing, onEvent, onRawContent);
const result = await this.replayFromCassette(agentId, agentName, cwd, env, providerName, existing, onEvent, onRawContent);
this.pendingReplays.set(result.pid, { cassette: existing, agentCwd: cwd });
return result;
}
@@ -94,7 +94,7 @@ export class CassetteProcessManager extends ProcessManager {
// auto or record: run the real agent and record the cassette on completion.
console.log(`[cassette] recording new cassette for agent '${agentName}' (${providerName})`);
const result = super.spawnDetached(agentId, agentName, command, args, cwd, env, providerName, prompt, onEvent, onRawContent);
const result = await super.spawnDetached(agentId, agentName, command, args, cwd, env, providerName, prompt, onEvent, onRawContent);
this.pendingRecordings.set(result.pid, { key, outputFilePath: result.outputFilePath, agentCwd: cwd });
return result;
}
@@ -230,7 +230,7 @@ export class CassetteProcessManager extends ProcessManager {
}
}
private replayFromCassette(
private async replayFromCassette(
agentId: string,
agentName: string,
cwd: string,
@@ -239,7 +239,7 @@ export class CassetteProcessManager extends ProcessManager {
cassette: CassetteEntry,
onEvent?: (event: StreamEvent) => void,
onRawContent?: (content: string) => void,
): { pid: number; outputFilePath: string; tailer: FileTailer } {
): Promise<{ pid: number; outputFilePath: string; tailer: FileTailer }> {
console.log(`[cassette] replaying cassette for agent '${agentName}' (${cassette.recording.jsonlLines.length} lines)`);
return super.spawnDetached(