@@ -22,10 +22,14 @@ struct PerSessionTurnState {
2222 agent_message_item_id : Option < String > ,
2323 /// OpenCode part ID for the current text part (used to handle part removals/reset).
2424 agent_message_part_id : Option < String > ,
25+ /// Number of bytes already emitted for the current agent text part.
26+ agent_message_text_len : usize ,
2527 /// Stable item ID for the current reasoning stream.
2628 reasoning_item_id : Option < String > ,
2729 /// OpenCode part ID for the current reasoning part (used to route `message.part.delta` events).
2830 reasoning_part_id : Option < String > ,
31+ /// Number of bytes already emitted for the current reasoning part.
32+ reasoning_text_len : usize ,
2933 /// Stable item ID for the current user-message being assembled from SSE chunks.
3034 user_message_item_id : Option < String > ,
3135 /// Buffered text for the current user-message being assembled from SSE chunks.
@@ -126,8 +130,10 @@ impl SessionTranslationState {
126130 turn_state. tool_call_items . clear ( ) ;
127131 turn_state. agent_message_item_id = None ;
128132 turn_state. agent_message_part_id = None ;
133+ turn_state. agent_message_text_len = 0 ;
129134 turn_state. reasoning_item_id = None ;
130135 turn_state. reasoning_part_id = None ;
136+ turn_state. reasoning_text_len = 0 ;
131137 turn_state. user_message_item_id = preserved_user_message_item_id;
132138 turn_state. user_message_text = preserved_user_message_text;
133139 }
@@ -141,6 +147,9 @@ impl SessionTranslationState {
141147 turn_state. agent_message_item_id = None ;
142148 turn_state. reasoning_item_id = None ;
143149 turn_state. reasoning_part_id = None ;
150+ turn_state. reasoning_text_len = 0 ;
151+ turn_state. agent_message_part_id = None ;
152+ turn_state. agent_message_text_len = 0 ;
144153 // Preserve user_message_item_id so replay reuses the same ID
145154 // the live SSE translator already emitted (prevents duplicates).
146155 turn_state. user_message_text . clear ( ) ;
@@ -185,6 +194,7 @@ impl SessionTranslationState {
185194 let turn_state = self . get_turn_state_mut ( session_id) ;
186195 turn_state. agent_message_item_id = None ;
187196 turn_state. agent_message_part_id = None ;
197+ turn_state. agent_message_text_len = 0 ;
188198 }
189199
190200 fn remove_part_mapping ( & mut self , session_id : & str , part_id : & str ) {
@@ -196,10 +206,12 @@ impl SessionTranslationState {
196206 if turn_state. reasoning_part_id . as_deref ( ) == Some ( part_id) {
197207 turn_state. reasoning_part_id = None ;
198208 turn_state. reasoning_item_id = None ;
209+ turn_state. reasoning_text_len = 0 ;
199210 }
200211 if turn_state. agent_message_part_id . as_deref ( ) == Some ( part_id) {
201212 turn_state. agent_message_part_id = None ;
202213 turn_state. agent_message_item_id = None ;
214+ turn_state. agent_message_text_len = 0 ;
203215 }
204216 }
205217
@@ -209,8 +221,10 @@ impl SessionTranslationState {
209221 turn_state. tool_call_items . clear ( ) ;
210222 turn_state. agent_message_item_id = None ;
211223 turn_state. agent_message_part_id = None ;
224+ turn_state. agent_message_text_len = 0 ;
212225 turn_state. reasoning_item_id = None ;
213226 turn_state. reasoning_part_id = None ;
227+ turn_state. reasoning_text_len = 0 ;
214228 turn_state. user_message_item_id = None ;
215229 turn_state. user_message_text . clear ( ) ;
216230 }
@@ -233,8 +247,10 @@ impl SessionTranslationState {
233247 turn_state. tool_call_items . clear ( ) ;
234248 turn_state. agent_message_item_id = None ;
235249 turn_state. agent_message_part_id = None ;
250+ turn_state. agent_message_text_len = 0 ;
236251 turn_state. reasoning_item_id = None ;
237252 turn_state. reasoning_part_id = None ;
253+ turn_state. reasoning_text_len = 0 ;
238254 }
239255 }
240256
@@ -256,6 +272,19 @@ impl SessionTranslationState {
256272 }
257273}
258274
275+ fn unseen_suffix < ' a > ( full_text : & ' a str , emitted_len : usize ) -> Option < & ' a str > {
276+ if full_text. is_empty ( ) {
277+ return None ;
278+ }
279+ if emitted_len == 0 {
280+ return Some ( full_text) ;
281+ }
282+ if full_text. len ( ) <= emitted_len {
283+ return None ;
284+ }
285+ full_text. get ( emitted_len..) . filter ( |suffix| !suffix. is_empty ( ) )
286+ }
287+
259288fn parse_u64 ( value : Option < & Value > ) -> Option < u64 > {
260289 value
261290 . and_then ( |v| {
@@ -542,10 +571,20 @@ fn translate_part_updated(properties: &Value, state: &mut SessionTranslationStat
542571 // Emit a userMessage item so the prompt is visible immediately
543572 // (particularly important for subagent sessions whose prompts
544573 // aren't injected by send_user_message_core).
574+ let effective_text_owned;
545575 let effective_text = if delta. is_empty ( ) {
546- part. get ( "text" )
547- . and_then ( |v| v. as_str ( ) )
548- . unwrap_or_default ( )
576+ let full_text = part. get ( "text" ) . and_then ( |v| v. as_str ( ) ) . unwrap_or_default ( ) ;
577+ let current_user_text = state
578+ . get_turn_state ( & thread_id)
579+ . map ( |ts| ts. user_message_text . clone ( ) )
580+ . unwrap_or_default ( ) ;
581+ if full_text. starts_with ( current_user_text. as_str ( ) ) {
582+ let suffix = & full_text[ current_user_text. len ( ) ..] ;
583+ effective_text_owned = suffix. to_string ( ) ;
584+ effective_text_owned. as_str ( )
585+ } else {
586+ full_text
587+ }
549588 } else {
550589 delta
551590 } ;
@@ -575,18 +614,37 @@ fn translate_part_updated(properties: &Value, state: &mut SessionTranslationStat
575614 }
576615 if let Some ( pid) = part. get ( "id" ) . and_then ( |v| v. as_str ( ) ) {
577616 let turn_state = state. get_turn_state_mut ( & thread_id) ;
578- turn_state. agent_message_part_id = Some ( pid. to_string ( ) ) ;
617+ if turn_state. agent_message_part_id . as_deref ( ) != Some ( pid) {
618+ turn_state. agent_message_part_id = Some ( pid. to_string ( ) ) ;
619+ turn_state. agent_message_text_len = 0 ;
620+ }
579621 }
580622 let effective_delta = if delta. is_empty ( ) {
581- part. get ( "text" )
582- . and_then ( |v| v. as_str ( ) )
583- . unwrap_or_default ( )
623+ let full_text = part. get ( "text" ) . and_then ( |v| v. as_str ( ) ) . unwrap_or_default ( ) ;
624+ let emitted_len = state
625+ . get_turn_state ( & thread_id)
626+ . map ( |ts| ts. agent_message_text_len )
627+ . unwrap_or ( 0 ) ;
628+ match unseen_suffix ( full_text, emitted_len) {
629+ Some ( suffix) => suffix,
630+ None => return vec ! [ ] ,
631+ }
584632 } else {
585633 delta
586634 } ;
587635 if effective_delta. is_empty ( ) {
588636 return vec ! [ ] ;
589637 }
638+ if delta. is_empty ( ) {
639+ let full_len = part
640+ . get ( "text" )
641+ . and_then ( |v| v. as_str ( ) )
642+ . map ( |s| s. len ( ) )
643+ . unwrap_or ( effective_delta. len ( ) ) ;
644+ state. get_turn_state_mut ( & thread_id) . agent_message_text_len = full_len;
645+ } else {
646+ state. get_turn_state_mut ( & thread_id) . agent_message_text_len += effective_delta. len ( ) ;
647+ }
590648
591649 let item_id = state. agent_message_item ( & thread_id) ;
592650 vec ! [ json!( {
@@ -606,18 +664,37 @@ fn translate_part_updated(properties: &Value, state: &mut SessionTranslationStat
606664 }
607665 if let Some ( pid) = part. get ( "id" ) . and_then ( |v| v. as_str ( ) ) {
608666 let turn_state = state. get_turn_state_mut ( & thread_id) ;
609- turn_state. reasoning_part_id = Some ( pid. to_string ( ) ) ;
667+ if turn_state. reasoning_part_id . as_deref ( ) != Some ( pid) {
668+ turn_state. reasoning_part_id = Some ( pid. to_string ( ) ) ;
669+ turn_state. reasoning_text_len = 0 ;
670+ }
610671 }
611672 let effective_delta = if delta. is_empty ( ) {
612- part. get ( "text" )
613- . and_then ( |v| v. as_str ( ) )
614- . unwrap_or_default ( )
673+ let full_text = part. get ( "text" ) . and_then ( |v| v. as_str ( ) ) . unwrap_or_default ( ) ;
674+ let emitted_len = state
675+ . get_turn_state ( & thread_id)
676+ . map ( |ts| ts. reasoning_text_len )
677+ . unwrap_or ( 0 ) ;
678+ match unseen_suffix ( full_text, emitted_len) {
679+ Some ( suffix) => suffix,
680+ None => return vec ! [ ] ,
681+ }
615682 } else {
616683 delta
617684 } ;
618685 if effective_delta. is_empty ( ) {
619686 return vec ! [ ] ;
620687 }
688+ if delta. is_empty ( ) {
689+ let full_len = part
690+ . get ( "text" )
691+ . and_then ( |v| v. as_str ( ) )
692+ . map ( |s| s. len ( ) )
693+ . unwrap_or ( effective_delta. len ( ) ) ;
694+ state. get_turn_state_mut ( & thread_id) . reasoning_text_len = full_len;
695+ } else {
696+ state. get_turn_state_mut ( & thread_id) . reasoning_text_len += effective_delta. len ( ) ;
697+ }
621698 let item_id = state. reasoning_item ( & thread_id) ;
622699 vec ! [ json!( {
623700 "method" : "item/reasoning/textDelta" ,
@@ -751,6 +828,7 @@ fn translate_part_delta(properties: &Value, state: &mut SessionTranslationState)
751828
752829 if is_reasoning {
753830 let item_id = state. reasoning_item ( & thread_id) ;
831+ state. get_turn_state_mut ( & thread_id) . reasoning_text_len += delta. len ( ) ;
754832 return vec ! [ json!( {
755833 "method" : "item/reasoning/textDelta" ,
756834 "params" : {
@@ -763,6 +841,7 @@ fn translate_part_delta(properties: &Value, state: &mut SessionTranslationState)
763841 }
764842
765843 let item_id = state. agent_message_item ( & thread_id) ;
844+ state. get_turn_state_mut ( & thread_id) . agent_message_text_len += delta. len ( ) ;
766845 vec ! [ json!( {
767846 "method" : "item/agentMessage/delta" ,
768847 "params" : {
@@ -2429,6 +2508,62 @@ mod tests {
24292508 assert_eq ! ( events[ 0 ] [ "params" ] [ "delta" ] , "hello" ) ;
24302509 }
24312510
2511+ #[ test]
2512+ fn text_part_updated_uses_only_unseen_suffix_after_streamed_deltas ( ) {
2513+ let mut state = make_state ( ) ;
2514+
2515+ let delta1 = json ! ( {
2516+ "type" : "message.part.delta" ,
2517+ "properties" : {
2518+ "sessionID" : "ses_test123" ,
2519+ "messageID" : "msg_1" ,
2520+ "partID" : "prt_text_1" ,
2521+ "field" : "text" ,
2522+ "delta" : "I'm currently in Plan Mode (read-only), "
2523+ }
2524+ } ) ;
2525+ let events1 = translate_sse_event ( & delta1, & mut state) ;
2526+ assert_eq ! ( events1. len( ) , 1 ) ;
2527+ assert_eq ! ( events1[ 0 ] [ "params" ] [ "delta" ] , "I'm currently in Plan Mode (read-only), " ) ;
2528+
2529+ let delta2 = json ! ( {
2530+ "type" : "message.part.delta" ,
2531+ "properties" : {
2532+ "sessionID" : "ses_test123" ,
2533+ "messageID" : "msg_1" ,
2534+ "partID" : "prt_text_1" ,
2535+ "field" : "text" ,
2536+ "delta" : "so I can't make any file edits right now."
2537+ }
2538+ } ) ;
2539+ let events2 = translate_sse_event ( & delta2, & mut state) ;
2540+ assert_eq ! ( events2. len( ) , 1 ) ;
2541+ assert_eq ! (
2542+ events2[ 0 ] [ "params" ] [ "delta" ] ,
2543+ "so I can't make any file edits right now."
2544+ ) ;
2545+
2546+ let updated = json ! ( {
2547+ "type" : "message.part.updated" ,
2548+ "properties" : {
2549+ "part" : {
2550+ "type" : "text" ,
2551+ "id" : "prt_text_1" ,
2552+ "sessionID" : "ses_test123" ,
2553+ "messageID" : "msg_1" ,
2554+ "text" : "I'm currently in Plan Mode (read-only), so I can't make any file edits right now.\n \n To make edits, switch out of Plan Mode first."
2555+ }
2556+ }
2557+ } ) ;
2558+ let events3 = translate_sse_event ( & updated, & mut state) ;
2559+ assert_eq ! ( events3. len( ) , 1 ) ;
2560+ assert_eq ! ( events3[ 0 ] [ "method" ] , "item/agentMessage/delta" ) ;
2561+ assert_eq ! (
2562+ events3[ 0 ] [ "params" ] [ "delta" ] ,
2563+ "\n \n To make edits, switch out of Plan Mode first."
2564+ ) ;
2565+ }
2566+
24322567 #[ test]
24332568 fn user_text_part_updated_emits_user_message_item ( ) {
24342569 let mut state = make_state ( ) ;
0 commit comments