@@ -524,6 +524,7 @@ fn default_output_format_for_channel(channel_type: &str) -> OutputFormat {
524524 "telegram" => OutputFormat :: TelegramHtml ,
525525 "slack" => OutputFormat :: SlackMrkdwn ,
526526 "wecom" => OutputFormat :: PlainText ,
527+ "signal" => OutputFormat :: PlainText ,
527528 _ => OutputFormat :: Markdown ,
528529 }
529530}
@@ -642,12 +643,20 @@ async fn dispatch_message(
642643 . as_ref ( )
643644 . map ( |o| o. lifecycle_reactions )
644645 . unwrap_or ( true ) ;
645- let thread_id = if threading_enabled {
646- message. thread_id . as_deref ( )
646+
647+ // --- Auto-thread: decide intent now, but create AFTER all policy guards ---
648+ let auto_thread_name = if !threading_enabled && message. thread_id . is_none ( ) {
649+ adapter. should_auto_thread ( message) . await
647650 } else {
648651 None
649652 } ;
650653
654+ // thread_id is resolved later, after all guards pass.
655+ // Always propagate an existing thread_id (message arrived inside a thread),
656+ // regardless of threading_enabled — that flag controls explicit threading config,
657+ // not auto-detected thread context.
658+ let mut effective_thread_id: Option < String > = message. thread_id . clone ( ) ;
659+
651660 // --- DM/Group policy check ---
652661 if let Some ( ref ov) = overrides {
653662 if message. is_group {
@@ -708,12 +717,42 @@ async fn dispatch_message(
708717 if let Err ( msg) =
709718 rate_limiter. check ( ct_str, sender_user_id ( message) , ov. rate_limit_per_user )
710719 {
711- send_response ( adapter, & message. sender , msg, thread_id, output_format) . await ;
720+ // Rate-limit rejection: don't create a thread, use existing thread if any
721+ send_response (
722+ adapter,
723+ & message. sender ,
724+ msg,
725+ message. thread_id . as_deref ( ) ,
726+ output_format,
727+ )
728+ . await ;
712729 return ;
713730 }
714731 }
715732 }
716733
734+ // --- Create auto-thread NOW (after all policy guards have passed) ---
735+ if let Some ( ref thread_name) = auto_thread_name {
736+ match adapter
737+ . create_thread ( & message. sender , & message. platform_message_id , thread_name)
738+ . await
739+ {
740+ Ok ( new_thread_id) => {
741+ info ! (
742+ "Created auto-thread {} for message {}" ,
743+ thread_name, message. platform_message_id
744+ ) ;
745+ effective_thread_id = Some ( new_thread_id) ;
746+ }
747+ Err ( e) => {
748+ warn ! ( "Failed to create auto-thread: {}" , e) ;
749+ }
750+ }
751+ }
752+
753+ // Resolve final thread_id reference used by all downstream send_response calls
754+ let thread_id = effective_thread_id. as_deref ( ) ;
755+
717756 // Handle commands first (early return)
718757 if let ChannelContent :: Command { ref name, ref args } = message. content {
719758 let result = handle_command ( name, args, handle, router, & message. sender ) . await ;
@@ -1946,6 +1985,10 @@ mod tests {
19461985 default_output_format_for_channel( "discord" ) ,
19471986 OutputFormat :: Markdown
19481987 ) ;
1988+ assert_eq ! (
1989+ default_output_format_for_channel( "signal" ) ,
1990+ OutputFormat :: PlainText
1991+ )
19491992 }
19501993
19511994 #[ tokio:: test]
0 commit comments