@@ -35,9 +35,12 @@ use std::process::Stdio;
3535use std:: sync:: Arc ;
3636use tokio:: io:: { AsyncBufReadExt , AsyncWriteExt , BufReader } ;
3737use tokio:: process:: Command ;
38- use tokio:: sync:: Semaphore ;
38+ use tokio:: sync:: { Mutex , Semaphore } ;
3939use tracing:: { error, info, warn} ;
4040
41+ const REVIEW_LOG_MAX_ENTRIES : usize = 2000 ;
42+ const REVIEW_LOG_STRING_LIMIT : usize = 12_000 ;
43+
4144#[ derive( Clone ) ]
4245struct ReviewContext {
4346 semaphore : Arc < Semaphore > ,
@@ -1473,6 +1476,72 @@ impl Reviewer {
14731476 }
14741477}
14751478
1479+ fn truncate_review_log_string ( s : & str ) -> String {
1480+ let redacted = redact_secret ( s) ;
1481+ if redacted. chars ( ) . count ( ) <= REVIEW_LOG_STRING_LIMIT {
1482+ return redacted;
1483+ }
1484+
1485+ let mut truncated: String = redacted. chars ( ) . take ( REVIEW_LOG_STRING_LIMIT ) . collect ( ) ;
1486+ truncated. push_str ( "\n ...[truncated]" ) ;
1487+ truncated
1488+ }
1489+
1490+ fn truncate_review_log_value ( value : & mut Value ) {
1491+ match value {
1492+ Value :: String ( s) => {
1493+ * s = truncate_review_log_string ( s) ;
1494+ }
1495+ Value :: Array ( values) => {
1496+ for value in values {
1497+ truncate_review_log_value ( value) ;
1498+ }
1499+ }
1500+ Value :: Object ( map) => {
1501+ for value in map. values_mut ( ) {
1502+ truncate_review_log_value ( value) ;
1503+ }
1504+ }
1505+ _ => { }
1506+ }
1507+ }
1508+
1509+ async fn push_review_log_entry ( log : & Arc < Mutex < Vec < Value > > > , mut entry : Value ) {
1510+ truncate_review_log_value ( & mut entry) ;
1511+
1512+ let mut entries = log. lock ( ) . await ;
1513+ if entries. len ( ) < REVIEW_LOG_MAX_ENTRIES {
1514+ entries. push ( entry) ;
1515+ } else if entries. len ( ) == REVIEW_LOG_MAX_ENTRIES {
1516+ entries. push ( json ! ( {
1517+ "role" : "system" ,
1518+ "content" : "Additional review log entries were omitted."
1519+ } ) ) ;
1520+ }
1521+ }
1522+
1523+ async fn push_review_log_text ( log : & Arc < Mutex < Vec < Value > > > , stream : & str , line : & str ) {
1524+ push_review_log_entry (
1525+ log,
1526+ json ! ( {
1527+ "role" : "system" ,
1528+ "kind" : "process_log" ,
1529+ "stream" : stream,
1530+ "content" : line
1531+ } ) ,
1532+ )
1533+ . await ;
1534+ }
1535+
1536+ async fn review_logs_json ( log : & Arc < Mutex < Vec < Value > > > ) -> Option < String > {
1537+ let entries = log. lock ( ) . await ;
1538+ if entries. is_empty ( ) {
1539+ None
1540+ } else {
1541+ serde_json:: to_string_pretty ( & * entries) . ok ( )
1542+ }
1543+ }
1544+
14761545#[ allow( clippy:: too_many_arguments) ]
14771546async fn run_review_tool (
14781547 patchset_id : i64 ,
@@ -1575,12 +1644,15 @@ async fn run_review_tool(
15751644 cmd. kill_on_drop ( true ) ;
15761645
15771646 let mut child = cmd. spawn ( ) ?;
1647+ let captured_log: Arc < Mutex < Vec < Value > > > = Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) ;
15781648
1579- if let Some ( stderr) = child. stderr . take ( ) {
1580- tokio:: spawn ( async move {
1649+ let stderr_task = if let Some ( stderr) = child. stderr . take ( ) {
1650+ let captured_log = captured_log. clone ( ) ;
1651+ Some ( tokio:: spawn ( async move {
15811652 let reader = BufReader :: new ( stderr) ;
15821653 let mut lines = reader. lines ( ) ;
15831654 while let Ok ( Some ( line) ) = lines. next_line ( ) . await {
1655+ push_review_log_text ( & captured_log, "stderr" , & line) . await ;
15841656 if line. contains ( " ERROR " )
15851657 || line. starts_with ( "Error:" )
15861658 || line. contains ( "panicked" )
@@ -1592,8 +1664,10 @@ async fn run_review_tool(
15921664 info ! ( "[review-bin] {}" , line) ;
15931665 }
15941666 }
1595- } ) ;
1596- }
1667+ } ) )
1668+ } else {
1669+ None
1670+ } ;
15971671
15981672 let mut stdin = child
15991673 . stdin
@@ -1665,6 +1739,26 @@ async fn run_review_tool(
16651739 serde_json:: from_value :: < AiRequest > ( payload_val. clone ( ) )
16661740 {
16671741 turn_count += 1 ;
1742+ let mut request_entry = req
1743+ . messages
1744+ . last ( )
1745+ . and_then ( |m| serde_json:: to_value ( m) . ok ( ) )
1746+ . unwrap_or_else ( || {
1747+ json ! ( {
1748+ "role" : "user" ,
1749+ "content" : "AI request contained no messages"
1750+ } )
1751+ } ) ;
1752+ if let Some ( obj) = request_entry. as_object_mut ( ) {
1753+ obj. insert (
1754+ "context_tag" . to_string ( ) ,
1755+ Value :: String (
1756+ req. context_tag . clone ( ) . unwrap_or_default ( ) ,
1757+ ) ,
1758+ ) ;
1759+ obj. insert ( "turn" . to_string ( ) , json ! ( turn_count) ) ;
1760+ }
1761+ push_review_log_entry ( & captured_log, request_entry) . await ;
16681762 if settings. ai . log_turns {
16691763 let n_msgs = req. messages . len ( ) ;
16701764 let last = req. messages . last ( ) ;
@@ -1726,6 +1820,21 @@ async fn run_review_tool(
17261820
17271821 let reply = match resp_payload {
17281822 Ok ( p) => {
1823+ let mut response_entry = serde_json:: to_value ( & p)
1824+ . unwrap_or_else ( |_| {
1825+ json ! ( {
1826+ "content" : "Failed to serialize AI response"
1827+ } )
1828+ } ) ;
1829+ if let Some ( obj) = response_entry. as_object_mut ( ) {
1830+ obj. insert (
1831+ "role" . to_string ( ) ,
1832+ Value :: String ( "assistant" . to_string ( ) ) ,
1833+ ) ;
1834+ obj. insert ( "turn" . to_string ( ) , json ! ( turn_count) ) ;
1835+ }
1836+ push_review_log_entry ( & captured_log, response_entry)
1837+ . await ;
17291838 if let Some ( usage) = & p. usage {
17301839 let cached = usage. cached_tokens . unwrap_or ( 0 ) ;
17311840 let uncached_input = usage. prompt_tokens . saturating_sub ( cached) ;
@@ -1791,6 +1900,14 @@ async fn run_review_tool(
17911900 Err ( e) => {
17921901 let message = e. to_string ( ) ;
17931902 let class = classify_ai_error ( & e) ;
1903+ push_review_log_entry (
1904+ & captured_log,
1905+ json ! ( {
1906+ "role" : "system" ,
1907+ "content" : format!( "AI provider error: {message}" )
1908+ } ) ,
1909+ )
1910+ . await ;
17941911 let payload = RemoteAiErrorPayload :: new ( message, class) ;
17951912 json ! ( { "type" : "error" , "payload" : payload } )
17961913 }
@@ -1809,6 +1926,8 @@ async fn run_review_tool(
18091926 if json_msg. get ( "patchset_id" ) . is_some ( ) {
18101927 final_result = Some ( json_msg) ;
18111928 break ;
1929+ } else {
1930+ push_review_log_text ( & captured_log, "stdout" , & line) . await ;
18121931 }
18131932 }
18141933 }
@@ -1817,10 +1936,13 @@ async fn run_review_tool(
18171936 if json_msg. get ( "patchset_id" ) . is_some ( ) {
18181937 final_result = Some ( json_msg) ;
18191938 break ;
1939+ } else {
1940+ push_review_log_text ( & captured_log, "stdout" , & line) . await ;
18201941 }
18211942 }
18221943 } else {
18231944 // Non-JSON line. Log it.
1945+ push_review_log_text ( & captured_log, "stdout" , & line) . await ;
18241946 warn ! ( "Review tool stdout: {}" , line) ;
18251947 }
18261948 }
@@ -1838,9 +1960,19 @@ async fn run_review_tool(
18381960 // Interaction finished (Success or Error inside interaction)
18391961 drop ( stdin) ; // Close stdin to signal EOF/finish to child if it's still running
18401962 let _ = child. wait ( ) . await ; // Reap zombie
1963+ if let Some ( task) = stderr_task {
1964+ let _ = task. await ;
1965+ }
18411966
18421967 match interaction_result {
1843- Ok ( json) => {
1968+ Ok ( mut json) => {
1969+ if json. get ( "history" ) . is_none ( )
1970+ && let Some ( logs) = review_logs_json ( & captured_log) . await
1971+ && let Ok ( history) = serde_json:: from_str :: < Value > ( & logs)
1972+ && let Some ( obj) = json. as_object_mut ( )
1973+ {
1974+ obj. insert ( "history" . to_string ( ) , history) ;
1975+ }
18441976 // Update DB with patch statuses if final_result available
18451977 if let Some ( patches) = json[ "patches" ] . as_array ( ) {
18461978 for p in patches {
@@ -1887,6 +2019,16 @@ async fn run_review_tool(
18872019 Ok ( json)
18882020 }
18892021 Err ( e) => {
2022+ if let Some ( logs) = review_logs_json ( & captured_log) . await
2023+ && let Err ( log_err) = db
2024+ . update_review_status ( review_id, ReviewStatus :: InReview . as_str ( ) , Some ( & logs) )
2025+ . await
2026+ {
2027+ warn ! (
2028+ "Failed to persist partial review log for review {}: {}" ,
2029+ review_id, log_err
2030+ ) ;
2031+ }
18902032 // Check if it's the specific active time exceeded error we throw in the loop
18912033 if e. to_string ( )
18922034 . contains ( "Review tool timed out (active time exceeded)" )
0 commit comments