Skip to content

Commit 10ae7f5

Browse files
committed
fix: resolve agent refs and retire stale mission help
1 parent 1196972 commit 10ae7f5

9 files changed

Lines changed: 345 additions & 36 deletions

File tree

src/app/mcp/tools.rs

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,7 +1493,12 @@ pub fn mission_reject_tool(state: Arc<McpState>) -> Tool {
14931493
};
14941494
let storage = mission_storage(&state);
14951495
match storage.get_mission(mission_id).await {
1496-
Ok(Some(_)) => {}
1496+
Ok(Some(mut mission)) => {
1497+
mission.clear_help_request_state();
1498+
if let Err(e) = storage.save_mission(&mission).await {
1499+
return Ok(error_response(e.to_string()));
1500+
}
1501+
}
14971502
Ok(None) => {
14981503
return Ok(error_response(format!(
14991504
"Mission {} not found",
@@ -1551,7 +1556,7 @@ pub fn mission_pause_tool(state: Arc<McpState>) -> Tool {
15511556
Err(msg) => return Ok(error_response(msg)),
15521557
};
15531558
let storage = mission_storage(&state);
1554-
let Some(mission) = (match storage.get_mission(mission_id).await {
1559+
let Some(mut mission) = (match storage.get_mission(mission_id).await {
15551560
Ok(mission) => mission,
15561561
Err(e) => return Ok(error_response(e.to_string())),
15571562
}) else {
@@ -1578,9 +1583,21 @@ pub fn mission_pause_tool(state: Arc<McpState>) -> Tool {
15781583
"mcp",
15791584
"pause requested via mission.pause",
15801585
);
1586+
mission.clear_help_request_state();
1587+
if let Err(e) = storage.save_mission(&mission).await {
1588+
return Ok(error_response(e.to_string()));
1589+
}
15811590
if let Err(e) = storage.save_control_message(&note).await {
15821591
return Ok(error_response(e.to_string()));
15831592
}
1593+
if let Err(e) = crate::mission::retire_help_requests_for_mission(
1594+
state.town.channel(),
1595+
mission_id,
1596+
)
1597+
.await
1598+
{
1599+
return Ok(error_response(e.to_string()));
1600+
}
15841601
let dispatcher = crate::mission::MissionDispatcher::new(
15851602
storage.clone(),
15861603
state.town.channel().clone(),
@@ -1614,7 +1631,7 @@ pub fn mission_resume_tool(state: Arc<McpState>) -> Tool {
16141631
Err(msg) => return Ok(error_response(msg)),
16151632
};
16161633
let storage = mission_storage(&state);
1617-
let Some(mission) = (match storage.get_mission(mission_id).await {
1634+
let Some(mut mission) = (match storage.get_mission(mission_id).await {
16181635
Ok(mission) => mission,
16191636
Err(e) => return Ok(error_response(e.to_string())),
16201637
}) else {
@@ -1654,9 +1671,21 @@ pub fn mission_resume_tool(state: Arc<McpState>) -> Tool {
16541671
"mcp",
16551672
"resume requested via mission.resume",
16561673
);
1674+
mission.clear_help_request_state();
1675+
if let Err(e) = storage.save_mission(&mission).await {
1676+
return Ok(error_response(e.to_string()));
1677+
}
16571678
if let Err(e) = storage.save_control_message(&note).await {
16581679
return Ok(error_response(e.to_string()));
16591680
}
1681+
if let Err(e) = crate::mission::retire_help_requests_for_mission(
1682+
state.town.channel(),
1683+
mission_id,
1684+
)
1685+
.await
1686+
{
1687+
return Ok(error_response(e.to_string()));
1688+
}
16601689
let dispatcher = crate::mission::MissionDispatcher::new(
16611690
storage.clone(),
16621691
state.town.channel().clone(),
@@ -1798,10 +1827,22 @@ pub fn mission_note_tool(state: Arc<McpState>) -> Tool {
17981827
if let Err(e) = storage.save_control_message(&note).await {
17991828
return Ok(error_response(e.to_string()));
18001829
}
1830+
let retired = match crate::mission::retire_help_requests_for_mission(
1831+
state.town.channel(),
1832+
mission_id,
1833+
)
1834+
.await
1835+
{
1836+
Ok(retired) => retired,
1837+
Err(e) => return Ok(error_response(e.to_string())),
1838+
};
18011839
if let Err(e) = storage
18021840
.log_event(
18031841
mission_id,
1804-
&format!("Operator note queued via MCP: {}", input.message),
1842+
&format!(
1843+
"Operator note queued via MCP: {} (retired {} stale help request(s))",
1844+
input.message, retired
1845+
),
18051846
)
18061847
.await
18071848
{
@@ -1954,17 +1995,30 @@ pub fn mission_stop_tool(state: Arc<McpState>) -> Tool {
19541995
} else {
19551996
mission.block("Stopped by user");
19561997
}
1998+
mission.clear_help_request_state();
19571999

19582000
if let Err(e) = storage.save_mission(&mission).await {
19592001
return Ok(error_response(e.to_string()));
19602002
}
19612003
if let Err(e) = storage.remove_active(mission_id).await {
19622004
return Ok(error_response(e.to_string()));
19632005
}
2006+
let retired = match crate::mission::retire_help_requests_for_mission(
2007+
state.town.channel(),
2008+
mission_id,
2009+
)
2010+
.await
2011+
{
2012+
Ok(retired) => retired,
2013+
Err(e) => return Ok(error_response(e.to_string())),
2014+
};
19642015
if let Err(e) = storage
19652016
.log_event(
19662017
mission_id,
1967-
&format!("Mission stopped via MCP (force={})", input.force),
2018+
&format!(
2019+
"Mission stopped via MCP (force={}) and retired {} stale help request(s)",
2020+
input.force, retired
2021+
),
19682022
)
19692023
.await
19702024
{

src/app/server.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,7 @@ async fn stop_mission(
11811181
} else {
11821182
mission.block("Stopped by user");
11831183
}
1184+
mission.clear_help_request_state();
11841185

11851186
storage
11861187
.save_mission(&mission)
@@ -1190,10 +1191,16 @@ async fn stop_mission(
11901191
.remove_active(id)
11911192
.await
11921193
.map_err(|e| ProblemDetails::internal_error(&e.to_string()))?;
1194+
let retired = crate::mission::retire_help_requests_for_mission(state.town.channel(), id)
1195+
.await
1196+
.map_err(|e| ProblemDetails::internal_error(&e.to_string()))?;
11931197
let _ = storage
11941198
.log_event(
11951199
id,
1196-
&format!("Mission stopped via API (force={})", req.force),
1200+
&format!(
1201+
"Mission stopped via API (force={}) and retired {} stale help request(s)",
1202+
req.force, retired
1203+
),
11971204
)
11981205
.await;
11991206

@@ -1226,6 +1233,7 @@ async fn resume_mission(
12261233
}
12271234

12281235
mission.start();
1236+
mission.clear_help_request_state();
12291237
storage
12301238
.save_mission(&mission)
12311239
.await
@@ -1234,7 +1242,18 @@ async fn resume_mission(
12341242
.add_active(id)
12351243
.await
12361244
.map_err(|e| ProblemDetails::internal_error(&e.to_string()))?;
1237-
let _ = storage.log_event(id, "Mission resumed via API").await;
1245+
let retired = crate::mission::retire_help_requests_for_mission(state.town.channel(), id)
1246+
.await
1247+
.map_err(|e| ProblemDetails::internal_error(&e.to_string()))?;
1248+
let _ = storage
1249+
.log_event(
1250+
id,
1251+
&format!(
1252+
"Mission resumed via API (retired {} stale help request(s))",
1253+
retired
1254+
),
1255+
)
1256+
.await;
12381257

12391258
Ok(Json(serde_json::json!({
12401259
"id": id.to_string(),

src/channel.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,49 @@ impl Channel {
533533
Ok(agents.into_iter().find(|a| a.name == name))
534534
}
535535

536+
/// Resolve a user-supplied agent reference.
537+
///
538+
/// Accepts a full UUID, short UUID, canonical name, nickname/display name,
539+
/// or full display label. Returns `Ok(None)` when there is no match.
540+
pub async fn resolve_agent_ref(&self, raw: &str) -> Result<Option<crate::agent::Agent>> {
541+
let raw = raw.trim();
542+
if raw.is_empty() {
543+
return Ok(None);
544+
}
545+
546+
if let Ok(agent_id) = raw.parse::<AgentId>() {
547+
return self.get_agent_state(agent_id).await;
548+
}
549+
550+
let raw_lower = raw.to_ascii_lowercase();
551+
let agents = self.list_agents().await?;
552+
let matches: Vec<_> = agents
553+
.into_iter()
554+
.filter(|agent| {
555+
agent.name.eq_ignore_ascii_case(raw)
556+
|| agent.display_name().eq_ignore_ascii_case(raw)
557+
|| agent.display_label().eq_ignore_ascii_case(raw)
558+
|| agent.id.short_id().eq_ignore_ascii_case(&raw_lower)
559+
})
560+
.collect();
561+
562+
match matches.as_slice() {
563+
[] => Ok(None),
564+
[agent] => Ok(Some(agent.clone())),
565+
_ => {
566+
let labels = matches
567+
.iter()
568+
.map(|agent| format!("{} ({})", agent.display_label(), agent.id.short_id()))
569+
.collect::<Vec<_>>()
570+
.join(", ");
571+
Err(crate::Error::Config(format!(
572+
"Agent reference '{}' is ambiguous: {}",
573+
raw, labels
574+
)))
575+
}
576+
}
577+
}
578+
536579
/// Delete an agent from Redis.
537580
pub async fn delete_agent(&self, agent_id: AgentId) -> Result<()> {
538581
let mut conn = self.conn.clone();
@@ -948,6 +991,43 @@ impl Channel {
948991
Ok(messages)
949992
}
950993

994+
/// Remove inbox messages matching a predicate while preserving order.
995+
pub async fn remove_inbox_messages_matching<F>(
996+
&self,
997+
agent_id: AgentId,
998+
mut predicate: F,
999+
) -> Result<usize>
1000+
where
1001+
F: FnMut(&Message) -> bool,
1002+
{
1003+
let mut conn = self.conn.clone();
1004+
let inbox_key = self.inbox_key(agent_id);
1005+
let items: Vec<String> = conn.lrange(&inbox_key, 0, -1).await?;
1006+
if items.is_empty() {
1007+
return Ok(0);
1008+
}
1009+
1010+
let mut kept = Vec::with_capacity(items.len());
1011+
let mut removed = 0usize;
1012+
for item in items {
1013+
match serde_json::from_str::<Message>(&item) {
1014+
Ok(message) if predicate(&message) => removed += 1,
1015+
_ => kept.push(item),
1016+
}
1017+
}
1018+
1019+
if removed == 0 {
1020+
return Ok(0);
1021+
}
1022+
1023+
let _: () = conn.del(&inbox_key).await?;
1024+
if !kept.is_empty() {
1025+
let _: () = conn.rpush(&inbox_key, kept).await?;
1026+
}
1027+
1028+
Ok(removed)
1029+
}
1030+
9511031
/// Move a message to another agent's inbox.
9521032
pub async fn move_message_to_inbox(&self, message: &Message, to_agent: AgentId) -> Result<()> {
9531033
// Create a new message with updated recipient

0 commit comments

Comments
 (0)