fix(daemon): scope active tools to turn

This commit is contained in:
Miguel Palhas
2026-09-05 01:30:16 +01:00
parent fd3ea64949
commit eea337ce36
+76 -17
View File
@@ -1,6 +1,6 @@
//! Fold persisted ACP notifications into client-neutral transcript and state.
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use maestro_acp::schema::{ContentChunk, SessionNotification, SessionUpdate, ToolCall};
use maestro_protocol::{
@@ -36,6 +36,7 @@ pub struct Projection {
pending_tool_updates: HashMap<String, Vec<maestro_acp::schema::ToolCallUpdateFields>>,
state: ReducedState,
turn_active: bool,
turn_tool_calls: HashSet<String>,
}
impl Projection {
@@ -110,11 +111,15 @@ impl Projection {
self.turn_active = true;
self.state.turn.responding = true;
self.state.turn.thinking = true;
self.turn_tool_calls.clear();
self.state.turn.active_tool_calls.clear();
}
TurnLifecycle::Finish | TurnLifecycle::Cancel | TurnLifecycle::Failure => {
self.turn_active = false;
self.state.turn.responding = false;
self.state.turn.thinking = false;
self.turn_tool_calls.clear();
self.state.turn.active_tool_calls.clear();
}
}
ProjectionUpdate {
@@ -159,7 +164,7 @@ impl Projection {
TranscriptRowKind::ToolCall
};
self.tools.insert(tool_id, canonical.clone());
self.sync_active_tools();
self.track_tool_call(&canonical);
Some(self.replace(
id,
if tool_has_diff(&canonical) {
@@ -203,7 +208,7 @@ impl Projection {
row.data = to_value(tool);
row.clone()
};
self.sync_active_tools();
self.sync_active_tool(&tool_id);
Some(TranscriptDelta::Patch { row })
}
@@ -238,20 +243,33 @@ impl Projection {
}
}
fn sync_active_tools(&mut self) {
self.state.turn.active_tool_calls = self
.tools
.iter()
.filter(|(_, tool)| {
matches!(
tool.status,
maestro_acp::schema::ToolCallStatus::Pending
| maestro_acp::schema::ToolCallStatus::InProgress
)
})
.map(|(id, _)| id.clone())
.collect();
self.state.turn.active_tool_calls.sort();
fn track_tool_call(&mut self, tool: &ToolCall) {
if self.turn_active {
self.turn_tool_calls.insert(tool.tool_call_id.to_string());
self.sync_active_tool(&tool.tool_call_id.to_string());
}
}
fn sync_active_tool(&mut self, tool_id: &str) {
if !self.turn_active || !self.turn_tool_calls.contains(tool_id) {
return;
}
let active = self.tools.get(tool_id).is_some_and(|tool| {
matches!(
tool.status,
maestro_acp::schema::ToolCallStatus::Pending
| maestro_acp::schema::ToolCallStatus::InProgress
)
});
let active_tool_calls = &mut self.state.turn.active_tool_calls;
if active {
if !active_tool_calls.iter().any(|id| id == tool_id) {
active_tool_calls.push(tool_id.to_owned());
active_tool_calls.sort();
}
} else {
active_tool_calls.retain(|id| id != tool_id);
}
}
}
@@ -546,4 +564,45 @@ mod tests {
assert!(!projection.state().turn.thinking);
}
}
fn terminal_turn_drops_unfinished_tools(
terminal: TurnLifecycle,
unfinished_id: &str,
next_id: &str,
) {
let mut projection = Projection::default();
projection.turn_lifecycle(TurnLifecycle::Begin);
projection.apply(
1,
&event(serde_json::json!({
"sessionUpdate":"tool_call", "toolCallId":unfinished_id,
"title":"Unfinished", "status":"in_progress"
})),
);
assert_eq!(projection.state().turn.active_tool_calls, [unfinished_id]);
projection.turn_lifecycle(terminal);
assert!(projection.state().turn.active_tool_calls.is_empty());
assert_eq!(projection.snapshot(1).rows.len(), 1);
projection.turn_lifecycle(TurnLifecycle::Begin);
projection.apply(
2,
&event(serde_json::json!({
"sessionUpdate":"tool_call", "toolCallId":next_id,
"title":"Next", "status":"pending"
})),
);
assert_eq!(projection.state().turn.active_tool_calls, [next_id]);
}
#[test]
fn cancelled_turn_does_not_reactivate_unfinished_tools() {
terminal_turn_drops_unfinished_tools(TurnLifecycle::Cancel, "cancelled-1", "next-1");
}
#[test]
fn failed_turn_does_not_reactivate_unfinished_tools() {
terminal_turn_drops_unfinished_tools(TurnLifecycle::Failure, "failed-1", "next-1");
}
}