From e0a54a3b43e8c653540f739620d9d7652fab16c8 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Thu, 2 Apr 2026 15:10:40 -0400 Subject: [PATCH] save request payload on any API error, not just timeouts Serialize request JSON before send_and_check so it's available for both HTTP errors and stream errors. Extracted save logic into save_failed_request helper on SseReader. Co-Authored-By: Proof of Concept --- src/agent/api/mod.rs | 58 ++++++++++++++++++++++++++--------------- src/agent/api/openai.rs | 4 ++- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/agent/api/mod.rs b/src/agent/api/mod.rs index 81a068a..c7151c3 100644 --- a/src/agent/api/mod.rs +++ b/src/agent/api/mod.rs @@ -192,6 +192,7 @@ pub(crate) async fn send_and_check( extra_headers: &[(&str, &str)], ui_tx: &UiSender, debug_label: &str, + request_json: Option<&str>, ) -> Result { let debug = std::env::var("POC_DEBUG").is_ok(); let start = Instant::now(); @@ -262,6 +263,18 @@ pub(crate) async fn send_and_check( url, &body[..body.len().min(500)] ))); + if let Some(json) = request_json { + let log_dir = dirs::home_dir() + .unwrap_or_default() + .join(".consciousness/logs"); + let ts = chrono::Local::now().format("%Y%m%dT%H%M%S"); + let path = log_dir.join(format!("failed-request-{}.json", ts)); + if std::fs::write(&path, json).is_ok() { + let _ = ui_tx.send(UiMessage::Debug(format!( + "saved failed request to {} (HTTP {})", path.display(), status + ))); + } + } anyhow::bail!("HTTP {} ({}): {}", status, url, &body[..body.len().min(1000)]); } @@ -291,8 +304,8 @@ pub(crate) struct SseReader { debug: bool, ui_tx: UiSender, done: bool, - /// Serialized request payload — saved to disk on timeout for replay debugging. - request_json: Option, + /// Serialized request payload — saved to disk on errors for replay debugging. + pub(crate) request_json: Option, } impl SseReader { @@ -312,8 +325,19 @@ impl SseReader { } /// Attach the serialized request payload for error diagnostics. - pub fn set_request(&mut self, request: &impl serde::Serialize) { - self.request_json = serde_json::to_string_pretty(request).ok(); + /// Save the request payload to disk for replay debugging. + fn save_failed_request(&self, reason: &str) { + let Some(ref json) = self.request_json else { return }; + let log_dir = dirs::home_dir() + .unwrap_or_default() + .join(".consciousness/logs"); + let ts = chrono::Local::now().format("%Y%m%dT%H%M%S"); + let path = log_dir.join(format!("failed-request-{}.json", ts)); + if std::fs::write(&path, json).is_ok() { + let _ = self.ui_tx.send(UiMessage::Debug(format!( + "saved failed request to {} ({})", path.display(), reason + ))); + } } /// Read the next SSE event from the response stream. @@ -374,27 +398,19 @@ impl SseReader { self.line_buf.push_str(&String::from_utf8_lossy(&chunk)); } Ok(Ok(None)) => return Ok(None), - Ok(Err(e)) => return Err(e.into()), + Ok(Err(e)) => { + self.save_failed_request(&format!("stream error: {}", e)); + return Err(e.into()); + } Err(_) => { - let _ = self.ui_tx.send(UiMessage::Debug(format!( - "TIMEOUT: no data for {}s ({} chunks, {:.1}s elapsed)", + let msg = format!( + "stream timeout: no data for {}s ({} chunks, {:.1}s elapsed)", self.chunk_timeout.as_secs(), self.chunks_received, self.stream_start.elapsed().as_secs_f64() - ))); - // Save the request for replay debugging - if let Some(ref json) = self.request_json { - let log_dir = dirs::home_dir() - .unwrap_or_default() - .join(".consciousness/logs"); - let ts = chrono::Local::now().format("%Y%m%dT%H%M%S"); - let path = log_dir.join(format!("failed-request-{}.json", ts)); - if std::fs::write(&path, json).is_ok() { - let _ = self.ui_tx.send(UiMessage::Debug(format!( - "saved failed request to {}", path.display() - ))); - } - } + ); + let _ = self.ui_tx.send(UiMessage::Debug(msg.clone())); + self.save_failed_request(&msg); anyhow::bail!( "stream timeout: no data for {}s ({} chunks received)", self.chunk_timeout.as_secs(), diff --git a/src/agent/api/openai.rs b/src/agent/api/openai.rs index 0d6a11f..257e18a 100644 --- a/src/agent/api/openai.rs +++ b/src/agent/api/openai.rs @@ -55,6 +55,7 @@ pub async fn stream_events( None => String::new(), }; let debug_label = format!("{} messages, model={}{}", msg_count, model, pri_label); + let request_json = serde_json::to_string_pretty(&request).ok(); let mut response = super::send_and_check( client, @@ -64,11 +65,12 @@ pub async fn stream_events( &[], ui_tx, &debug_label, + request_json.as_deref(), ) .await?; let mut reader = super::SseReader::new(ui_tx); - reader.set_request(&request); + reader.request_json = request_json; let mut content_len: usize = 0; let mut reasoning_chars: usize = 0;