@@ -119,9 +119,12 @@ impl SessionTranslationState {
119119 }
120120
121121 /// Start a new turn for a specific session.
122+ ///
123+ /// INVARIANT: User message ID is always lowest in a turn. If not already set,
124+ /// we pre-allocate it here so any subsequent tool IDs are guaranteed higher.
122125 pub ( crate ) fn start_turn ( & mut self , session_id : String , turn_id : String ) {
123126 self . session_id = session_id. clone ( ) ;
124- let turn_state = self . session_turns . entry ( session_id) . or_default ( ) ;
127+ let turn_state = self . session_turns . entry ( session_id. clone ( ) ) . or_default ( ) ;
125128 // User prompt parts can arrive before `session.status=active` (common for
126129 // subagent sessions). Preserve the in-progress user message so later chunks
127130 // keep merging into the same frontend item instead of creating a duplicate.
@@ -135,8 +138,19 @@ impl SessionTranslationState {
135138 turn_state. reasoning_item_id = None ;
136139 turn_state. reasoning_part_id = None ;
137140 turn_state. reasoning_text_len = 0 ;
138- turn_state. user_message_item_id = preserved_user_message_item_id;
139- turn_state. user_message_text = preserved_user_message_text;
141+
142+ // INVARIANT: Pre-allocate user message ID if not already set.
143+ // This ensures user message ID is always lower than any tool IDs in this turn.
144+ if let Some ( id) = preserved_user_message_item_id {
145+ let turn_state = self . session_turns . get_mut ( & session_id) . unwrap ( ) ;
146+ turn_state. user_message_item_id = Some ( id) ;
147+ turn_state. user_message_text = preserved_user_message_text;
148+ } else {
149+ let pre_allocated_id = self . next_item_id ( ) ;
150+ let turn_state = self . session_turns . get_mut ( & session_id) . unwrap ( ) ;
151+ turn_state. user_message_item_id = Some ( pre_allocated_id) ;
152+ turn_state. user_message_text = String :: new ( ) ;
153+ }
140154 }
141155
142156 /// Prepare translation state for replaying historical messages.
@@ -2942,6 +2956,68 @@ mod tests {
29422956 ) ;
29432957 }
29442958
2959+ #[ test]
2960+ fn user_message_id_always_lower_than_tool_ids_in_turn ( ) {
2961+ let mut state = SessionTranslationState :: new ( "ses_test" . into ( ) ) ;
2962+
2963+ // Simulate turn starting with session.status=active
2964+ state. start_turn ( "ses_test" . into ( ) , "turn_1" . into ( ) ) ;
2965+
2966+ // Get user message ID (should be pre-allocated by start_turn)
2967+ let user_id = state. user_message_item ( "ses_test" ) ;
2968+
2969+ // Simulate tool calls getting IDs
2970+ let tool_id_1 = state. next_item_id ( ) ;
2971+ let tool_id_2 = state. next_item_id ( ) ;
2972+
2973+ // Parse sequence numbers
2974+ let user_seq: u64 = user_id. strip_prefix ( "item_" ) . unwrap ( ) . parse ( ) . unwrap ( ) ;
2975+ let tool_seq_1: u64 = tool_id_1. strip_prefix ( "item_" ) . unwrap ( ) . parse ( ) . unwrap ( ) ;
2976+ let tool_seq_2: u64 = tool_id_2. strip_prefix ( "item_" ) . unwrap ( ) . parse ( ) . unwrap ( ) ;
2977+
2978+ assert ! (
2979+ user_seq < tool_seq_1,
2980+ "User message ID ({user_id}) must be lower than first tool ID ({tool_id_1})"
2981+ ) ;
2982+ assert ! (
2983+ user_seq < tool_seq_2,
2984+ "User message ID ({user_id}) must be lower than second tool ID ({tool_id_2})"
2985+ ) ;
2986+ }
2987+
2988+ #[ test]
2989+ fn start_turn_preserves_existing_user_message_id ( ) {
2990+ let mut state = SessionTranslationState :: new ( "ses_test" . into ( ) ) ;
2991+
2992+ // Simulate user message arriving BEFORE session.status=active
2993+ let early_user_id = state. user_message_item ( "ses_test" ) ;
2994+
2995+ // Now turn starts - should preserve the existing user message ID
2996+ state. start_turn ( "ses_test" . into ( ) , "turn_1" . into ( ) ) ;
2997+
2998+ // Get user message ID again - should be the same
2999+ let after_start_id = state. user_message_item ( "ses_test" ) ;
3000+
3001+ assert_eq ! (
3002+ early_user_id, after_start_id,
3003+ "start_turn must preserve existing user message ID"
3004+ ) ;
3005+
3006+ // Tool IDs should still be higher
3007+ let tool_id = state. next_item_id ( ) ;
3008+ let user_seq: u64 = early_user_id
3009+ . strip_prefix ( "item_" )
3010+ . unwrap ( )
3011+ . parse ( )
3012+ . unwrap ( ) ;
3013+ let tool_seq: u64 = tool_id. strip_prefix ( "item_" ) . unwrap ( ) . parse ( ) . unwrap ( ) ;
3014+
3015+ assert ! (
3016+ user_seq < tool_seq,
3017+ "User message ID ({early_user_id}) must be lower than tool ID ({tool_id})"
3018+ ) ;
3019+ }
3020+
29453021 #[ test]
29463022 fn part_delta_routes_reasoning_by_part_id ( ) {
29473023 let mut state = make_state ( ) ;
0 commit comments