Skip to content

Commit 5b30870

Browse files
committed
review: expose failed review diagnostics in raw logs
Review failures were stored as review-level result descriptions, while the patchset page mostly surfaced patchset.failed_reason. When the review worker failed before producing a final JSON payload, the raw log page could also be empty because no interaction logs had been persisted. Keep patchset details concise and route failure diagnosis through the existing raw-log link. Make the raw log page show the final failure reason above the interaction log, then fall back to saved raw output or input context when no structured interaction log was recorded. Capture partial stdout, stderr, and AI turn summaries while the review tool runs, keep those logs if completion later stores no log payload, and store process output with explicit process-log metadata so it can be rendered separately from AI interactions. Signed-off-by: Muchun Song <songmuchun@bytedance.com>
1 parent a45fb6e commit 5b30870

3 files changed

Lines changed: 417 additions & 83 deletions

File tree

src/db.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,7 @@ impl Database {
762762
) -> Result<()> {
763763
self.conn
764764
.execute(
765-
"UPDATE reviews SET status = ?, result_description = ?, summary = ?, interaction_id = ?, inline_review = ?, logs = ? WHERE id = ?",
765+
"UPDATE reviews SET status = ?, result_description = ?, summary = ?, interaction_id = ?, inline_review = ?, logs = COALESCE(?, logs) WHERE id = ?",
766766
libsql::params![status, result, summary, interaction_id, inline_review, logs, review_id],
767767
)
768768
.await?;

src/reviewer.rs

Lines changed: 148 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,12 @@ use std::process::Stdio;
3535
use std::sync::Arc;
3636
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
3737
use tokio::process::Command;
38-
use tokio::sync::Semaphore;
38+
use tokio::sync::{Mutex, Semaphore};
3939
use 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)]
4245
struct 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)]
14771546
async 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

Comments
 (0)