fix(agent-codex): reject pending requests before emitting error event

Move this.emit("error", err) after the pending-rejection loop in
handleProcessError, matching handleProcessExit's order. Without this,
emit("error") with no listeners throws synchronously, skipping
cleanup of pending requests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
suraj-markup 2026-02-25 12:08:00 +05:30
parent 89d2b5ead3
commit 3f81936467
2 changed files with 26 additions and 3 deletions

View File

@ -747,6 +747,28 @@ describe("CodexAppServerClient", () => {
expect(client.isConnected).toBe(false);
});
it("rejects pending requests before emitting error (no listener crash safe)", async () => {
const proc = createFakeProcess();
const client = new CodexAppServerClient({ requestTimeout: 5000 });
await connectClient(client, proc);
// Do NOT attach an error listener — emit("error") will throw
// Start a request that will be pending
const requestPromise = client.threadList();
await new Promise((r) => setTimeout(r, 10));
// Process emits error — without a listener, emit("error") throws.
// Pending requests must still be rejected before that throw.
try {
proc.emit("error", new Error("spawn ENOENT"));
} catch {
// Expected: uncaught error event from EventEmitter
}
await expect(requestPromise).rejects.toThrow("spawn ENOENT");
});
it("handles malformed JSON lines gracefully", async () => {
const proc = createFakeProcess();
const client = new CodexAppServerClient({ requestTimeout: 500 });

View File

@ -494,13 +494,14 @@ export class CodexAppServerClient extends EventEmitter {
this.readline = null;
}
this.emit("error", err);
// Reject all pending requests
// Reject all pending requests before emitting "error" — emit("error")
// with no listeners throws synchronously, which would skip cleanup.
for (const [id, pending] of this.pending) {
clearTimeout(pending.timer);
pending.reject(err);
this.pending.delete(id);
}
this.emit("error", err);
}
}