Skip to content

Commit 154aa4c

Browse files
authored
Merge pull request #122 from softwaremill/fix/flush-413-and-status-pending
fix(cli): fix flush 413 infinite loop, add timeout/progress, fix status pending check
2 parents b607241 + 38b4308 commit 154aa4c

6 files changed

Lines changed: 84 additions & 22 deletions

File tree

.gitmodules

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
[submodule "enterprise"]
22
path = enterprise
33
url = git@github.com:softwaremill/tracevault-enterprise.git
4+
update = none
45
[submodule "website"]
56
path = website
6-
url = git@github.com:softwaremill/tracevault-website.git
7+
url = https://github.com/softwaremill/tracevault-website.git
8+
update = none

crates/tracevault-cli/src/api_client.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,10 @@ impl ApiClient {
142142
Self {
143143
base_url: base_url.trim_end_matches('/').to_string(),
144144
api_key: api_key.map(String::from),
145-
client: reqwest::Client::new(),
145+
client: reqwest::Client::builder()
146+
.timeout(std::time::Duration::from_secs(60))
147+
.build()
148+
.unwrap_or_default(),
146149
}
147150
}
148151

crates/tracevault-cli/src/commands/flush.rs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,25 +39,45 @@ pub async fn run_flush(project_root: &Path) -> Result<(), Box<dyn std::error::Er
3939
continue;
4040
}
4141

42+
let event_total = events.len();
4243
let mut failed_events: Vec<StreamEventRequest> = Vec::new();
4344

44-
for event in events {
45+
for (i, mut event) in events.into_iter().enumerate() {
46+
eprint!(
47+
"\r Session {} — event {}/{} ...",
48+
&event.session_id[..8],
49+
i + 1,
50+
event_total
51+
);
52+
event.truncate_large_fields();
4553
match client.stream_event(&org_slug, &repo_id, &event).await {
4654
Ok(_) => {
4755
total_sent += 1;
4856
}
4957
Err(e) => {
50-
eprintln!(
51-
"Warning: failed to send event (session {}): {e}",
52-
event.session_id
53-
);
54-
failed_events.push(event);
55-
total_failed += 1;
58+
eprintln!();
59+
let err_str = e.to_string();
60+
if err_str.contains("413") {
61+
// Payload too large even after truncation — drop it.
62+
eprintln!(
63+
" Warning: dropped event (session {}) — still too large after truncation",
64+
event.session_id
65+
);
66+
total_failed += 1;
67+
} else {
68+
eprintln!(
69+
" Warning: failed to send event (session {}): {e}",
70+
event.session_id
71+
);
72+
failed_events.push(event);
73+
total_failed += 1;
74+
}
5675
}
5776
}
5877
}
78+
eprintln!();
5979

60-
// Re-enqueue failed events
80+
// Re-enqueue transiently failed events (not 413s)
6181
if !failed_events.is_empty() {
6282
append_pending(&pending_path, &failed_events)?;
6383
}

crates/tracevault-cli/src/commands/status.rs

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -412,32 +412,47 @@ fn session_checks(project_root: &Path) -> Vec<Check> {
412412
let sessions_dir = project_root.join(".tracevault/sessions");
413413
if !sessions_dir.exists() {
414414
return vec![Check::skip(
415-
"Unpushed sessions",
415+
"Pending events",
416416
"no .tracevault/sessions/ yet (no captures recorded)",
417417
)];
418418
}
419419

420-
let mut total = 0usize;
421-
let mut unpushed = 0usize;
420+
let mut total_sessions = 0usize;
421+
let mut sessions_with_pending = 0usize;
422+
let mut pending_event_count = 0usize;
423+
422424
if let Ok(read) = fs::read_dir(&sessions_dir) {
423425
for entry in read.flatten() {
424426
if !entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
425427
continue;
426428
}
427-
total += 1;
428-
if !entry.path().join(".pushed").exists() {
429-
unpushed += 1;
429+
total_sessions += 1;
430+
let pending_path = entry.path().join("pending.jsonl");
431+
if pending_path.exists() {
432+
let count = fs::read_to_string(&pending_path)
433+
.unwrap_or_default()
434+
.lines()
435+
.filter(|l| !l.trim().is_empty())
436+
.count();
437+
if count > 0 {
438+
sessions_with_pending += 1;
439+
pending_event_count += count;
440+
}
430441
}
431442
}
432443
}
433444

434-
let detail = format!("{unpushed} pending / {total} total");
435-
vec![if unpushed == 0 {
436-
Check::ok("Unpushed sessions", detail)
445+
vec![if sessions_with_pending == 0 {
446+
Check::ok(
447+
"Pending events",
448+
format!("{total_sessions} session(s), all synced"),
449+
)
437450
} else {
438451
Check::warn(
439-
"Unpushed sessions",
440-
format!("{detail} — will be pushed on next `tracevault push` or pre-push hook"),
452+
"Pending events",
453+
format!(
454+
"{pending_event_count} event(s) in {sessions_with_pending}/{total_sessions} session(s) — run `tracevault flush`"
455+
),
441456
)
442457
}]
443458
}

crates/tracevault-cli/src/commands/stream.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ pub async fn run_stream(
114114
_ => StreamEventType::ToolUse,
115115
};
116116

117-
let req = StreamEventRequest {
117+
let mut req = StreamEventRequest {
118118
protocol_version: 1,
119119
tool: Some("claude-code".to_string()),
120120
event_type: stream_event_type,
@@ -136,6 +136,8 @@ pub async fn run_stream(
136136
final_stats: None,
137137
};
138138

139+
req.truncate_large_fields();
140+
139141
// 6. Resolve credentials
140142
let (server_url, token) = crate::api_client::resolve_credentials(project_root);
141143

crates/tracevault-core/src/streaming.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,26 @@ pub fn is_file_modifying_tool(tool_name: &str) -> bool {
7979
matches!(tool_name, "Write" | "Edit" | "Bash")
8080
}
8181

82+
impl StreamEventRequest {
83+
/// Drop optional fields largest-first until the serialized payload is
84+
/// under 512 KB. Prevents 413 errors on both real-time sends and flush.
85+
pub fn truncate_large_fields(&mut self) {
86+
const MAX_BYTES: usize = 512 * 1024;
87+
if serde_json::to_string(self).map(|s| s.len()).unwrap_or(0) <= MAX_BYTES {
88+
return;
89+
}
90+
self.transcript_lines = None;
91+
if serde_json::to_string(self).map(|s| s.len()).unwrap_or(0) <= MAX_BYTES {
92+
return;
93+
}
94+
self.tool_response = None;
95+
if serde_json::to_string(self).map(|s| s.len()).unwrap_or(0) <= MAX_BYTES {
96+
return;
97+
}
98+
self.tool_input = None;
99+
}
100+
}
101+
82102
pub fn extract_file_change(
83103
tool_name: &str,
84104
tool_input: &serde_json::Value,

0 commit comments

Comments
 (0)