2424import java .util .List ;
2525import java .util .Map ;
2626import java .util .Objects ;
27+ import java .util .Set ;
2728import java .util .concurrent .CompletableFuture ;
2829import java .util .concurrent .CompletionException ;
2930import java .util .concurrent .CompletionStage ;
@@ -74,6 +75,7 @@ class CodexAppServerTurn implements WebSocket.Listener {
7475 private final AtomicLong rpcIds = new AtomicLong ();
7576 private final StringBuilder content = new StringBuilder ();
7677 private final StringBuilder frameBuffer = new StringBuilder ();
78+ private final Set <String > streamedAgentItemIds = ConcurrentHashMap .newKeySet ();
7779 /** Records outgoing RPC payloads; without a socket they are only recorded, for contract tests. */
7880 private final List <String > sentMessages = new ArrayList <>();
7981
@@ -246,6 +248,7 @@ void handleFrame(String frame) {
246248 JsonNode params = node .path ("params" );
247249 switch (method ) {
248250 case CodexAppServerProtocol .TURN_STARTED -> onTurnStarted (params );
251+ case CodexAppServerProtocol .AGENT_MESSAGE_DELTA -> onAgentMessageDelta (params );
249252 case CodexAppServerProtocol .ITEM_COMPLETED -> onItemCompleted (params );
250253 case CodexAppServerProtocol .TURN_COMPLETED -> onTurnCompleted (params );
251254 case CodexAppServerProtocol .TURN_FAILED -> completeError (new CodexAppServerException (
@@ -266,6 +269,22 @@ private void onTurnStarted(JsonNode params) {
266269 }
267270 }
268271
272+ private void onAgentMessageDelta (JsonNode params ) {
273+ String delta = firstText (params , "delta" );
274+ if (!hasText (delta )) {
275+ return ;
276+ }
277+ String itemId = firstText (params , "itemId" , "item_id" );
278+ if (hasText (itemId )) {
279+ streamedAgentItemIds .add (itemId );
280+ }
281+ String applied = truncateToContentCap (delta );
282+ content .append (applied );
283+ if (!applied .isEmpty () && Objects .nonNull (request .getOnDelta ())) {
284+ request .getOnDelta ().accept (applied );
285+ }
286+ }
287+
269288 private void onItemCompleted (JsonNode params ) {
270289 JsonNode item = params .path ("item" );
271290 String type = firstText (item , "type" , "itemType" );
@@ -277,6 +296,10 @@ private void onItemCompleted(JsonNode params) {
277296 if (!hasText (text )) {
278297 return ;
279298 }
299+ String itemId = firstText (item , "id" , "itemId" , "item_id" );
300+ if (hasText (itemId ) && streamedAgentItemIds .contains (itemId )) {
301+ return ;
302+ }
280303 String applied = truncateToContentCap (text );
281304 content .append (applied );
282305 if (!applied .isEmpty () && Objects .nonNull (request .getOnDelta ())) {
0 commit comments