Skip to content

Commit 3ae80ab

Browse files
howieclaude
andcommitted
fix(adapter): address session-reset sentinel, suppress() reactions, Slack delete
Four fixes on top of the initial silence-handling commit: 1. Session reset + sentinel (Codex P2): text_buf was pre-populated with the reset prelude before agent output, so text_buf.trim() == "<silent />" never matched after a session reset. Track reset_prelude separately and prepend it to streaming updates and final_content after sentinel/suppress checks. 2. reactions.suppress() (Claude Important): early-return suppress paths returned Ok(()), causing dispatch.rs to call reactions.set_done() and add a done emoji to the user message even though no reply was posted. Added suppress() to StatusReactionController (clear + finished=true) so the subsequent set_done() is a no-op. 3. delete_message error logging (Claude Important): tokio::spawn blocks for placeholder deletion were silently discarding errors. Now emit warn! so failures appear in telemetry. 4. Slack delete_message (Codex P2): SlackAdapter lacked a delete_message override, falling back to edit_message with U+200B. Added chat.delete so the placeholder is truly removed rather than left as a blank message. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a97188c commit 3ae80ab

3 files changed

Lines changed: 78 additions & 21 deletions

File tree

src/adapter.rs

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -478,10 +478,14 @@ impl AdapterRouter {
478478

479479
let mut text_buf = String::new();
480480
let mut tool_lines: Vec<ToolEntry> = Vec::new();
481-
482-
if reset {
483-
text_buf.push_str("⚠️ _Session expired, starting fresh..._\n\n");
484-
}
481+
// Kept separate from text_buf so the sentinel check ("is the
482+
// agent's actual output <silent />?") is not confused by the
483+
// synthetic prelude. Prepended to final_content before send.
484+
let reset_prelude = if reset {
485+
"⚠️ _Session expired, starting fresh..._\n\n"
486+
} else {
487+
""
488+
};
485489

486490
// Streaming edit: send placeholder, spawn edit loop
487491
let (buf_tx, placeholder_msg) = if streaming {
@@ -587,11 +591,14 @@ impl AdapterRouter {
587591
AcpEvent::Text(t) => {
588592
text_buf.push_str(&t);
589593
if let Some(tx) = &buf_tx {
590-
let _ = tx.send(compose_display(
591-
&tool_lines,
592-
&text_buf,
593-
true,
594-
tool_display,
594+
let _ = tx.send(format!(
595+
"{reset_prelude}{}",
596+
compose_display(
597+
&tool_lines,
598+
&text_buf,
599+
true,
600+
tool_display,
601+
)
595602
));
596603
}
597604
}
@@ -612,11 +619,14 @@ impl AdapterRouter {
612619
});
613620
}
614621
if let Some(tx) = &buf_tx {
615-
let _ = tx.send(compose_display(
616-
&tool_lines,
617-
&text_buf,
618-
true,
619-
tool_display,
622+
let _ = tx.send(format!(
623+
"{reset_prelude}{}",
624+
compose_display(
625+
&tool_lines,
626+
&text_buf,
627+
true,
628+
tool_display,
629+
)
620630
));
621631
}
622632
}
@@ -640,11 +650,14 @@ impl AdapterRouter {
640650
});
641651
}
642652
if let Some(tx) = &buf_tx {
643-
let _ = tx.send(compose_display(
644-
&tool_lines,
645-
&text_buf,
646-
true,
647-
tool_display,
653+
let _ = tx.send(format!(
654+
"{reset_prelude}{}",
655+
compose_display(
656+
&tool_lines,
657+
&text_buf,
658+
true,
659+
tool_display,
660+
)
648661
));
649662
}
650663
}
@@ -669,9 +682,14 @@ impl AdapterRouter {
669682
// Sentinel: checked post-loop — chunks may transiently match mid-stream.
670683
if text_buf.trim() == "<silent />" {
671684
info!(platform = %adapter.platform(), "agent emitted <silent /> sentinel -- suppressing reply");
685+
reactions.suppress().await;
672686
if let Some(msg) = placeholder_msg {
673687
let a = adapter.clone();
674-
tokio::spawn(async move { let _ = a.delete_message(&msg).await; });
688+
tokio::spawn(async move {
689+
if let Err(e) = a.delete_message(&msg).await {
690+
warn!(error = ?e, "delete placeholder failed after silent sentinel");
691+
}
692+
});
675693
}
676694
return Ok(());
677695
}
@@ -684,9 +702,14 @@ impl AdapterRouter {
684702
format!("⚠️ {err}")
685703
} else if !empty_reply_placeholder {
686704
debug!(platform = %adapter.platform(), "empty reply suppressed; empty_reply_placeholder disabled");
705+
reactions.suppress().await;
687706
if let Some(msg) = placeholder_msg {
688707
let a = adapter.clone();
689-
tokio::spawn(async move { let _ = a.delete_message(&msg).await; });
708+
tokio::spawn(async move {
709+
if let Err(e) = a.delete_message(&msg).await {
710+
warn!(error = ?e, "delete placeholder failed after empty reply suppression");
711+
}
712+
});
690713
}
691714
return Ok(());
692715
} else {
@@ -698,6 +721,7 @@ impl AdapterRouter {
698721
final_content
699722
};
700723

724+
let final_content = format!("{reset_prelude}{final_content}");
701725
let final_content = markdown::convert_tables(&final_content, table_mode);
702726
let chunks = format::split_message(&final_content, message_limit);
703727
if let Some(msg) = placeholder_msg {

src/reactions.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,27 @@ impl StatusReactionController {
129129
}
130130
}
131131

132+
/// Remove the current reaction, cancel all timers, and mark as finished so
133+
/// subsequent set_done/set_error calls are no-ops. Used when a reply is
134+
/// suppressed (sentinel or empty_reply_placeholder=false) — prevents the
135+
/// done emoji from appearing on a message the user never saw a reply for.
136+
pub async fn suppress(&self) {
137+
if !self.enabled {
138+
return;
139+
}
140+
let mut inner = self.inner.lock().await;
141+
inner.finished = true;
142+
cancel_timers(&mut inner);
143+
let current = inner.current.clone();
144+
if !current.is_empty() {
145+
let _ = inner
146+
.adapter
147+
.remove_reaction(&inner.message, &current)
148+
.await;
149+
inner.current.clear();
150+
}
151+
}
152+
132153
async fn apply_immediate(&self, emoji: &str) {
133154
let mut inner = self.inner.lock().await;
134155
if inner.finished || emoji == inner.current {

src/slack.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,18 @@ impl ChatAdapter for SlackAdapter {
447447
}
448448
}
449449

450+
async fn delete_message(&self, msg: &MessageRef) -> Result<()> {
451+
self.api_post(
452+
"chat.delete",
453+
serde_json::json!({
454+
"channel": msg.channel.channel_id,
455+
"ts": msg.message_id,
456+
}),
457+
)
458+
.await?;
459+
Ok(())
460+
}
461+
450462
async fn edit_message(&self, msg: &MessageRef, content: &str) -> Result<()> {
451463
let mrkdwn = markdown_to_mrkdwn(content);
452464
self.api_post(

0 commit comments

Comments
 (0)