fix(session): isolate event listener failures in agent fan-out

Both `AgentSession.#emit` (session/agent-session.ts) and `Agent.#emit`
(packages/agent/src/agent.ts) iterated listeners with no error isolation.
A synchronous throw in any subscriber aborted the for-loop, so later
subscribers (TUI rendering, ACP bridge, task executor progress,
hindsight) silently missed events. Many listeners — see
`modes/controllers/event-controller.ts:141` and
`modes/controllers/input-controller.ts:576` — are registered as
`async (event) => { await this.handleEvent(event); }`; the returned
Promise was dropped, so any rejection became an unhandled rejection.

Wrap each listener invocation in try/catch and attach a `.catch` to any
returned thenable. Errors are logged via `logger.warn` (already imported
in agent-session.ts) and `console.error` (agent.ts has no logger
dependency, keep it that way).

Test: new `test/session/emit-listener-isolation.test.ts` registers two
listeners on both classes; first listener throws (or returns a rejecting
Promise); asserts the second listener still receives the event AND no
`unhandledRejection` fires. 4 cases (sync+async × Agent+AgentSession).
All fail on current main; all pass with the fix.
This commit is contained in:
oldschoola
2026-05-26 23:26:15 -07:00
parent 159f2d8fb1
commit 08bccfe0ec
3 changed files with 171 additions and 3 deletions
@@ -1307,10 +1307,25 @@ export class AgentSession {
/** Emit an event to all listeners */
#emit(event: AgentSessionEvent): void {
// Copy array before iteration to avoid mutation during iteration
// Copy array before iteration to avoid mutation during iteration.
const listeners = [...this.#eventListeners];
for (const l of listeners) {
l(event);
try {
const result = l(event);
// Listener may be an async function whose returned Promise we don't await;
// attach a catch so a rejection does not become an unhandled rejection.
if (result && typeof (result as Promise<unknown>).then === "function") {
(result as Promise<unknown>).catch(err => {
logger.warn("AgentSession listener rejected", {
error: err instanceof Error ? err.message : String(err),
});
});
}
} catch (err) {
logger.warn("AgentSession listener threw", {
error: err instanceof Error ? err.message : String(err),
});
}
}
}