Skip to content

Commit d30a0f0

Browse files
committed
fix: handle malformed lifecycle ingress safely
1 parent 5009284 commit d30a0f0

2 files changed

Lines changed: 140 additions & 9 deletions

File tree

src/agent/mod.rs

Lines changed: 131 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1519,7 +1519,7 @@ fn spawn_main_chat_reasoning_turn(
15191519
Arc::ptr_eq(stored, &task_token_arc)
15201520
});
15211521

1522-
let next_inbound = pending_inbound.get(&task_chat_id).and_then(|r| {
1522+
let mut next_inbound = pending_inbound.get(&task_chat_id).and_then(|r| {
15231523
let mut g = match r.lock() {
15241524
Ok(guard) => guard,
15251525
Err(poisoned) => {
@@ -1536,10 +1536,30 @@ fn spawn_main_chat_reasoning_turn(
15361536
g.pop_front()
15371537
});
15381538

1539-
if let Some(mut next_inbound) = next_inbound {
1540-
match ensure_run_id(&mut next_inbound) {
1539+
while let Some(mut inbound) = next_inbound {
1540+
let next_from_queue = || {
1541+
pending_inbound.get(&task_chat_id).and_then(|r| {
1542+
let mut g = match r.lock() {
1543+
Ok(guard) => guard,
1544+
Err(poisoned) => {
1545+
let _ = logger_tx.send(BusMessage::Log(
1546+
LogEvent::warn(
1547+
"AgentLogic",
1548+
"pending_inbound mutex poisoned after reasoning turn; recovering queue.",
1549+
)
1550+
.with_chat_id(&task_chat_id),
1551+
));
1552+
poisoned.into_inner()
1553+
}
1554+
};
1555+
g.pop_front()
1556+
})
1557+
};
1558+
1559+
match ensure_run_id(&mut inbound) {
15411560
Ok(next_run_id) => {
1542-
spawn_main_chat_reasoning_turn(args_for_chain, next_inbound, next_run_id);
1561+
spawn_main_chat_reasoning_turn(args_for_chain, inbound, next_run_id);
1562+
break;
15431563
}
15441564
Err(error) => {
15451565
let _ = logger_tx.send(BusMessage::Log(
@@ -1549,6 +1569,7 @@ fn spawn_main_chat_reasoning_turn(
15491569
)
15501570
.with_chat_id(&task_chat_id),
15511571
));
1572+
next_inbound = next_from_queue();
15521573
}
15531574
}
15541575
}
@@ -1980,7 +2001,26 @@ impl ActorLogic<BusMessage> for AgentLogic {
19802001
return Ok(None);
19812002
}
19822003
BusMessage::Inbound(mut inbound) => {
1983-
let run_id = ensure_run_id(&mut inbound).map_err(ActorError::from)?;
2004+
let run_id = match ensure_run_id(&mut inbound) {
2005+
Ok(run_id) => run_id,
2006+
Err(error) => {
2007+
let _ = self.logger_tx.send(BusMessage::Log(
2008+
LogEvent::error(
2009+
&self.name,
2010+
&format!("Rejecting inbound message: {}", error),
2011+
)
2012+
.with_chat_id(&inbound.chat_id),
2013+
));
2014+
let notice = crate::channels::terminal::build_channel_error_notice(
2015+
&inbound.channel,
2016+
&inbound.chat_id,
2017+
inbound.thread_id.as_deref(),
2018+
&error,
2019+
);
2020+
let _ = self.outbound_tx.send(BusMessage::Outbound(notice)).await;
2021+
return Ok(None);
2022+
}
2023+
};
19842024
let chat_id = inbound.chat_id.clone();
19852025
let session_key = inbound.clarification_session_key();
19862026
if self
@@ -3973,6 +4013,7 @@ mod tests {
39734013
Router,
39744014
};
39754015
use serde_json::Value;
4016+
use std::collections::VecDeque;
39764017
use std::path::PathBuf;
39774018
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
39784019
use std::sync::{Arc, Mutex};
@@ -4869,6 +4910,91 @@ mod tests {
48694910
);
48704911
}
48714912

4913+
#[tokio::test]
4914+
async fn invalid_tauri_inbound_is_rejected_without_stopping_the_actor() {
4915+
let provider = RespondingProvider {
4916+
tag: "done".to_string(),
4917+
};
4918+
let (mut agent, mut outbound_rx) = build_agent_with_provider(Box::new(provider));
4919+
let mut invalid = test_inbound("invalid-run-id", "hello");
4920+
invalid.channel = "tauri".to_string();
4921+
4922+
assert!(matches!(
4923+
agent.process(BusMessage::Inbound(invalid)).await,
4924+
Ok(None)
4925+
));
4926+
assert!(matches!(
4927+
outbound_rx.recv().await,
4928+
Some(BusMessage::Outbound(_))
4929+
));
4930+
4931+
let mut valid = test_inbound("valid-after-rejection", "hello");
4932+
valid.channel = "tauri".to_string();
4933+
valid.metadata.insert(
4934+
METADATA_RUN_ID.to_string(),
4935+
serde_json::json!("valid-run-id"),
4936+
);
4937+
agent
4938+
.process(BusMessage::Inbound(valid))
4939+
.await
4940+
.expect("actor remains usable after rejecting malformed inbound");
4941+
assert!(matches!(
4942+
tokio::time::timeout(Duration::from_secs(2), outbound_rx.recv()).await,
4943+
Ok(Some(BusMessage::RunLifecycle(
4944+
RunLifecycleEvent::Started { .. }
4945+
)))
4946+
));
4947+
}
4948+
4949+
#[tokio::test]
4950+
async fn invalid_queued_inbound_does_not_strand_following_valid_message() {
4951+
let (unblock_tx, unblock_rx) = tokio::sync::oneshot::channel();
4952+
let calls = Arc::new(AtomicUsize::new(0));
4953+
let provider = GateFirstChatProvider {
4954+
calls: calls.clone(),
4955+
first_unblock: Arc::new(tokio::sync::Mutex::new(Some(unblock_rx))),
4956+
};
4957+
let (mut agent, _outbound_rx) = build_agent_with_provider(Box::new(provider));
4958+
let chat_id = "skip-invalid-queued";
4959+
agent
4960+
.process(BusMessage::Inbound(test_inbound(chat_id, "first")))
4961+
.await
4962+
.expect("start first turn");
4963+
for _ in 0..200 {
4964+
if calls.load(Ordering::SeqCst) == 1 {
4965+
break;
4966+
}
4967+
tokio::time::sleep(Duration::from_millis(5)).await;
4968+
}
4969+
assert_eq!(calls.load(Ordering::SeqCst), 1);
4970+
4971+
let mut invalid = test_inbound(chat_id, "invalid queued");
4972+
invalid.channel = "tauri".to_string();
4973+
let mut valid = test_inbound(chat_id, "valid queued");
4974+
valid.channel = "tauri".to_string();
4975+
valid.metadata.insert(
4976+
METADATA_RUN_ID.to_string(),
4977+
serde_json::json!("queued-run-id"),
4978+
);
4979+
agent.pending_inbound.insert(
4980+
chat_id.to_string(),
4981+
Mutex::new(VecDeque::from([invalid, valid])),
4982+
);
4983+
4984+
unblock_tx.send(()).expect("unblock first turn");
4985+
for _ in 0..400 {
4986+
if calls.load(Ordering::SeqCst) == 2 {
4987+
break;
4988+
}
4989+
tokio::time::sleep(Duration::from_millis(5)).await;
4990+
}
4991+
assert_eq!(
4992+
calls.load(Ordering::SeqCst),
4993+
2,
4994+
"the valid queued message must run after the invalid item is dropped"
4995+
);
4996+
}
4997+
48724998
#[tokio::test]
48734999
async fn started_lifecycle_event_preserves_caller_run_id() {
48745000
let provider = RespondingProvider {

src/logging.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -415,9 +415,13 @@ impl LoggingActor {
415415
let mut total_bytes = 0u64;
416416
let mut rotated = Vec::new();
417417
for entry in fs::read_dir(&self.logs_dir)? {
418-
let entry = entry?;
418+
let Ok(entry) = entry else {
419+
continue;
420+
};
419421
let path = entry.path();
420-
let metadata = entry.metadata()?;
422+
let Ok(metadata) = entry.metadata() else {
423+
continue;
424+
};
421425
if !metadata.is_file() || !recognized_log_file(&path) {
422426
continue;
423427
}
@@ -439,8 +443,9 @@ impl LoggingActor {
439443
if total_bytes <= self.max_total_bytes {
440444
break;
441445
}
442-
fs::remove_file(&path)?;
443-
total_bytes = total_bytes.saturating_sub(bytes);
446+
if fs::remove_file(&path).is_ok() {
447+
total_bytes = total_bytes.saturating_sub(bytes);
448+
}
444449
}
445450
Ok(())
446451
}

0 commit comments

Comments
 (0)