From 59861303c643ab16089e1bd4ee43ebe964e9fc32 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Sat, 27 Jun 2026 08:55:32 -0400 Subject: [PATCH 1/7] feat(ambient): observe threads under configured channels by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ambient v1 only buffered top-level messages in a configured channel (gated on !in_thread). But most OpenAB conversation happens in auto-created threads, so ambient saw almost nothing. Now, when ambient is enabled, messages in a thread whose parent is an ambient channel are observed by default — keyed by the thread id so each thread batches independently. Only threads the bot does NOT own are observed passively; threads the bot owns keep using normal (immediate) dispatch, so there's no double-handling. @mention still discards the buffer and takes priority. No new config knob — ambient's existing 'enabled' flag is the opt-in; once on, it covers the channel and its threads. Routing is encapsulated in AmbientDispatcher::should_buffer(channel_id, in_thread, bot_owns_thread, parent_id) with unit tests. --- crates/openab-core/src/ambient.rs | 69 +++++++++++++++++++++++++++++++ crates/openab-core/src/discord.rs | 13 ++++-- docs/ambient.md | 4 +- 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/crates/openab-core/src/ambient.rs b/crates/openab-core/src/ambient.rs index d9ad774bd..228f2e620 100644 --- a/crates/openab-core/src/ambient.rs +++ b/crates/openab-core/src/ambient.rs @@ -286,6 +286,35 @@ impl AmbientDispatcher { self.config.enabled && !self.enabled_channels.is_empty() && self.enabled_channels.contains(&channel_id) } + /// Decide whether a message should be ambient-buffered. + /// + /// - Top-level message directly in an ambient channel → yes (buffer keyed by + /// `channel_id`). + /// - Thread message → yes when the thread's `parent_id` is an ambient channel + /// and the bot does NOT own the thread. Owned threads are actively handled + /// by normal (immediate) dispatch, so observing them here would double up. + /// Threads are batched independently (keyed by the thread's `channel_id`). + /// + /// Threads under an ambient channel are observed by default — most OpenAB + /// conversation happens in auto-created threads, not the parent channel. + /// + /// Returns false when ambient is disabled or no channels are configured. + pub fn should_buffer( + &self, + channel_id: u64, + in_thread: bool, + bot_owns_thread: bool, + parent_id: Option, + ) -> bool { + if !self.config.enabled || self.enabled_channels.is_empty() { + return false; + } + if !in_thread { + return self.enabled_channels.contains(&channel_id); + } + !bot_owns_thread && parent_id.is_some_and(|p| self.enabled_channels.contains(&p)) + } + /// Whether bot messages are allowed in the ambient buffer. pub fn allow_bot_messages(&self) -> bool { self.config.discord.allow_bot_messages @@ -584,6 +613,46 @@ pub fn is_no_reply(response: &str) -> bool { mod tests { use super::*; + fn dispatcher(channels: &[&str], enabled: bool) -> AmbientDispatcher { + let config = AmbientConfig { + enabled, + discord: crate::config::AmbientDiscordConfig { + channels: channels.iter().map(|s| s.to_string()).collect(), + ..Default::default() + }, + ..Default::default() + }; + AmbientDispatcher::new(config) + } + + #[test] + fn should_buffer_toplevel_message_in_ambient_channel() { + let d = dispatcher(&["100"], true); + assert!(d.should_buffer(100, false, false, None)); + // Different channel → not buffered. + assert!(!d.should_buffer(200, false, false, None)); + } + + #[test] + fn should_buffer_thread_under_ambient_channel_by_default() { + let d = dispatcher(&["100"], true); + // Thread under an ambient channel, bot does not own it → buffer (default). + assert!(d.should_buffer(999, true, false, Some(100))); + // Thread whose parent is NOT an ambient channel → no. + assert!(!d.should_buffer(999, true, false, Some(200))); + // Thread the bot owns → normal dispatch handles it, not ambient. + assert!(!d.should_buffer(999, true, true, Some(100))); + // Thread with no resolvable parent → no. + assert!(!d.should_buffer(999, true, false, None)); + } + + #[test] + fn should_buffer_false_when_ambient_disabled() { + let d = dispatcher(&["100"], false); + assert!(!d.should_buffer(100, false, false, None)); + assert!(!d.should_buffer(999, true, false, Some(100))); + } + #[test] fn is_no_reply_exact() { assert!(is_no_reply("[NO_REPLY]")); diff --git a/crates/openab-core/src/discord.rs b/crates/openab-core/src/discord.rs index a212a7c25..6078f9e0d 100644 --- a/crates/openab-core/src/discord.rs +++ b/crates/openab-core/src/discord.rs @@ -608,11 +608,16 @@ impl EventHandler for Handler { } // --- Ambient Mode routing --- - // If the message is in an ambient-enabled channel, NOT a @mention, - // NOT in a thread, and NOT a DM → route to ambient dispatcher. - // @mention in an ambient channel → discard buffer + normal dispatch. + // Route to ambient when the message belongs to an ambient context: + // - a top-level message directly in an ambient channel, or + // - (include_threads) a message in a thread under an ambient channel + // that the bot does NOT own (owned threads use normal dispatch). + // @mention in an ambient context → discard buffer + normal dispatch. if let Some(ref ambient) = self.ambient { - if ambient.is_ambient_channel(channel_id) && !in_thread && !is_dm { + let parent_u64 = thread_parent_id + .as_deref() + .and_then(|p| p.parse::().ok()); + if !is_dm && ambient.should_buffer(channel_id, in_thread, bot_owns_thread, parent_u64) { if is_mentioned { // Discard ambient buffer — mention takes priority. ambient.discard_buffer(&channel_id.to_string()).await; diff --git a/docs/ambient.md b/docs/ambient.md index 81f3ac17e..e3c403597 100644 --- a/docs/ambient.md +++ b/docs/ambient.md @@ -23,7 +23,7 @@ max_concurrent_flushes = 3 # Global LLM concurrency limit flush_timeout_seconds = 120 # Safety timeout per flush [ambient.discord] -channels = ["1234567890"] # Channel IDs to monitor +channels = ["1234567890"] # Channel IDs to monitor (and their threads) allow_bot_messages = false # Include other bots' messages in buffer ``` @@ -40,6 +40,8 @@ allow_bot_messages = false # Include other bots' messages in buffer | `channels` | `[]` | Explicit channel allowlist (required). Empty = ambient disabled. | | `allow_bot_messages` | `false` | Whether other bots' messages enter the ambient buffer. | +> **Threads are observed by default.** Messages in **threads** whose parent is a configured channel are buffered too (most OpenAB conversation happens in auto-created threads, not the parent channel). Threads the bot does **not** own are observed passively; threads the bot **owns** are handled by normal (immediate) dispatch instead, so there's no double-reply. Each thread batches independently (keyed by the thread ID), and an @mention in a thread discards its buffer and gets an immediate reply. + ### Reserved fields (v2, not yet enforced) | Field | Default | Description | From 634dbaf077d69f8717e8cf710587b783b2c1ab47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E6=B8=A1=E6=B3=95=E5=B8=AB?= Date: Sat, 27 Jun 2026 13:06:46 +0000 Subject: [PATCH 2/7] fix(ambient): observe bot-owned threads too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the !bot_owns_thread gate from should_buffer(). Bot-owned threads are now also passively observed via ambient — the bot can follow thread conversation it started without requiring @mention. An @mention still discards the buffer and triggers immediate dispatch. --- crates/openab-core/src/ambient.rs | 19 +++++++++++-------- crates/openab-core/src/discord.rs | 4 ++-- docs/ambient.md | 2 +- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/crates/openab-core/src/ambient.rs b/crates/openab-core/src/ambient.rs index 228f2e620..9efa8d796 100644 --- a/crates/openab-core/src/ambient.rs +++ b/crates/openab-core/src/ambient.rs @@ -290,9 +290,11 @@ impl AmbientDispatcher { /// /// - Top-level message directly in an ambient channel → yes (buffer keyed by /// `channel_id`). - /// - Thread message → yes when the thread's `parent_id` is an ambient channel - /// and the bot does NOT own the thread. Owned threads are actively handled - /// by normal (immediate) dispatch, so observing them here would double up. + /// - Thread message → yes when the thread's `parent_id` is an ambient channel, + /// regardless of whether the bot owns the thread. Bot-owned threads are + /// also observed so the bot can passively follow conversation without + /// requiring an @mention. An @mention in any ambient context discards the + /// buffer and falls through to immediate dispatch — no double handling. /// Threads are batched independently (keyed by the thread's `channel_id`). /// /// Threads under an ambient channel are observed by default — most OpenAB @@ -303,7 +305,7 @@ impl AmbientDispatcher { &self, channel_id: u64, in_thread: bool, - bot_owns_thread: bool, + _bot_owns_thread: bool, parent_id: Option, ) -> bool { if !self.config.enabled || self.enabled_channels.is_empty() { @@ -312,7 +314,7 @@ impl AmbientDispatcher { if !in_thread { return self.enabled_channels.contains(&channel_id); } - !bot_owns_thread && parent_id.is_some_and(|p| self.enabled_channels.contains(&p)) + parent_id.is_some_and(|p| self.enabled_channels.contains(&p)) } /// Whether bot messages are allowed in the ambient buffer. @@ -636,12 +638,13 @@ mod tests { #[test] fn should_buffer_thread_under_ambient_channel_by_default() { let d = dispatcher(&["100"], true); - // Thread under an ambient channel, bot does not own it → buffer (default). + // Thread under an ambient channel, bot does not own it → buffer. assert!(d.should_buffer(999, true, false, Some(100))); // Thread whose parent is NOT an ambient channel → no. assert!(!d.should_buffer(999, true, false, Some(200))); - // Thread the bot owns → normal dispatch handles it, not ambient. - assert!(!d.should_buffer(999, true, true, Some(100))); + // Thread the bot owns → ALSO buffered (bot passively observes its own + // threads; @mention triggers immediate dispatch with buffer discard). + assert!(d.should_buffer(999, true, true, Some(100))); // Thread with no resolvable parent → no. assert!(!d.should_buffer(999, true, false, None)); } diff --git a/crates/openab-core/src/discord.rs b/crates/openab-core/src/discord.rs index 6078f9e0d..1a3b10fc3 100644 --- a/crates/openab-core/src/discord.rs +++ b/crates/openab-core/src/discord.rs @@ -610,8 +610,8 @@ impl EventHandler for Handler { // --- Ambient Mode routing --- // Route to ambient when the message belongs to an ambient context: // - a top-level message directly in an ambient channel, or - // - (include_threads) a message in a thread under an ambient channel - // that the bot does NOT own (owned threads use normal dispatch). + // - a message in a thread under an ambient channel (including + // bot-owned threads — the bot passively observes all threads). // @mention in an ambient context → discard buffer + normal dispatch. if let Some(ref ambient) = self.ambient { let parent_u64 = thread_parent_id diff --git a/docs/ambient.md b/docs/ambient.md index e3c403597..9f2450727 100644 --- a/docs/ambient.md +++ b/docs/ambient.md @@ -40,7 +40,7 @@ allow_bot_messages = false # Include other bots' messages in buffer | `channels` | `[]` | Explicit channel allowlist (required). Empty = ambient disabled. | | `allow_bot_messages` | `false` | Whether other bots' messages enter the ambient buffer. | -> **Threads are observed by default.** Messages in **threads** whose parent is a configured channel are buffered too (most OpenAB conversation happens in auto-created threads, not the parent channel). Threads the bot does **not** own are observed passively; threads the bot **owns** are handled by normal (immediate) dispatch instead, so there's no double-reply. Each thread batches independently (keyed by the thread ID), and an @mention in a thread discards its buffer and gets an immediate reply. +> **Threads are observed by default.** Messages in **threads** whose parent is a configured channel are buffered too (most OpenAB conversation happens in auto-created threads, not the parent channel). **Both** bot-owned and non-owned threads are observed — the bot passively follows all thread conversation under an ambient channel. An @mention in any thread discards its buffer and triggers immediate dispatch, so there is no double-reply. Each thread batches independently (keyed by the thread ID). ### Reserved fields (v2, not yet enforced) From dd2d99f79c7d2bd29f3ed527613455b400c6a7e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E6=B8=A1=E6=B3=95=E5=B8=AB?= Date: Sat, 27 Jun 2026 13:19:10 +0000 Subject: [PATCH 3/7] fix(ambient): decouple thread detection from normal allowlist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Z渡 correctly identified that ambient thread observation was unreachable when the channel was only in [ambient.discord].channels but not in [discord].allowed_channels. The early return gate and detect_thread() both depended on the normal allowlist. Fix: - Always preserve thread_parent_id (not gated on detect_thread result) - Add is_structural_thread from thread_metadata for ambient routing - Compute in_ambient_context before the early return gate - Let ambient-eligible messages bypass the normal allowlist gate Also addresses reviewer suggestions: - Add startup info! log when ambient+threads is active - Add tracing::debug! on thread_parent_id parse failure --- crates/openab-core/src/ambient.rs | 6 +++++ crates/openab-core/src/discord.rs | 38 ++++++++++++++++++++----------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/crates/openab-core/src/ambient.rs b/crates/openab-core/src/ambient.rs index 9efa8d796..8d7506509 100644 --- a/crates/openab-core/src/ambient.rs +++ b/crates/openab-core/src/ambient.rs @@ -273,6 +273,12 @@ impl AmbientDispatcher { .filter_map(|s| s.parse().ok()) .collect(); let flush_semaphore = Arc::new(Semaphore::new(config.max_concurrent_flushes.max(1))); + if config.enabled && !enabled_channels.is_empty() { + tracing::info!( + channels = ?enabled_channels, + "ambient: thread observation is default-on for configured channels" + ); + } Self { config, channels: Mutex::new(HashMap::new()), diff --git a/crates/openab-core/src/discord.rs b/crates/openab-core/src/discord.rs index 1a3b10fc3..6d85fc0c2 100644 --- a/crates/openab-core/src/discord.rs +++ b/crates/openab-core/src/discord.rs @@ -551,15 +551,16 @@ impl EventHandler for Handler { // Thread detection: single to_channel() call for both allowed and // non-allowed channels. Uses thread_metadata (not parent_id) to // identify threads — see detect_thread() doc comments for rationale. - let (in_thread, bot_owns_thread, thread_parent_id, is_dm) = match msg + let (in_thread, bot_owns_thread, thread_parent_id, is_dm, is_structural_thread) = match msg .channel_id .to_channel(&ctx.http) .await { Ok(serenity::model::channel::Channel::Guild(gc)) => { let parent = gc.parent_id.map(|id| id.get().to_string()); + let has_thread_metadata = gc.thread_metadata.is_some(); let result = detect_thread( - gc.thread_metadata.is_some(), + has_thread_metadata, gc.parent_id.map(|id| id.get()), gc.owner_id.map(|id| id.get()), bot_id.get(), @@ -571,7 +572,7 @@ impl EventHandler for Handler { channel_id = %msg.channel_id, parent_id = ?gc.parent_id, owner_id = ?gc.owner_id, - has_thread_metadata = gc.thread_metadata.is_some(), + has_thread_metadata, in_thread = result.0, bot_owns = ?result.1, "thread check" @@ -579,21 +580,22 @@ impl EventHandler for Handler { ( result.0, result.1.unwrap_or(false), - if result.0 { parent } else { None }, + parent, false, + has_thread_metadata, ) } Ok(serenity::model::channel::Channel::Private(_)) => { tracing::debug!(channel_id = %msg.channel_id, "DM channel"); - (false, false, None, true) + (false, false, None, true, false) } Ok(other) => { tracing::debug!(channel_id = %msg.channel_id, kind = ?other, "not a guild thread"); - (false, false, None, false) + (false, false, None, false, false) } Err(e) => { tracing::debug!(channel_id = %msg.channel_id, error = %e, "to_channel failed"); - (false, false, None, false) + (false, false, None, false, false) } }; @@ -603,7 +605,19 @@ impl EventHandler for Handler { return; } - if !is_dm && !in_allowed_channel && !in_thread { + // Check if message is in an ambient context (needed before early return). + // Uses structural thread detection (thread_metadata) decoupled from the + // normal dispatch allowlist, so ambient-only channels work correctly. + let in_ambient_context = self.ambient.as_ref().is_some_and(|ambient| { + let parent_u64 = thread_parent_id.as_deref().and_then(|p| { + p.parse::().map_err(|e| { + tracing::debug!(parent_id = p, error = %e, "failed to parse thread parent_id"); + }).ok() + }); + ambient.should_buffer(channel_id, is_structural_thread, bot_owns_thread, parent_u64) + }); + + if !is_dm && !in_allowed_channel && !in_thread && !in_ambient_context { return; } @@ -613,11 +627,9 @@ impl EventHandler for Handler { // - a message in a thread under an ambient channel (including // bot-owned threads — the bot passively observes all threads). // @mention in an ambient context → discard buffer + normal dispatch. - if let Some(ref ambient) = self.ambient { - let parent_u64 = thread_parent_id - .as_deref() - .and_then(|p| p.parse::().ok()); - if !is_dm && ambient.should_buffer(channel_id, in_thread, bot_owns_thread, parent_u64) { + if in_ambient_context { + let ambient = self.ambient.as_ref().unwrap(); + if !is_dm { if is_mentioned { // Discard ambient buffer — mention takes priority. ambient.discard_buffer(&channel_id.to_string()).await; From 12b4d8ecd1e46f93df0f51aca14ac2461f72c786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E6=B8=A1=E6=B3=95=E5=B8=AB?= Date: Sat, 27 Jun 2026 13:19:37 +0000 Subject: [PATCH 4/7] docs: sync ambient channel comments to mention thread observation --- docs/config-reference.md | 2 +- docs/discord.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/config-reference.md b/docs/config-reference.md index c21aa38c0..e4e42d944 100644 --- a/docs/config-reference.md +++ b/docs/config-reference.md @@ -483,7 +483,7 @@ session_ttl_minutes = 60 context_flushes = 3 [ambient.discord] -channels = [] # Channel ID allowlist (required) +channels = [] # Channel ID allowlist — and their threads (required) allow_bot_messages = false ``` diff --git a/docs/discord.md b/docs/discord.md index f152b026a..e179d8a1c 100644 --- a/docs/discord.md +++ b/docs/discord.md @@ -218,7 +218,7 @@ Ambient mode allows the bot to passively listen to configured channels and respo enabled = true [ambient.discord] -channels = ["1234567890"] # Channel IDs to monitor +channels = ["1234567890"] # Channel IDs to monitor (and their threads) ``` When enabled: From 7ea0ac3f2636029ac858305409311eb8378531b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E6=B8=A1=E6=B3=95=E5=B8=AB?= Date: Sat, 27 Jun 2026 13:21:31 +0000 Subject: [PATCH 5/7] docs(ambient): clarify allowed_channels vs ambient channels relationship --- docs/ambient.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/ambient.md b/docs/ambient.md index 9f2450727..67f83c665 100644 --- a/docs/ambient.md +++ b/docs/ambient.md @@ -42,6 +42,15 @@ allow_bot_messages = false # Include other bots' messages in buffer > **Threads are observed by default.** Messages in **threads** whose parent is a configured channel are buffered too (most OpenAB conversation happens in auto-created threads, not the parent channel). **Both** bot-owned and non-owned threads are observed — the bot passively follows all thread conversation under an ambient channel. An @mention in any thread discards its buffer and triggers immediate dispatch, so there is no double-reply. Each thread batches independently (keyed by the thread ID). +> **`[ambient.discord].channels` vs `[discord].allowed_channels`** — these are independent allowlists with an OR relationship: +> +> | Config | Purpose | Effect | +> |--------|---------|--------| +> | `[discord].allowed_channels` | Normal dispatch | Bot responds to @mentions and direct messages in these channels/threads | +> | `[ambient.discord].channels` | Passive observation | Bot silently buffers messages (no @mention required) and decides whether to reply | +> +> A channel can appear in one or both. Ambient observation does **not** require the channel to also be in `allowed_channels`. + ### Reserved fields (v2, not yet enforced) | Field | Default | Description | From c81b8a3ba9570c37679d0c3e243cc67c32dab96f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E6=B8=A1=E6=B3=95=E5=B8=AB?= Date: Sat, 27 Jun 2026 13:23:12 +0000 Subject: [PATCH 6/7] docs(ambient): clarify @mention always works in ambient-only channels --- docs/ambient.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/ambient.md b/docs/ambient.md index 67f83c665..f78faa86e 100644 --- a/docs/ambient.md +++ b/docs/ambient.md @@ -50,6 +50,8 @@ allow_bot_messages = false # Include other bots' messages in buffer > | `[ambient.discord].channels` | Passive observation | Bot silently buffers messages (no @mention required) and decides whether to reply | > > A channel can appear in one or both. Ambient observation does **not** require the channel to also be in `allowed_channels`. +> +> **@mention always works in ambient channels.** Even if a channel is only in `[ambient.discord].channels` (not in `allowed_channels`), an @mention still triggers immediate normal dispatch — the ambient buffer is discarded and the bot responds directly. The only difference between the two configs is behavior **without** a mention: `allowed_channels` ignores unmentioned messages entirely, while `ambient.discord.channels` passively observes them. ### Reserved fields (v2, not yet enforced) From 5d47cfcde019c69eebdefe6745e716d4c644eef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E6=B8=A1=E6=B3=95=E5=B8=AB?= Date: Sat, 27 Jun 2026 13:28:46 +0000 Subject: [PATCH 7/7] fix(ambient): gate thread_parent_id on has_thread_metadata Unconditionally setting thread_parent_id to gc.parent_id caused non-thread channels (under categories) to incorrectly populate SenderContext with the category ID as parent. Fix: split into two fields: - thread_parent_id (String): gated on has_thread_metadata, used for SenderContext and downstream - structural_parent_id (u64): gated on has_thread_metadata, used for ambient routing (avoids string parsing entirely) --- crates/openab-core/src/discord.rs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/crates/openab-core/src/discord.rs b/crates/openab-core/src/discord.rs index 6d85fc0c2..71c4054c8 100644 --- a/crates/openab-core/src/discord.rs +++ b/crates/openab-core/src/discord.rs @@ -551,7 +551,7 @@ impl EventHandler for Handler { // Thread detection: single to_channel() call for both allowed and // non-allowed channels. Uses thread_metadata (not parent_id) to // identify threads — see detect_thread() doc comments for rationale. - let (in_thread, bot_owns_thread, thread_parent_id, is_dm, is_structural_thread) = match msg + let (in_thread, bot_owns_thread, thread_parent_id, is_dm, is_structural_thread, structural_parent_id) = match msg .channel_id .to_channel(&ctx.http) .await @@ -559,9 +559,10 @@ impl EventHandler for Handler { Ok(serenity::model::channel::Channel::Guild(gc)) => { let parent = gc.parent_id.map(|id| id.get().to_string()); let has_thread_metadata = gc.thread_metadata.is_some(); + let parent_u64 = gc.parent_id.map(|id| id.get()); let result = detect_thread( has_thread_metadata, - gc.parent_id.map(|id| id.get()), + parent_u64, gc.owner_id.map(|id| id.get()), bot_id.get(), &self.allowed_channels, @@ -580,22 +581,23 @@ impl EventHandler for Handler { ( result.0, result.1.unwrap_or(false), - parent, + if has_thread_metadata { parent } else { None }, false, has_thread_metadata, + if has_thread_metadata { parent_u64 } else { None }, ) } Ok(serenity::model::channel::Channel::Private(_)) => { tracing::debug!(channel_id = %msg.channel_id, "DM channel"); - (false, false, None, true, false) + (false, false, None, true, false, None) } Ok(other) => { tracing::debug!(channel_id = %msg.channel_id, kind = ?other, "not a guild thread"); - (false, false, None, false, false) + (false, false, None, false, false, None) } Err(e) => { tracing::debug!(channel_id = %msg.channel_id, error = %e, "to_channel failed"); - (false, false, None, false, false) + (false, false, None, false, false, None) } }; @@ -609,12 +611,7 @@ impl EventHandler for Handler { // Uses structural thread detection (thread_metadata) decoupled from the // normal dispatch allowlist, so ambient-only channels work correctly. let in_ambient_context = self.ambient.as_ref().is_some_and(|ambient| { - let parent_u64 = thread_parent_id.as_deref().and_then(|p| { - p.parse::().map_err(|e| { - tracing::debug!(parent_id = p, error = %e, "failed to parse thread parent_id"); - }).ok() - }); - ambient.should_buffer(channel_id, is_structural_thread, bot_owns_thread, parent_u64) + ambient.should_buffer(channel_id, is_structural_thread, bot_owns_thread, structural_parent_id) }); if !is_dm && !in_allowed_channel && !in_thread && !in_ambient_context {