@@ -21,6 +21,8 @@ use codex_app_server_protocol::ExecOneOffCommandResponse;
2121use  codex_app_server_protocol:: FuzzyFileSearchParams ; 
2222use  codex_app_server_protocol:: FuzzyFileSearchResponse ; 
2323use  codex_app_server_protocol:: GetAccountRateLimitsResponse ; 
24+ use  codex_app_server_protocol:: GetConversationSummaryParams ; 
25+ use  codex_app_server_protocol:: GetConversationSummaryResponse ; 
2426use  codex_app_server_protocol:: GetUserAgentResponse ; 
2527use  codex_app_server_protocol:: GetUserSavedConfigResponse ; 
2628use  codex_app_server_protocol:: GitDiffToRemoteResponse ; 
@@ -85,6 +87,7 @@ use codex_core::protocol::EventMsg;
8587use  codex_core:: protocol:: ExecApprovalRequestEvent ; 
8688use  codex_core:: protocol:: Op ; 
8789use  codex_core:: protocol:: ReviewDecision ; 
90+ use  codex_core:: read_head_records; 
8891use  codex_login:: ServerOptions  as  LoginServerOptions ; 
8992use  codex_login:: ShutdownHandle ; 
9093use  codex_login:: run_login_server; 
@@ -98,6 +101,8 @@ use codex_protocol::user_input::UserInput as CoreInputItem;
98101use  codex_utils_json_to_toml:: json_to_toml; 
99102use  std:: collections:: HashMap ; 
100103use  std:: ffi:: OsStr ; 
104+ use  std:: io:: Error  as  IoError ; 
105+ use  std:: path:: Path ; 
101106use  std:: path:: PathBuf ; 
102107use  std:: sync:: Arc ; 
103108use  std:: sync:: atomic:: AtomicBool ; 
@@ -170,6 +175,9 @@ impl CodexMessageProcessor {
170175                // created before processing any subsequent messages. 
171176                self . process_new_conversation ( request_id,  params) . await ; 
172177            } 
178+             ClientRequest :: GetConversationSummary  {  request_id,  params }  => { 
179+                 self . get_conversation_summary ( request_id,  params) . await ; 
180+             } 
173181            ClientRequest :: ListConversations  {  request_id,  params }  => { 
174182                self . handle_list_conversations ( request_id,  params) . await ; 
175183            } 
@@ -813,6 +821,39 @@ impl CodexMessageProcessor {
813821        } 
814822    } 
815823
824+     async  fn  get_conversation_summary ( 
825+         & self , 
826+         request_id :  RequestId , 
827+         params :  GetConversationSummaryParams , 
828+     )  { 
829+         let  GetConversationSummaryParams  {  rollout_path }  = params; 
830+         let  path = if  rollout_path. is_relative ( )  { 
831+             self . config . codex_home . join ( & rollout_path) 
832+         }  else  { 
833+             rollout_path. clone ( ) 
834+         } ; 
835+         let  fallback_provider = self . config . model_provider_id . as_str ( ) ; 
836+ 
837+         match  read_summary_from_rollout ( & path,  fallback_provider) . await  { 
838+             Ok ( summary)  => { 
839+                 let  response = GetConversationSummaryResponse  {  summary } ; 
840+                 self . outgoing . send_response ( request_id,  response) . await ; 
841+             } 
842+             Err ( err)  => { 
843+                 let  error = JSONRPCErrorError  { 
844+                     code :  INTERNAL_ERROR_CODE , 
845+                     message :  format ! ( 
846+                         "failed to load conversation summary from {}: {}" , 
847+                         path. display( ) , 
848+                         err
849+                     ) , 
850+                     data :  None , 
851+                 } ; 
852+                 self . outgoing . send_error ( request_id,  error) . await ; 
853+             } 
854+         } 
855+     } 
856+ 
816857    async  fn  handle_list_conversations ( 
817858        & self , 
818859        request_id :  RequestId , 
@@ -1644,6 +1685,34 @@ async fn on_exec_approval_response(
16441685    } 
16451686} 
16461687
1688+ async  fn  read_summary_from_rollout ( 
1689+     path :  & Path , 
1690+     fallback_provider :  & str , 
1691+ )  -> std:: io:: Result < ConversationSummary >  { 
1692+     let  head = read_head_records ( path,  1 ) . await ?; 
1693+ 
1694+     let  Some ( first)  = head. first ( )  else  { 
1695+         return  Err ( IoError :: other ( format ! ( 
1696+             "rollout at {} is empty" , 
1697+             path. display( ) 
1698+         ) ) ) ; 
1699+     } ; 
1700+ 
1701+     serde_json:: from_value :: < SessionMeta > ( first. clone ( ) ) . map_err ( |_| { 
1702+         IoError :: other ( format ! ( 
1703+             "rollout at {} does not start with session metadata" , 
1704+             path. display( ) 
1705+         ) ) 
1706+     } ) ?; 
1707+ 
1708+     extract_conversation_summary ( path. to_path_buf ( ) ,  & head,  fallback_provider) . ok_or_else ( || { 
1709+         IoError :: other ( format ! ( 
1710+             "rollout at {} is missing a user message" , 
1711+             path. display( ) 
1712+         ) ) 
1713+     } ) 
1714+ } 
1715+ 
16471716fn  extract_conversation_summary ( 
16481717    path :  PathBuf , 
16491718    head :  & [ serde_json:: Value ] , 
@@ -1730,14 +1799,15 @@ mod tests {
17301799        let  summary =
17311800            extract_conversation_summary ( path. clone ( ) ,  & head,  "test-provider" ) . expect ( "summary" ) ; 
17321801
1733-         assert_eq ! ( summary. conversation_id,  conversation_id) ; 
1734-         assert_eq ! ( 
1735-             summary. timestamp, 
1736-             Some ( "2025-09-05T16:53:11.850Z" . to_string( ) ) 
1737-         ) ; 
1738-         assert_eq ! ( summary. path,  path) ; 
1739-         assert_eq ! ( summary. preview,  "Count to 5" ) ; 
1740-         assert_eq ! ( summary. model_provider,  "test-provider" ) ; 
1802+         let  expected = ConversationSummary  { 
1803+             conversation_id, 
1804+             timestamp, 
1805+             path, 
1806+             preview :  "Count to 5" . to_string ( ) , 
1807+             model_provider :  "test-provider" . to_string ( ) , 
1808+         } ; 
1809+ 
1810+         assert_eq ! ( summary,  expected) ; 
17411811        Ok ( ( ) ) 
17421812    } 
17431813} 
0 commit comments