const PARKING_PHRASES: &[&str] = &[
"background polling monitor",
"background poll",
"background monitor",
"background watcher",
"monitor in the background",
"monitoring in the background",
"monitoring the checks",
"monitoring pr",
"monitoring the pr",
"waiting for the monitor",
"the monitor to report",
"monitor to report back",
"standing by",
"will report back",
"will report when",
"report back once",
"report back when",
"i'll report back",
"check back later",
"checking back later",
"wait for the notification",
"waiting for the notification",
"waiting for a notification",
"wait for a notification",
"await the notification",
"let me know once",
"let me know when the",
"notify me when",
"ping me when",
"i'll wait for the",
"i will wait for the",
"will resume once",
];
const HUMAN_ADDRESSED_PHRASES: &[&str] = &[
"let me know once",
"let me know when the",
"notify me when",
"ping me when",
];
pub fn is_nudge_eligible(phrase: &str) -> bool {
!HUMAN_ADDRESSED_PHRASES.contains(&phrase)
}
pub fn detect_idle_parking(final_message: &str) -> Option<&'static str> {
if final_message.trim().is_empty() {
return None;
}
let haystack = final_message.to_lowercase();
PARKING_PHRASES
.iter()
.copied()
.find(|phrase| haystack.contains(phrase))
}
pub fn last_assistant_text_from_transcript(jsonl: &str) -> Option<String> {
for line in jsonl.lines().rev() {
let line = line.trim();
if line.is_empty() {
continue;
}
let Ok(val) = serde_json::from_str::<serde_json::Value>(line) else {
continue;
};
if val.get("type").and_then(serde_json::Value::as_str) != Some("assistant") {
continue;
}
let content = val.get("message").and_then(|m| m.get("content"));
let text = extract_text_blocks(content);
if !text.trim().is_empty() {
return Some(text);
}
}
None
}
fn extract_text_blocks(content: Option<&serde_json::Value>) -> String {
match content {
Some(serde_json::Value::Array(blocks)) => blocks
.iter()
.filter_map(|b| b.get("text").and_then(serde_json::Value::as_str))
.collect::<Vec<_>>()
.join("\n"),
Some(serde_json::Value::String(s)) => s.clone(),
_ => String::new(),
}
}
pub fn detect_idle_parking_in_transcript(jsonl: &str) -> Option<&'static str> {
let last = last_assistant_text_from_transcript(jsonl)?;
detect_idle_parking(&last)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_each_parking_phrase() {
for phrase in PARKING_PHRASES {
let msg = format!("All set. {phrase} the CI completes and I'll continue.");
assert_eq!(
detect_idle_parking(&msg),
Some(*phrase),
"phrase {phrase:?} must be detected"
);
}
}
#[test]
fn detects_observed_incident_messages() {
let samples = [
"I've kicked off a background polling monitor for the release and will \
resume once it reports.",
"Monitoring PR #2606 checks via a passive watcher; standing by.",
"Waiting for the monitor to report the quality gate run.",
"I'll report back once the checks go green.",
];
for s in samples {
assert!(
detect_idle_parking(s).is_some(),
"incident message must be flagged: {s:?}"
);
}
}
#[test]
fn human_addressed_phrases_are_not_nudge_eligible() {
let msg =
"Deploy config is ready. Let me know once you approve the deploy and I'll ship it.";
let phrase = detect_idle_parking(msg).expect("must still be flagged for logging");
assert_eq!(phrase, "let me know once");
assert!(
!is_nudge_eligible(phrase),
"a human-addressed phrase must not be nudge-eligible"
);
for phrase in HUMAN_ADDRESSED_PHRASES {
assert!(
!is_nudge_eligible(phrase),
"{phrase:?} must be excluded from nudge eligibility"
);
}
}
#[test]
fn machine_wait_phrases_are_nudge_eligible() {
let msg = "I'll wait for the CI notification before continuing.";
let phrase = detect_idle_parking(msg).expect("must be flagged");
assert_eq!(phrase, "i'll wait for the");
assert!(
is_nudge_eligible(phrase),
"a machine-wait phrase must remain nudge-eligible"
);
for phrase in PARKING_PHRASES {
if HUMAN_ADDRESSED_PHRASES.contains(phrase) {
continue;
}
assert!(
is_nudge_eligible(phrase),
"{phrase:?} is not human-addressed and must be nudge-eligible"
);
}
}
#[test]
fn ignores_benign_final_message() {
let clean = [
"I ran `gh pr checks 2606 --watch --fail-fast` in the foreground; all \
12 checks passed. PR is green.",
"cargo test → test result: ok. 68 passed; 0 failed. Done.",
"Fixed the race condition in the async handler and verified the build.",
"",
" ",
];
for c in clean {
assert_eq!(detect_idle_parking(c), None, "must be clean: {c:?}");
}
}
#[test]
fn detection_is_case_insensitive() {
assert_eq!(detect_idle_parking("STANDING BY."), Some("standing by"));
assert_eq!(
detect_idle_parking("Will Report Back when it finishes"),
Some("will report back")
);
}
#[test]
fn empty_message_is_clean() {
assert_eq!(detect_idle_parking(""), None);
assert_eq!(detect_idle_parking("\n\t "), None);
}
#[test]
fn extracts_last_assistant_text() {
let jsonl = concat!(
r#"{"type":"user","message":{"content":[{"type":"text","text":"go"}]}}"#,
"\n",
r#"{"type":"assistant","message":{"content":[{"type":"text","text":"first"}]}}"#,
"\n",
r#"{"type":"assistant","message":{"content":[{"type":"text","text":"final answer"}]}}"#,
"\n",
);
assert_eq!(
last_assistant_text_from_transcript(jsonl).as_deref(),
Some("final answer")
);
}
#[test]
fn extracts_across_tool_use_blocks() {
let jsonl = concat!(
r#"{"type":"assistant","message":{"content":[{"type":"text","text":"standing by"},{"type":"tool_use","name":"Bash","input":{}}]}}"#,
"\n",
);
let text = last_assistant_text_from_transcript(jsonl).expect("has assistant text");
assert_eq!(text, "standing by");
assert_eq!(detect_idle_parking(&text), Some("standing by"));
}
#[test]
fn tolerates_truncated_and_blank_lines() {
let jsonl = concat!(
r#"pe":"assistant","message":{"content":[{"type":"text","text":"truncated"}"#,
"\n",
"\n",
r#"{"type":"assistant","message":{"content":[{"type":"text","text":"monitoring the checks"}]}}"#,
"\n",
" \n",
);
assert_eq!(
detect_idle_parking_in_transcript(jsonl),
Some("monitoring the checks")
);
}
#[test]
fn returns_none_without_assistant() {
let jsonl = concat!(
r#"{"type":"user","message":{"content":[{"type":"text","text":"waiting for the notification"}]}}"#,
"\n",
r#"{"type":"system","message":{"content":"standing by"}}"#,
"\n",
);
assert_eq!(last_assistant_text_from_transcript(jsonl), None);
assert_eq!(detect_idle_parking_in_transcript(jsonl), None);
}
#[test]
fn detects_parking_from_transcript() {
let jsonl = concat!(
r#"{"type":"assistant","message":{"content":[{"type":"text","text":"I'll report back once green."}]}}"#,
"\n",
);
assert_eq!(
detect_idle_parking_in_transcript(jsonl),
Some("report back once")
);
}
#[test]
fn clean_transcript_yields_none() {
let jsonl = concat!(
r#"{"type":"assistant","message":{"content":[{"type":"text","text":"All checks passed. Merged."}]}}"#,
"\n",
);
assert_eq!(detect_idle_parking_in_transcript(jsonl), None);
}
#[test]
fn bare_string_content_is_supported() {
let jsonl = r#"{"type":"assistant","message":{"content":"standing by for CI"}}"#;
assert_eq!(
last_assistant_text_from_transcript(jsonl).as_deref(),
Some("standing by for CI")
);
}
}