Skip to content

Commit 196ee3c

Browse files
wilfriedrosetclaudeDouwe Osinga
authored
fix: resolve tokio::sync::Mutex deadlock in recipe retry path (#7832)
Signed-off-by: Wilfried Roset <wilfriedroset@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Douwe Osinga <douwe@squareup.com>
1 parent b0bfb57 commit 196ee3c

2 files changed

Lines changed: 40 additions & 31 deletions

File tree

crates/goose/src/agents/agent.rs

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,12 +1136,10 @@ impl Agent {
11361136
break;
11371137
}
11381138

1139-
if let Some(final_output_tool) = self.final_output_tool.lock().await.as_ref() {
1140-
if final_output_tool.final_output.is_some() {
1141-
let final_event = AgentEvent::Message(
1142-
Message::assistant().with_text(final_output_tool.final_output.clone().unwrap())
1143-
);
1144-
yield final_event;
1139+
{
1140+
let guard = self.final_output_tool.lock().await;
1141+
if let Some(ref output) = guard.as_ref().and_then(|fot| fot.final_output.clone()) {
1142+
yield AgentEvent::Message(Message::assistant().with_text(output));
11451143
break;
11461144
}
11471145
}
@@ -1600,42 +1598,52 @@ impl Agent {
16001598
}
16011599

16021600
if no_tools_called {
1603-
if let Some(final_output_tool) = self.final_output_tool.lock().await.as_ref() {
1604-
if final_output_tool.final_output.is_none() {
1601+
// Lock, extract state, drop guard before branching — handle_retry_logic
1602+
// also locks final_output_tool and tokio::sync::Mutex is not reentrant.
1603+
let final_output = {
1604+
let guard = self.final_output_tool.lock().await;
1605+
guard.as_ref().map(|fot| fot.final_output.clone())
1606+
};
1607+
1608+
match final_output {
1609+
Some(None) => {
16051610
warn!("Final output tool has not been called yet. Continuing agent loop.");
16061611
let message = Message::user().with_text(FINAL_OUTPUT_CONTINUATION_MESSAGE);
16071612
session_manager.add_message(&session_config.id, &message).await?;
16081613
conversation.push(message.clone());
16091614
yield AgentEvent::Message(message);
1610-
} else {
1611-
let message = Message::assistant().with_text(final_output_tool.final_output.clone().unwrap());
1615+
}
1616+
Some(Some(output)) => {
1617+
let message = Message::assistant().with_text(output);
16121618
session_manager.add_message(&session_config.id, &message).await?;
16131619
conversation.push(message.clone());
16141620
yield AgentEvent::Message(message);
16151621
exit_chat = true;
16161622
}
1617-
} else if did_recovery_compact_this_iteration {
1618-
// Avoid setting exit_chat; continue from last user message in the conversation
1619-
} else {
1620-
match self.handle_retry_logic(&mut conversation, &session_config, &initial_messages).await {
1621-
Ok(should_retry) => {
1622-
if should_retry {
1623-
info!("Retry logic triggered, restarting agent loop");
1624-
session_manager.replace_conversation(&session_config.id, &conversation).await?;
1625-
yield AgentEvent::HistoryReplaced(conversation.clone());
1626-
} else {
1623+
None if did_recovery_compact_this_iteration => {
1624+
// continue from last user message after recovery compact
1625+
}
1626+
None => {
1627+
match self.handle_retry_logic(&mut conversation, &session_config, &initial_messages).await {
1628+
Ok(should_retry) => {
1629+
if should_retry {
1630+
info!("Retry logic triggered, restarting agent loop");
1631+
session_manager.replace_conversation(&session_config.id, &conversation).await?;
1632+
yield AgentEvent::HistoryReplaced(conversation.clone());
1633+
} else {
1634+
exit_chat = true;
1635+
}
1636+
}
1637+
Err(e) => {
1638+
error!("Retry logic failed: {}", e);
1639+
yield AgentEvent::Message(
1640+
Message::assistant().with_text(
1641+
format!("Retry logic encountered an error: {}", e)
1642+
)
1643+
);
16271644
exit_chat = true;
16281645
}
16291646
}
1630-
Err(e) => {
1631-
error!("Retry logic failed: {}", e);
1632-
yield AgentEvent::Message(
1633-
Message::assistant().with_text(
1634-
format!("Retry logic encountered an error: {}", e)
1635-
)
1636-
);
1637-
exit_chat = true;
1638-
}
16391647
}
16401648
}
16411649
}

crates/goose/src/agents/retry.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,9 @@ impl RetryManager {
104104
*messages = Conversation::new_unvalidated(initial_messages.to_vec());
105105
info!("Reset message history to initial state for retry");
106106

107-
if let Some(final_output_tool) = final_output_tool.lock().await.as_mut() {
108-
final_output_tool.final_output = None;
107+
let mut guard = final_output_tool.lock().await;
108+
if let Some(fot) = guard.as_mut() {
109+
fot.final_output = None;
109110
info!("Cleared final output tool state for retry");
110111
}
111112
}

0 commit comments

Comments
 (0)