@@ -394,6 +394,7 @@ pub(crate) fn translate_sse_event(
394394 "session.updated" => translate_session_created_or_updated ( properties, state, true ) ,
395395 "session.status" => translate_session_status ( properties, state) ,
396396 "session.idle" => translate_session_idle ( properties, state) ,
397+ "session.error" => translate_session_error ( properties, state) ,
397398 "permission.asked" => translate_sse_permission ( properties, state) ,
398399 "question.asked" => translate_question_asked ( properties, state) ,
399400 "question.replied" => translate_question_completed ( properties) ,
@@ -1447,13 +1448,16 @@ fn translate_session_status(properties: &Value, state: &mut SessionTranslationSt
14471448 vec ! [ build_turn_started( & thread_id, & synthetic_turn_id) ]
14481449 }
14491450 "idle" => {
1450- if turn_id . is_empty ( ) {
1451+ if thread_id . is_empty ( ) {
14511452 return vec ! [ ] ;
14521453 }
14531454 let mut events = Vec :: new ( ) ;
14541455 if let Some ( msg_completed) = build_agent_message_completed ( state, & thread_id) {
14551456 events. push ( msg_completed) ;
14561457 }
1458+ // Background helper prompts can complete without ever emitting an
1459+ // "active" status, so still emit turn/completed with an empty turn
1460+ // id to unblock shared background collectors.
14571461 events. push ( build_turn_completed ( & thread_id, & turn_id) ) ;
14581462 state. finish_turn ( & thread_id) ;
14591463 events
@@ -1518,6 +1522,52 @@ fn translate_session_idle(properties: &Value, state: &mut SessionTranslationStat
15181522 events
15191523}
15201524
1525+ fn translate_session_error ( properties : & Value , state : & mut SessionTranslationState ) -> Vec < Value > {
1526+ let session_id = properties
1527+ . get ( "sessionID" )
1528+ . or_else ( || properties. get ( "session_id" ) )
1529+ . or_else ( || properties. get ( "id" ) )
1530+ . and_then ( |v| v. as_str ( ) )
1531+ . unwrap_or_default ( ) ;
1532+
1533+ if !session_id. is_empty ( ) {
1534+ state. session_id = session_id. to_string ( ) ;
1535+ }
1536+
1537+ let thread_id = state. session_id . clone ( ) ;
1538+ if thread_id. is_empty ( ) {
1539+ return vec ! [ ] ;
1540+ }
1541+
1542+ let turn_id = state
1543+ . get_turn_state ( & thread_id)
1544+ . map ( |ts| ts. turn_id . clone ( ) )
1545+ . unwrap_or_default ( ) ;
1546+
1547+ let error = properties. get ( "error" ) . unwrap_or ( & Value :: Null ) ;
1548+ let error_msg = error
1549+ . get ( "data" )
1550+ . and_then ( |data| data. get ( "message" ) )
1551+ . or_else ( || error. get ( "message" ) )
1552+ . or_else ( || error. get ( "error" ) )
1553+ . or_else ( || error. get ( "name" ) )
1554+ . and_then ( |v| v. as_str ( ) )
1555+ . unwrap_or ( "unknown error" ) ;
1556+
1557+ state. finish_turn ( & thread_id) ;
1558+ vec ! [ json!( {
1559+ "method" : "error" ,
1560+ "params" : {
1561+ "threadId" : thread_id,
1562+ "turnId" : turn_id,
1563+ "willRetry" : false ,
1564+ "error" : {
1565+ "message" : error_msg
1566+ }
1567+ }
1568+ } ) ]
1569+ }
1570+
15211571// ---------------------------------------------------------------------------
15221572// permission.updated — permission requests from the agent
15231573// ---------------------------------------------------------------------------
@@ -2441,6 +2491,48 @@ mod tests {
24412491 assert ! ( events. iter( ) . any( |e| e[ "method" ] == "turn/completed" ) ) ;
24422492 }
24432493
2494+ #[ test]
2495+ fn session_status_idle_without_active_turn_still_completes ( ) {
2496+ let mut state = SessionTranslationState :: new ( String :: new ( ) ) ;
2497+ let event = json ! ( {
2498+ "type" : "session.status" ,
2499+ "properties" : {
2500+ "sessionID" : "ses_background_1" ,
2501+ "status" : { "type" : "idle" }
2502+ }
2503+ } ) ;
2504+
2505+ let events = translate_sse_event ( & event, & mut state) ;
2506+ assert_eq ! ( events. len( ) , 1 ) ;
2507+ assert_eq ! ( events[ 0 ] [ "method" ] , "turn/completed" ) ;
2508+ assert_eq ! ( events[ 0 ] [ "params" ] [ "threadId" ] , "ses_background_1" ) ;
2509+ assert_eq ! ( events[ 0 ] [ "params" ] [ "turn" ] [ "id" ] , "" ) ;
2510+ }
2511+
2512+ #[ test]
2513+ fn session_error_produces_error_event_without_active_turn ( ) {
2514+ let mut state = SessionTranslationState :: new ( String :: new ( ) ) ;
2515+ let event = json ! ( {
2516+ "type" : "session.error" ,
2517+ "properties" : {
2518+ "sessionID" : "ses_background_2" ,
2519+ "error" : {
2520+ "name" : "BadRequestError" ,
2521+ "data" : {
2522+ "message" : "model not available"
2523+ }
2524+ }
2525+ }
2526+ } ) ;
2527+
2528+ let events = translate_sse_event ( & event, & mut state) ;
2529+ assert_eq ! ( events. len( ) , 1 ) ;
2530+ assert_eq ! ( events[ 0 ] [ "method" ] , "error" ) ;
2531+ assert_eq ! ( events[ 0 ] [ "params" ] [ "threadId" ] , "ses_background_2" ) ;
2532+ assert_eq ! ( events[ 0 ] [ "params" ] [ "turnId" ] , "" ) ;
2533+ assert_eq ! ( events[ 0 ] [ "params" ] [ "error" ] [ "message" ] , "model not available" ) ;
2534+ }
2535+
24442536 #[ test]
24452537 fn session_status_active_produces_turn_started_when_missing ( ) {
24462538 let mut state = SessionTranslationState :: new ( String :: new ( ) ) ;
0 commit comments