use serde_json::Value;
pub const CANCEL_PHRASES: &[&str] = &[
"Canceled by user",
"Canceled (session cost cap reached)",
"Canceled (Synaps restarted)",
"Canceled (session stopped by the host)",
"Canceled (session driver revoked)",
"Canceled",
];
pub const UNRECOGNISED_TOOL: &str = "(an unrecognised tool)";
pub fn is_plausible_tool_name(name: &str) -> bool {
(1..=128).contains(&name.len())
&& name.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'.' | b':' | b'-'))
}
pub fn tool_in_flight_note_text(phrase: &str, tools: &[String]) -> String {
let names = tools
.iter()
.map(|t| if is_plausible_tool_name(t) { format!("`{t}`") } else { UNRECOGNISED_TOOL.into() })
.collect::<Vec<_>>()
.join(", ");
let (call, was, it, its) = if tools.len() == 1 {
("tool call", "was", "It", "its")
} else {
("tool calls", "were", "They", "their")
};
format!(
"[{phrase} while the {call} {names} {was} running. {it} may have already started and \
partially applied {its} effects.]"
)
}
pub fn tool_in_flight_note_label(text: &str) -> Option<&str> {
if text.contains(['\n', '\r']) {
return None;
}
for phrase in CANCEL_PHRASES {
for (call, was) in [("tool call", "was"), ("tool calls", "were")] {
let Some(rest) = text.strip_prefix(&format!("[{phrase} while the {call} ")) else {
continue;
};
let Some(end) = rest.find(&format!(" {was} running. ")) else {
continue;
};
let tools: Option<Vec<String>> = rest[..end]
.split(", ")
.map(|item| match item.strip_prefix('`').and_then(|i| i.strip_suffix('`')) {
Some(name) if is_plausible_tool_name(name) => Some(name.to_string()),
None if item == UNRECOGNISED_TOOL => Some(String::new()),
_ => None,
})
.collect();
if tools.is_some_and(|tools| tool_in_flight_note_text(phrase, &tools) == text) {
return Some(&text[1..text.len() - 1]);
}
}
}
None
}
pub fn is_tool_in_flight_note(message: &Value) -> bool {
message["role"] == "user"
&& message["content"]
.as_str()
.is_some_and(|text| tool_in_flight_note_label(text).is_some())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn generated_notes_round_trip_and_nothing_else_matches() {
for phrase in CANCEL_PHRASES {
for tools in [vec!["bash".to_string()], vec!["read".into(), "edit".into(), "bash".into()]] {
let note = tool_in_flight_note_text(phrase, &tools);
assert_eq!(tool_in_flight_note_label(¬e), Some(¬e[1..note.len() - 1]));
assert!(is_tool_in_flight_note(&json!({"role": "user", "content": note})));
}
}
let note = tool_in_flight_note_text("Canceled by user", &["bash".into()]);
for text in [
note.replace("running.", "running.\n"),
note.replace("`bash`", "`bash` `x`"),
note.replace("`bash`", "`bash"),
format!(" {note}"),
"[]".into(),
"``".into(),
] {
assert_eq!(tool_in_flight_note_label(&text), None, "{text:?}");
}
assert!(!is_tool_in_flight_note(&json!({"role": "assistant", "content": note})));
}
#[test]
fn implausible_tool_names_become_the_fallback_and_stay_recognised() {
for bad in ["a`b".to_string(), "a\nb".into(), "a".repeat(200), String::new(), "rm -rf /".into()] {
let note = tool_in_flight_note_text("Canceled by user", std::slice::from_ref(&bad));
assert_eq!(
note,
"[Canceled by user while the tool call (an unrecognised tool) was running. It \
may have already started and partially applied its effects.]",
"{bad:?}"
);
assert!(tool_in_flight_note_label(¬e).is_some(), "{bad:?}");
}
let mixed = tool_in_flight_note_text("Canceled", &["read".into(), "x`y".into()]);
assert_eq!(
mixed,
"[Canceled while the tool calls `read`, (an unrecognised tool) were running. They \
may have already started and partially applied their effects.]"
);
assert!(tool_in_flight_note_label(&mixed).is_some());
let kept = tool_in_flight_note_text("Canceled", &["mcp__fs.read:v2-x".into(), "a".repeat(128)]);
assert!(kept.contains("`mcp__fs.read:v2-x`") && kept.contains(&format!("`{}`", "a".repeat(128))));
assert!(tool_in_flight_note_label(&kept).is_some());
let forged = "[Canceled while the tool call `a b` was running. It may have already started \
and partially applied its effects.]";
assert_eq!(tool_in_flight_note_label(forged), None);
}
}