@@ -18,10 +18,10 @@ use super::semantic::{
1818 GenAISemanticEvent , LLMCall , LLMRequest , LLMResponse ,
1919 InputMessage , OutputMessage , MessagePart , TokenUsage ,
2020} ;
21+ use super :: id_resolver:: IdResolver ;
2122use std:: collections:: HashMap ;
2223use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
2324use std:: time:: { SystemTime , UNIX_EPOCH } ;
24- use sha2:: { Sha256 , Digest } ;
2525
2626/// Output from `GenAIBuilder::build()`, containing built events and deferred resolution info.
2727pub struct BuildOutput {
@@ -39,6 +39,9 @@ pub struct GenAIBuilder {
3939 session_prefix : String ,
4040 /// Counter for generating unique IDs within a session
4141 call_counter : AtomicU64 ,
42+ /// Resolver for `session_id` fallback / `conversation_id` based on the
43+ /// earliest `response_id` observed within a session / conversation.
44+ id_resolver : IdResolver ,
4245}
4346
4447impl Default for GenAIBuilder {
@@ -58,6 +61,7 @@ impl GenAIBuilder {
5861 GenAIBuilder {
5962 session_prefix : format ! ( "{:x}_{:x}" , ts, pid) ,
6063 call_counter : AtomicU64 :: new ( 0 ) ,
64+ id_resolver : IdResolver :: new ( ) ,
6165 }
6266 }
6367
@@ -179,9 +183,19 @@ impl GenAIBuilder {
179183 . and_then ( |v| v. as_bool ( ) )
180184 . unwrap_or ( false ) ;
181185
182- // Parse messages from body to compute session_id, conversation_id,
183- // user_query, and serialise input_messages / system_instructions
184- let ( session_id, conversation_id, user_query, input_messages, system_instructions) =
186+ // Parse messages from body to extract user_query / input_messages /
187+ // system_instructions / first_user_text / last_user_text。session_id 与
188+ // conversation_id 在 request 阶段采用双层兑底:
189+ // 1. 优先走 IdResolver::peek_*(同 PID 之前有过正常完成的调用 →
190+ // LRU 已 anchor 首个 response_id,复用后与正常路径完全对齐)。
191+ // 2. 未命中时 → `crash_fallback_id`以 (agent_name, pid, user_text) 作为
192+ // 兑底 ID 输入,保证 crash-drain 路径同 PID 同 user_query 的
193+ // crash 记录归一桶,不同 user_query 分桶。
194+ //
195+ // 正常响应到达后 `complete_pending` 仍会用 `IdResolver::resolve_*`
196+ // 重新计算并 UPDATE 正常 ID,只有 crash 路径才会保留这里写入的
197+ // peek/fallback 值。
198+ let ( user_query, input_messages, system_instructions, first_user_text, last_user_text) =
185199 if let Some ( ref v) = body {
186200 if let Some ( messages) = v. get ( "messages" ) . and_then ( |m| m. as_array ( ) ) {
187201 // Helper: extract text from "content" which can be either
@@ -210,33 +224,20 @@ impl GenAIBuilder {
210224 None
211225 } ;
212226
213- // session_id: SHA256 of first user message text (same logic
214- // as compute_session_id but operating on raw JSON values)
227+ // First user message raw text — used as `session_key` material
228+ // for IdResolver peek / crash fallback.
215229 let first_user_text = messages. iter ( )
216230 . filter ( |m| m. get ( "role" ) . and_then ( |r| r. as_str ( ) ) == Some ( "user" ) )
217231 . find_map ( |m| extract_text ( m) )
218232 . unwrap_or_default ( ) ;
219233
220- let session_id = if !first_user_text. is_empty ( ) {
221- let hash = Sha256 :: digest ( first_user_text. as_bytes ( ) ) ;
222- Some ( format ! ( "{:x}" , hash) [ ..32 ] . to_string ( ) )
223- } else {
224- None
225- } ;
226-
227- // Last user message raw text — used for both conversation_id
228- // (fingerprint hash) and user_query (display text)
234+ // Last user message raw text — used for user_query (display text)
235+ // 以及 conversation_key (peek / crash fallback)。
229236 let last_user_raw = messages. iter ( )
230237 . rev ( )
231238 . filter ( |m| m. get ( "role" ) . and_then ( |r| r. as_str ( ) ) == Some ( "user" ) )
232239 . find_map ( |m| extract_text ( m) ) ;
233-
234- // conversation_id: SHA256 of last user message raw text
235- // (same logic as compute_user_query_fingerprint)
236- let conversation_id = last_user_raw. as_deref ( ) . map ( |text| {
237- let hash = Sha256 :: digest ( text. as_bytes ( ) ) ;
238- format ! ( "{:x}" , hash) [ ..32 ] . to_string ( )
239- } ) ;
240+ let last_user_text = last_user_raw. clone ( ) . unwrap_or_default ( ) ;
240241
241242 // user_query: last user message text, stripped of metadata prefix
242243 let user_query = last_user_raw. as_deref ( )
@@ -261,13 +262,13 @@ impl GenAIBuilder {
261262 serde_json:: to_string ( & sys) . ok ( )
262263 } ;
263264
264- ( session_id , conversation_id , user_query , input_messages , system_instructions )
265+ ( user_query , input_messages , system_instructions , first_user_text , last_user_text )
265266 } else {
266267 // messages key missing or not an array
267- ( None , None , None , None , None )
268+ ( None , None , None , String :: new ( ) , String :: new ( ) )
268269 }
269270 } else {
270- ( None , None , None , None , None )
271+ ( None , None , None , String :: new ( ) , String :: new ( ) )
271272 } ;
272273
273274 // Extract model from request body JSON "model" field
@@ -284,13 +285,48 @@ impl GenAIBuilder {
284285 let agent_name = Self :: resolve_agent_name_from_comm ( & request. source_event . comm , conn_id. pid as u32 , pid_agent_name_cache)
285286 . or_else ( || Some ( request. source_event . comm_str ( ) ) ) ;
286287
288+ // 双层兑底计算 session_id / conversation_id(详见上方注释)。
289+ // 这里不使用 unwrap_or_else(|| “”) 是为了让“同 PID 同 agent”上下文下
290+ // crash_fallback_id 输入始终相同。
291+ let agent_name_str = agent_name. as_deref ( ) . unwrap_or ( "" ) ;
292+ let pid_i32 = conn_id. pid as i32 ;
293+
294+ let session_id = self
295+ . id_resolver
296+ . peek_session_id ( agent_name_str, pid_i32, & first_user_text)
297+ . or_else ( || {
298+ Some ( super :: id_resolver:: crash_fallback_id (
299+ "session" ,
300+ agent_name_str,
301+ pid_i32,
302+ & first_user_text,
303+ ) )
304+ } ) ;
305+ let conversation_id = self
306+ . id_resolver
307+ . peek_conversation_id ( agent_name_str, pid_i32, & last_user_text)
308+ . or_else ( || {
309+ Some ( super :: id_resolver:: crash_fallback_id (
310+ "conversation" ,
311+ agent_name_str,
312+ pid_i32,
313+ & last_user_text,
314+ ) )
315+ } ) ;
316+
287317 Some ( PendingCallInfo {
288318 call_id,
289319 trace_id : None , // LLM API response_id, not available until response
290- conversation_id, // User query fingerprint hash (from request body)
320+ // session_id / conversation_id 在请求阶段采用双层兑底:
321+ // 1) IdResolver::peek_* 复用同 PID 之前正常完成调用的 anchor,
322+ // 响应到达后 `complete_pending` 会用同样的值覆盖;
323+ // 2) LRU miss 时走 `crash_fallback_id`(`crash-` 前缀与正常 ID 隔离),
324+ // 进程崩溃不会走到 complete_pending 时该值会保留,供
325+ // handle_agent_crash_detection 按 (sid, cid) 分组。
326+ conversation_id,
291327 session_id,
292328 start_timestamp_ns : request. source_event . timestamp_ns ,
293- pid : conn_id . pid as i32 ,
329+ pid : pid_i32 ,
294330 process_name : request. source_event . comm . clone ( ) ,
295331 agent_name,
296332 http_method : Some ( request. method . clone ( ) ) ,
@@ -457,17 +493,10 @@ impl GenAIBuilder {
457493 . or_else ( || Self :: extract_model_from_body ( & http. request_body , & http. response_body ) )
458494 . unwrap_or_else ( || "unknown" . to_string ( ) ) ;
459495
460- // 在 request move 之前提取用户查询、fingerprint 和 session_id
461- let query_fp = Self :: compute_user_query_fingerprint ( & request) ;
496+ // 在 request move 之前提取用户查询 / first&last user message 原文
462497 let user_query = Self :: extract_last_user_query ( & request) ;
463- // session_id: 优先从 agent 自身的 session 获取(通过 response ID → .jsonl UUID 映射),
464- // fallback 到基于首条 user message 的 hash 计算
465- let response_id_val = parsed_message. as_ref ( ) . and_then ( |m| m. response_id ( ) ) . map ( |s| s. to_string ( ) ) ;
466- let mapper_session = response_id_val. as_deref ( )
467- . and_then ( |rid| response_mapper. get_session_by_response_id ( rid) )
468- . map ( |s| s. to_string ( ) ) ;
469- let session_id = mapper_session. clone ( )
470- . unwrap_or_else ( || Self :: compute_session_id ( & request) ) ;
498+ let first_user_raw = Self :: extract_first_user_raw ( & request) . unwrap_or_default ( ) ;
499+ let last_user_raw = Self :: extract_last_user_raw ( & request) . unwrap_or_default ( ) ;
471500
472501 // 提取 LLM API 的 response_id(如 chatcmpl-xxx),用作 trace_id
473502 // 同时作为 call_id 的首选值:trace_id 有值时直接复用,避免两套 ID;
@@ -476,6 +505,39 @@ impl GenAIBuilder {
476505 let call_id = response_id. clone ( ) . unwrap_or_else ( || internal_id. clone ( ) ) ;
477506 let response_id = response_id. unwrap_or_else ( || call_id. clone ( ) ) ;
478507
508+ // 提前解析 agent_name,后面调用 IdResolver 需要它作为 LRU key 维度,
509+ // 避免同机不同 agent 在同一 user query 下撞同一 session_id。
510+ let agent_name = Self :: resolve_agent_name ( & http. comm , http. pid , pid_agent_name_cache)
511+ . unwrap_or_else ( || http. comm . clone ( ) ) ;
512+ let pid_i32 = http. pid as i32 ;
513+
514+ // session_id: 优先从 agent 自身的 session 获取(通过 response ID → .jsonl UUID 映射),
515+ // 未命中时 fallback 到 `SHA256("session" + 该 session 内最早 response_id)`。
516+ let parsed_response_id = parsed_message. as_ref ( )
517+ . and_then ( |m| m. response_id ( ) )
518+ . map ( |s| s. to_string ( ) ) ;
519+ let mapper_session = parsed_response_id. as_deref ( )
520+ . and_then ( |rid| response_mapper. get_session_by_response_id ( rid) )
521+ . map ( |s| s. to_string ( ) ) ;
522+ let session_id = match mapper_session {
523+ Some ( uuid) => Some ( uuid) ,
524+ None => self . id_resolver . resolve_session_id (
525+ & agent_name,
526+ pid_i32,
527+ & first_user_raw,
528+ & response_id,
529+ ) ,
530+ } ;
531+
532+ // conversation_id: SHA256("conversation" + 该 conversation 内最早 response_id)
533+ let conversation_id = self . id_resolver . resolve_conversation_id (
534+ & agent_name,
535+ pid_i32,
536+ & last_user_raw,
537+ & response_id,
538+ ) ;
539+
540+
479541 // Extract error message from response body when status_code >= 400
480542 let error = if http. status_code >= 400 {
481543 http. response_body . as_ref ( ) . and_then ( |body| {
@@ -542,10 +604,9 @@ impl GenAIBuilder {
542604 response,
543605 token_usage,
544606 error,
545- pid : http . pid as i32 ,
607+ pid : pid_i32 ,
546608 process_name : http. comm . clone ( ) ,
547- agent_name : Self :: resolve_agent_name ( & http. comm , http. pid , pid_agent_name_cache)
548- . or_else ( || Some ( http. comm . clone ( ) ) ) ,
609+ agent_name : Some ( agent_name. clone ( ) ) ,
549610 metadata : {
550611 let mut meta = HashMap :: new ( ) ;
551612 meta. insert ( "method" . to_string ( ) , http. method ) ;
@@ -573,15 +634,19 @@ impl GenAIBuilder {
573634 meta. insert ( "operation_name" . to_string ( ) , "chat" . to_string ( ) ) ;
574635 }
575636 // conversation_id: 对话ID,同一 user query 触发的所有调用共享
576- meta. insert ( "conversation_id" . to_string ( ) , query_fp) ;
637+ if let Some ( ref cid) = conversation_id {
638+ meta. insert ( "conversation_id" . to_string ( ) , cid. clone ( ) ) ;
639+ }
577640 // response_id: LLM API 返回的响应 ID,用作 trace_id
578641 meta. insert ( "response_id" . to_string ( ) , response_id) ;
579642 // user_query: 用户实际输入的原文
580643 if let Some ( ref q) = user_query {
581644 meta. insert ( "user_query" . to_string ( ) , q. clone ( ) ) ;
582645 }
583646 // session_id: 同一 agent 进程的完整会话标识
584- meta. insert ( "session_id" . to_string ( ) , session_id) ;
647+ if let Some ( ref sid) = session_id {
648+ meta. insert ( "session_id" . to_string ( ) , sid. clone ( ) ) ;
649+ }
585650 meta
586651 } ,
587652 } )
@@ -1124,15 +1189,12 @@ impl GenAIBuilder {
11241189 format ! ( "{}_{}" , self . session_prefix, seq)
11251190 }
11261191
1127- /// 生成 session_id(32 位 hex)
1192+ /// 提取第一条有实际文本内容的 user message 的原始文本
11281193 ///
1129- /// 基于第一条 user message 原文生成,原文包含时间戳前缀如
1130- /// `[Tue 2026-03-31 17:19 GMT+8] 用户输入`,天然唯一。
1131- /// - 同一会话(含退出重进):第一条 user message 不变 → session_id 稳定
1132- /// - 新会话:时间戳不同 → session_id 不同
1133- fn compute_session_id ( request : & LLMRequest ) -> String {
1134- // 找第一条有实际文本的 user message(原始文本,含时间戳)
1135- let first_user_raw: String = request. messages . iter ( )
1194+ /// 仅返回含非空 `Text` 片段的首条 user message,供 `IdResolver`
1195+ /// 生成 session_key 使用。跳过只含 tool_result 等的 user message。
1196+ fn extract_first_user_raw ( request : & LLMRequest ) -> Option < String > {
1197+ request. messages . iter ( )
11361198 . filter ( |m| m. role == "user" )
11371199 . find_map ( |m| {
11381200 let text: String = m. parts . iter ( )
@@ -1144,10 +1206,6 @@ impl GenAIBuilder {
11441206 . join ( "\n " ) ;
11451207 if text. is_empty ( ) { None } else { Some ( text) }
11461208 } )
1147- . unwrap_or_default ( ) ;
1148-
1149- let hash = Sha256 :: digest ( first_user_raw. as_bytes ( ) ) ;
1150- format ! ( "{:x}" , hash) [ ..32 ] . to_string ( )
11511209 }
11521210
11531211 /// 提取最后一条有实际文本内容的 user message 的原始文本
@@ -1204,20 +1262,6 @@ impl GenAIBuilder {
12041262 }
12051263 text. to_string ( )
12061264 }
1207-
1208- /// 计算 user query 的 fingerprint,用于关联同一个请求的调用链
1209- ///
1210- /// 使用原始文本(包含时间戳前缀)计算 hash,
1211- /// 这样相同命令在不同时间发送也会产生不同的 fingerprint
1212- fn compute_user_query_fingerprint ( request : & LLMRequest ) -> String {
1213- match Self :: extract_last_user_raw ( request) {
1214- Some ( content) => {
1215- let hash = Sha256 :: digest ( content. as_bytes ( ) ) ;
1216- format ! ( "{:x}" , hash) [ ..32 ] . to_string ( )
1217- }
1218- None => "no_user_query" . to_string ( ) ,
1219- }
1220- }
12211265
12221266 /// Resolve agent name from comm string only (no /proc access).
12231267 /// Used for dead-PID drain where the process is already gone.
@@ -1597,75 +1641,6 @@ mod tests {
15971641 assert_eq ! ( GenAIBuilder :: strip_user_query_prefix( text) , "[not a timestamp] content" ) ;
15981642 }
15991643
1600- #[ test]
1601- fn test_compute_session_id_deterministic ( ) {
1602- let req = LLMRequest {
1603- messages : vec ! [ InputMessage {
1604- role: "user" . to_string( ) ,
1605- parts: vec![ MessagePart :: Text { content: "hello" . to_string( ) } ] ,
1606- name: None ,
1607- } ] ,
1608- temperature : None , max_tokens : None , frequency_penalty : None ,
1609- presence_penalty : None , top_p : None , top_k : None , seed : None ,
1610- stop_sequences : None , stream : false , tools : None , raw_body : None ,
1611- } ;
1612- let id1 = GenAIBuilder :: compute_session_id ( & req) ;
1613- let id2 = GenAIBuilder :: compute_session_id ( & req) ;
1614- assert_eq ! ( id1, id2) ;
1615- assert_eq ! ( id1. len( ) , 32 ) ;
1616- }
1617-
1618- #[ test]
1619- fn test_compute_session_id_no_user ( ) {
1620- let req = LLMRequest {
1621- messages : vec ! [ InputMessage {
1622- role: "system" . to_string( ) ,
1623- parts: vec![ MessagePart :: Text { content: "sys" . to_string( ) } ] ,
1624- name: None ,
1625- } ] ,
1626- temperature : None , max_tokens : None , frequency_penalty : None ,
1627- presence_penalty : None , top_p : None , top_k : None , seed : None ,
1628- stop_sequences : None , stream : false , tools : None , raw_body : None ,
1629- } ;
1630- let id = GenAIBuilder :: compute_session_id ( & req) ;
1631- assert_eq ! ( id. len( ) , 32 ) ; // still produces 32-char hash of empty string
1632- }
1633-
1634- #[ test]
1635- fn test_compute_user_query_fingerprint ( ) {
1636- let req = LLMRequest {
1637- messages : vec ! [
1638- InputMessage {
1639- role: "user" . to_string( ) ,
1640- parts: vec![ MessagePart :: Text { content: "first" . to_string( ) } ] ,
1641- name: None ,
1642- } ,
1643- InputMessage {
1644- role: "user" . to_string( ) ,
1645- parts: vec![ MessagePart :: Text { content: "second" . to_string( ) } ] ,
1646- name: None ,
1647- } ,
1648- ] ,
1649- temperature : None , max_tokens : None , frequency_penalty : None ,
1650- presence_penalty : None , top_p : None , top_k : None , seed : None ,
1651- stop_sequences : None , stream : false , tools : None , raw_body : None ,
1652- } ;
1653- let fp = GenAIBuilder :: compute_user_query_fingerprint ( & req) ;
1654- assert_eq ! ( fp. len( ) , 32 ) ;
1655- // fingerprint uses last user message
1656- let req2 = LLMRequest {
1657- messages : vec ! [ InputMessage {
1658- role: "user" . to_string( ) ,
1659- parts: vec![ MessagePart :: Text { content: "second" . to_string( ) } ] ,
1660- name: None ,
1661- } ] ,
1662- temperature : None , max_tokens : None , frequency_penalty : None ,
1663- presence_penalty : None , top_p : None , top_k : None , seed : None ,
1664- stop_sequences : None , stream : false , tools : None , raw_body : None ,
1665- } ;
1666- assert_eq ! ( fp, GenAIBuilder :: compute_user_query_fingerprint( & req2) ) ;
1667- }
1668-
16691644 #[ test]
16701645 fn test_extract_last_user_query ( ) {
16711646 let req = LLMRequest {
0 commit comments