feat: implement timeout for plan approval and enhance error handling

- Added a 30-minute timeout for user plan approval to prevent indefinite waiting and memory leaks.
- Wrapped resolve/reject functions in the waitForPlanApproval method to ensure timeout is cleared upon resolution.
- Enhanced error handling in the stream processing loop to ensure proper cleanup and logging of errors.
- Improved the handling of task execution and phase completion events for better tracking and user feedback.
This commit is contained in:
Shirone
2026-01-04 03:45:21 +01:00
parent 3ed3a90bf6
commit ef06c13c1a
2 changed files with 501 additions and 437 deletions

View File

@@ -87,17 +87,27 @@ export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGener
resetTimeout();
// Setup abort handling
// Setup abort handling with cleanup
let abortHandler: (() => void) | null = null;
if (abortController) {
abortController.signal.addEventListener('abort', () => {
abortHandler = () => {
console.log('[SubprocessManager] Abort signal received, killing process');
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
childProcess.kill('SIGTERM');
});
};
abortController.signal.addEventListener('abort', abortHandler);
}
// Helper to clean up abort listener
const cleanupAbortListener = () => {
if (abortController && abortHandler) {
abortController.signal.removeEventListener('abort', abortHandler);
abortHandler = null;
}
};
// Parse stdout as JSONL (one JSON object per line)
if (childProcess.stdout) {
const rl = readline.createInterface({
@@ -130,7 +140,12 @@ export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGener
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
rl.close();
cleanupAbortListener();
}
} else {
// No stdout - still need to cleanup abort listener when process exits
cleanupAbortListener();
}
// Wait for process to exit
@@ -195,19 +210,31 @@ export async function spawnProcess(options: SubprocessOptions): Promise<Subproce
});
}
// Setup abort handling
// Setup abort handling with cleanup
let abortHandler: (() => void) | null = null;
const cleanupAbortListener = () => {
if (abortController && abortHandler) {
abortController.signal.removeEventListener('abort', abortHandler);
abortHandler = null;
}
};
if (abortController) {
abortController.signal.addEventListener('abort', () => {
abortHandler = () => {
cleanupAbortListener();
childProcess.kill('SIGTERM');
reject(new Error('Process aborted'));
});
};
abortController.signal.addEventListener('abort', abortHandler);
}
childProcess.on('exit', (code) => {
cleanupAbortListener();
resolve({ stdout, stderr, exitCode: code });
});
childProcess.on('error', (error) => {
cleanupAbortListener();
reject(error);
});
});