1010import io .a2a .server .tasks .TaskStore ;
1111import io .a2a .spec .A2AServerException ;
1212import io .a2a .spec .Event ;
13+ import io .a2a .spec .InternalError ;
1314import io .a2a .spec .Task ;
1415import io .a2a .spec .TaskArtifactUpdateEvent ;
1516import io .a2a .spec .TaskStatusUpdateEvent ;
@@ -140,39 +141,66 @@ private void processEvent(MainEventBusContext context) {
140141 LOGGER .debug ("MainEventBusProcessor: Processing event for task {}: {} (queue type: {})" ,
141142 taskId , event .getClass ().getSimpleName (), eventQueue .getClass ().getSimpleName ());
142143
144+ Event eventToDistribute = null ;
143145 try {
144146 // Step 1: Update TaskStore FIRST (persistence before clients see it)
145- updateTaskStore ( taskId , event );
147+ // If this throws, we distribute an error to ensure "persist before client visibility"
146148
147- // Step 2: Send push notification AFTER persistence (ensures notification sees latest state)
148- sendPushNotification (taskId );
149+ try {
150+ updateTaskStore (taskId , event );
151+ eventToDistribute = event ; // Success - distribute original event
152+ } catch (InternalError e ) {
153+ // Persistence failed - create error event to distribute instead
154+ LOGGER .error ("Failed to persist event for task {}, distributing error to clients" , taskId , e );
155+ String errorMessage = "Failed to persist event: " + e .getMessage ();
156+ eventToDistribute = e ;
157+ } catch (Exception e ) {
158+ LOGGER .error ("Failed to persist event for task {}, distributing error to clients" , taskId , e );
159+ String errorMessage = "Failed to persist event: " + e .getMessage ();
160+ eventToDistribute = new InternalError (errorMessage );
161+ }
162+
163+ // Step 2: Send push notification AFTER successful persistence
164+ if (eventToDistribute == event ) {
165+ sendPushNotification (taskId );
166+ }
149167
150- // Step 3: Then distribute to ChildQueues (clients see it AFTER persistence + notification )
168+ // Step 3: Then distribute to ChildQueues (clients see either event or error AFTER persistence attempt )
151169 if (eventQueue instanceof EventQueue .MainQueue mainQueue ) {
152170 int childCount = mainQueue .getChildCount ();
153- LOGGER .debug ("MainEventBusProcessor: Distributing event to {} children for task {}" , childCount , taskId );
154- mainQueue .distributeToChildren (context .eventQueueItem ());
155- LOGGER .debug ("MainEventBusProcessor: Distributed event {} to {} children for task {}" ,
156- event .getClass ().getSimpleName (), childCount , taskId );
171+ LOGGER .debug ("MainEventBusProcessor: Distributing {} to {} children for task {}" ,
172+ eventToDistribute .getClass ().getSimpleName (), childCount , taskId );
173+ // Create new EventQueueItem with the event to distribute (original or error)
174+ EventQueueItem itemToDistribute = new LocalEventQueueItem (eventToDistribute );
175+ mainQueue .distributeToChildren (itemToDistribute );
176+ LOGGER .debug ("MainEventBusProcessor: Distributed {} to {} children for task {}" ,
177+ eventToDistribute .getClass ().getSimpleName (), childCount , taskId );
157178 } else {
158179 LOGGER .warn ("MainEventBusProcessor: Expected MainQueue but got {} for task {}" ,
159180 eventQueue .getClass ().getSimpleName (), taskId );
160181 }
161182
162183 LOGGER .debug ("MainEventBusProcessor: Completed processing event for task {}" , taskId );
163184
164- // Step 4: Notify callback after all processing is complete
165- callback .onEventProcessed (taskId , event );
166-
167- // Step 5: If this is a final event, notify task finalization
168- if (isFinalEvent (event )) {
169- callback .onTaskFinalized (taskId );
170- }
171185 } finally {
172- // ALWAYS release semaphore, even if processing fails
173- // Balances the acquire() in MainQueue.enqueueEvent()
174- if (eventQueue instanceof EventQueue .MainQueue mainQueue ) {
175- mainQueue .releaseSemaphore ();
186+ try {
187+ // Step 4: Notify callback after all processing is complete
188+ // Call callback with the distributed event (original or error)
189+ if (eventToDistribute != null ) {
190+ callback .onEventProcessed (taskId , eventToDistribute );
191+
192+ // Step 5: If this is a final event, notify task finalization
193+ // Only for successful persistence (not for errors)
194+ if (eventToDistribute == event && isFinalEvent (event )) {
195+ callback .onTaskFinalized (taskId );
196+ }
197+ }
198+ } finally {
199+ // ALWAYS release semaphore, even if processing fails
200+ // Balances the acquire() in MainQueue.enqueueEvent()
201+ if (eventQueue instanceof EventQueue .MainQueue mainQueue ) {
202+ mainQueue .releaseSemaphore ();
203+ }
176204 }
177205 }
178206 }
@@ -184,8 +212,15 @@ private void processEvent(MainEventBusContext context) {
184212 * which handles all event types (Task, TaskStatusUpdateEvent, TaskArtifactUpdateEvent).
185213 * This leverages existing TaskManager logic for status updates, artifact appending, message history, etc.
186214 * </p>
215+ * <p>
216+ * If persistence fails, the exception is propagated to processEvent() which distributes an
217+ * InternalError to clients instead of the original event, ensuring "persist before visibility".
218+ * See Gemini's comment: https://github.com/a2aproject/a2a-java/pull/515#discussion_r2604621833
219+ * </p>
220+ *
221+ * @throws InternalError if persistence fails
187222 */
188- private void updateTaskStore (String taskId , Event event ) {
223+ private void updateTaskStore (String taskId , Event event ) throws InternalError {
189224 try {
190225 // Extract contextId from event (all relevant events have it)
191226 String contextId = extractContextId (event );
@@ -197,12 +232,14 @@ private void updateTaskStore(String taskId, Event event) {
197232 taskManager .process (event );
198233 LOGGER .debug ("TaskStore updated via TaskManager.process() for task {}: {}" ,
199234 taskId , event .getClass ().getSimpleName ());
200- } catch (A2AServerException e ) {
235+ } catch (InternalError e ) {
201236 LOGGER .error ("Error updating TaskStore via TaskManager for task {}" , taskId , e );
202- // Don't rethrow - we still want to distribute to ChildQueues
237+ // Rethrow to prevent distributing unpersisted event to clients
238+ throw e ;
203239 } catch (Exception e ) {
204240 LOGGER .error ("Unexpected error updating TaskStore for task {}" , taskId , e );
205- // Don't rethrow - we still want to distribute to ChildQueues
241+ // Rethrow to prevent distributing unpersisted event to clients
242+ throw new InternalError ("TaskStore persistence failed: " + e .getMessage ());
206243 }
207244 }
208245
0 commit comments