@@ -96,6 +96,7 @@ public final class CopilotSession implements AutoCloseable {
9696 private final AtomicReference <UserInputHandler > userInputHandler = new AtomicReference <>();
9797 private final AtomicReference <SessionHooks > hooksHandler = new AtomicReference <>();
9898 private volatile EventErrorHandler eventErrorHandler ;
99+ private volatile EventErrorPolicy eventErrorPolicy = EventErrorPolicy .CONTINUE ;
99100
100101 /**
101102 * Creates a new session with the given ID and RPC client.
@@ -158,12 +159,18 @@ public String getWorkspacePath() {
158159 * <p>
159160 * When an event handler registered via {@link #on(Consumer)} or
160161 * {@link #on(Class, Consumer)} throws an exception during event dispatch, the
161- * error handler is invoked instead of the default behavior (logging at
162- * {@link Level#SEVERE}) .
162+ * error handler is invoked with the event and exception. When no error handler
163+ * is set, exceptions are silently consumed .
163164 *
164165 * <p>
165- * If the error handler itself throws an exception, that exception is silently
166- * caught and logged to prevent cascading failures.
166+ * Whether dispatch continues or stops after an error is controlled by the
167+ * {@link EventErrorPolicy} set via {@link #setEventErrorPolicy}. The error
168+ * handler is always invoked regardless of the policy.
169+ *
170+ * <p>
171+ * If the error handler itself throws an exception, that exception is caught and
172+ * logged at {@link Level#SEVERE}, and dispatch is stopped regardless of the
173+ * configured policy.
167174 *
168175 * <p>
169176 * <b>Example:</b>
@@ -176,15 +183,53 @@ public String getWorkspacePath() {
176183 * }</pre>
177184 *
178185 * @param handler
179- * the error handler, or {@code null} to restore default logging
186+ * the error handler, or {@code null} to restore default silent
180187 * behavior
181188 * @see EventErrorHandler
189+ * @see #setEventErrorPolicy(EventErrorPolicy)
182190 * @since 1.0.8
183191 */
184192 public void setEventErrorHandler (EventErrorHandler handler ) {
185193 this .eventErrorHandler = handler ;
186194 }
187195
196+ /**
197+ * Sets the error propagation policy for event dispatch.
198+ * <p>
199+ * Controls whether remaining event listeners continue to execute when a
200+ * preceding listener throws an exception.
201+ *
202+ * <ul>
203+ * <li>{@link EventErrorPolicy#CONTINUE} (default) — dispatch to all remaining
204+ * listeners regardless of errors</li>
205+ * <li>{@link EventErrorPolicy#STOP} — stop dispatching after the first
206+ * error</li>
207+ * </ul>
208+ *
209+ * <p>
210+ * The configured {@link EventErrorHandler} (if any) is always invoked
211+ * regardless of the policy.
212+ *
213+ * <p>
214+ * <b>Example:</b>
215+ *
216+ * <pre>{@code
217+ * // Opt-in to short-circuit on first error
218+ * session.setEventErrorPolicy(EventErrorPolicy.STOP);
219+ * session.setEventErrorHandler(
220+ * (event, ex) -> logger.error("Handler failed, stopping dispatch: {}", ex.getMessage(), ex));
221+ * }</pre>
222+ *
223+ * @param policy
224+ * the error policy (default is {@link EventErrorPolicy#CONTINUE})
225+ * @see EventErrorPolicy
226+ * @see #setEventErrorHandler(EventErrorHandler)
227+ * @since 1.0.8
228+ */
229+ public void setEventErrorPolicy (EventErrorPolicy policy ) {
230+ this .eventErrorPolicy = policy ;
231+ }
232+
188233 /**
189234 * Sends a simple text message to the Copilot session.
190235 * <p>
@@ -330,8 +375,10 @@ public CompletableFuture<AssistantMessageEvent> sendAndWait(MessageOptions optio
330375 * instead.
331376 *
332377 * <p>
333- * <b>Exception isolation:</b> If a handler throws an exception, the error is
334- * logged and remaining handlers still execute.
378+ * <b>Exception handling:</b> If a handler throws an exception, the error is
379+ * routed to the configured {@link EventErrorHandler} (if set). Whether
380+ * remaining handlers execute depends on the configured
381+ * {@link EventErrorPolicy}.
335382 *
336383 * <p>
337384 * <b>Example:</b>
@@ -347,6 +394,7 @@ public CompletableFuture<AssistantMessageEvent> sendAndWait(MessageOptions optio
347394 * @return a Closeable that, when closed, unsubscribes the handler
348395 * @see #on(Class, Consumer)
349396 * @see AbstractSessionEvent
397+ * @see #setEventErrorPolicy(EventErrorPolicy)
350398 */
351399 public Closeable on (Consumer <AbstractSessionEvent > handler ) {
352400 eventHandlers .add (handler );
@@ -361,8 +409,10 @@ public Closeable on(Consumer<AbstractSessionEvent> handler) {
361409 * matching the specified type.
362410 *
363411 * <p>
364- * <b>Exception isolation:</b> If a handler throws an exception, the error is
365- * logged and remaining handlers still execute.
412+ * <b>Exception handling:</b> If a handler throws an exception, the error is
413+ * routed to the configured {@link EventErrorHandler} (if set). Whether
414+ * remaining handlers execute depends on the configured
415+ * {@link EventErrorPolicy}.
366416 *
367417 * <p>
368418 * <b>Example Usage</b>
@@ -409,16 +459,23 @@ public <T extends AbstractSessionEvent> Closeable on(Class<T> eventType, Consume
409459 * Dispatches an event to all registered handlers.
410460 * <p>
411461 * This is called internally when events are received from the server. Each
412- * handler is invoked in its own try/catch block so that an exception thrown by
413- * one handler does not prevent subsequent handlers from executing.
462+ * handler is invoked in its own try/catch block. Whether dispatch continues
463+ * after a handler error depends on the configured {@link EventErrorPolicy}:
464+ * <ul>
465+ * <li>{@link EventErrorPolicy#CONTINUE} (default) — remaining handlers still
466+ * execute</li>
467+ * <li>{@link EventErrorPolicy#STOP} — dispatch stops after the first error</li>
468+ * </ul>
414469 * <p>
415- * If a custom {@link EventErrorHandler} has been set via
416- * {@link #setEventErrorHandler(EventErrorHandler)}, it is called with the event
417- * and exception. Otherwise, exceptions are logged at {@link Level#SEVERE}.
470+ * The configured {@link EventErrorHandler} is always invoked (if set),
471+ * regardless of the policy. When no error handler is set, exceptions are
472+ * silently consumed. If the error handler itself throws, dispatch stops
473+ * regardless of policy and the error is logged at {@link Level#SEVERE}.
418474 *
419475 * @param event
420476 * the event to dispatch
421477 * @see #setEventErrorHandler(EventErrorHandler)
478+ * @see #setEventErrorPolicy(EventErrorPolicy)
422479 */
423480 void dispatchEvent (AbstractSessionEvent event ) {
424481 for (Consumer <AbstractSessionEvent > handler : eventHandlers ) {
@@ -431,9 +488,11 @@ void dispatchEvent(AbstractSessionEvent event) {
431488 errorHandler .handleError (event , e );
432489 } catch (Exception errorHandlerException ) {
433490 LOG .log (Level .SEVERE , "Error in event error handler" , errorHandlerException );
491+ break ; // error handler itself failed — stop regardless of policy
434492 }
435- } else {
436- LOG .log (Level .SEVERE , "Error in event handler" , e );
493+ }
494+ if (eventErrorPolicy == EventErrorPolicy .STOP ) {
495+ break ;
437496 }
438497 }
439498 }
0 commit comments