@@ -160,6 +160,9 @@ pub struct Handler {
160160 pub bot_turns : tokio:: sync:: Mutex < BotTurnTracker > ,
161161 /// Allow the bot to respond to Discord DMs.
162162 pub allow_dm : bool ,
163+ /// Batched-mode dispatcher (None in per-message mode).
164+ pub dispatcher : Option < Arc < crate :: dispatch:: Dispatcher > > ,
165+ pub message_processing_mode : crate :: config:: MessageProcessingMode ,
163166}
164167
165168impl Handler {
@@ -527,6 +530,7 @@ impl EventHandler for Handler {
527530 & msg. channel_id . to_string ( ) ,
528531 thread_parent_id. as_deref ( ) ,
529532 msg. author . bot ,
533+ & msg. timestamp . to_rfc3339 ( ) . unwrap_or_default ( ) ,
530534 ) ;
531535
532536 // Build extra content blocks from attachments (audio → STT, text → inline, image → encode)
@@ -622,19 +626,58 @@ impl EventHandler for Handler {
622626 let trigger_msg = discord_msg_ref ( & msg) ;
623627
624628 // Per-thread streaming: check if another bot is present in this thread
625- let other_bot_present = {
629+ let other_bot_present_flag = {
626630 let cache = self . multibot_threads . lock ( ) . await ;
627631 cache. contains_key ( & msg. channel_id . to_string ( ) )
628632 } ;
629633
630634 let router = self . router . clone ( ) ;
635+ let mode = self . message_processing_mode ;
636+ let dispatcher = self . dispatcher . clone ( ) ;
637+
631638 tokio:: spawn ( async move {
632639 let sender_json = serde_json:: to_string ( & sender) . unwrap ( ) ;
633- if let Err ( e) = router
634- . handle_message ( & adapter, & thread_channel, & sender_json, & prompt, extra_blocks, & trigger_msg, other_bot_present)
635- . await
636- {
637- error ! ( "handle_message error: {e}" ) ;
640+ match mode {
641+ crate :: config:: MessageProcessingMode :: PerMessage => {
642+ if let Err ( e) = router
643+ . handle_message (
644+ & adapter,
645+ & thread_channel,
646+ & sender_json,
647+ & prompt,
648+ extra_blocks,
649+ & trigger_msg,
650+ other_bot_present_flag,
651+ )
652+ . await
653+ {
654+ error ! ( "handle_message error: {e}" ) ;
655+ }
656+ }
657+ crate :: config:: MessageProcessingMode :: Batched => {
658+ if let Some ( dispatcher) = dispatcher {
659+ let thread_key = format ! ( "discord:{}" , thread_channel. channel_id) ;
660+ let estimated_tokens =
661+ crate :: dispatch:: estimate_tokens ( & prompt, & extra_blocks) ;
662+ let buf_msg = crate :: dispatch:: BufferedMessage {
663+ sender_json,
664+ prompt,
665+ extra_blocks,
666+ trigger_msg,
667+ arrived_at : std:: time:: Instant :: now ( ) ,
668+ estimated_tokens,
669+ other_bot_present : other_bot_present_flag,
670+ } ;
671+ if let Err ( e) = dispatcher
672+ . submit ( thread_key, thread_channel, adapter, buf_msg)
673+ . await
674+ {
675+ error ! ( "dispatcher submit error: {e}" ) ;
676+ }
677+ } else {
678+ error ! ( "batched mode enabled but no dispatcher configured" ) ;
679+ }
680+ }
638681 }
639682 } ) ;
640683 }
@@ -854,10 +897,25 @@ impl Handler {
854897 cmd : & serenity:: model:: application:: CommandInteraction ,
855898 ) {
856899 let thread_key = format ! ( "discord:{}" , cmd. channel_id. get( ) ) ;
900+
901+ // Drop any messages buffered for this thread before resetting the session.
902+ // /reset semantics include /cancel-all: discard buffered work, then reset.
903+ let dropped = self
904+ . dispatcher
905+ . as_ref ( )
906+ . map ( |d| d. cancel_buffered ( & thread_key) )
907+ . unwrap_or ( 0 ) ;
908+
857909 let result = self . router . pool ( ) . reset_session ( & thread_key) . await ;
858910
859911 let msg = match result {
912+ Ok ( ( ) ) if dropped > 0 => {
913+ format ! ( "🔄 Session reset. Dropped {dropped} buffered message(s). Start a new conversation!" )
914+ }
860915 Ok ( ( ) ) => "🔄 Session reset. Start a new conversation!" . to_string ( ) ,
916+ Err ( _) if dropped > 0 => {
917+ format ! ( "🔄 Dropped {dropped} buffered message(s). No active session to reset." )
918+ }
861919 Err ( _) => "⚠️ No active session to reset. Start a conversation first by @mentioning the bot." . to_string ( ) ,
862920 } ;
863921
@@ -1096,6 +1154,7 @@ fn build_sender_context(
10961154 msg_channel_id : & str ,
10971155 thread_parent_id : Option < & str > ,
10981156 is_bot : bool ,
1157+ timestamp : & str ,
10991158) -> SenderContext {
11001159 SenderContext {
11011160 schema : "openab.sender.v1" . into ( ) ,
@@ -1106,6 +1165,7 @@ fn build_sender_context(
11061165 channel_id : thread_parent_id. unwrap_or ( msg_channel_id) . to_string ( ) ,
11071166 thread_id : thread_parent_id. map ( |_| msg_channel_id. to_string ( ) ) ,
11081167 is_bot,
1168+ timestamp : timestamp. to_string ( ) ,
11091169 }
11101170}
11111171
@@ -1430,7 +1490,7 @@ mod tests {
14301490 /// In-thread message: channel_id = parent, thread_id = thread channel ID.
14311491 #[ test]
14321492 fn build_sender_context_in_thread ( ) {
1433- let ctx = build_sender_context ( "user1" , "alice" , "Alice" , "thread_ch" , Some ( "parent_ch" ) , false ) ;
1493+ let ctx = build_sender_context ( "user1" , "alice" , "Alice" , "thread_ch" , Some ( "parent_ch" ) , false , "2026-05-01T00:00:00Z" ) ;
14341494 assert_eq ! ( ctx. channel_id, "parent_ch" ) ;
14351495 assert_eq ! ( ctx. thread_id, Some ( "thread_ch" . to_string( ) ) ) ;
14361496 assert_eq ! ( ctx. channel, "discord" ) ;
@@ -1441,15 +1501,15 @@ mod tests {
14411501 /// Non-thread message: channel_id = message channel, thread_id = None.
14421502 #[ test]
14431503 fn build_sender_context_not_in_thread ( ) {
1444- let ctx = build_sender_context ( "user1" , "alice" , "Alice" , "main_ch" , None , false ) ;
1504+ let ctx = build_sender_context ( "user1" , "alice" , "Alice" , "main_ch" , None , false , "2026-05-01T00:00:00Z" ) ;
14451505 assert_eq ! ( ctx. channel_id, "main_ch" ) ;
14461506 assert_eq ! ( ctx. thread_id, None ) ;
14471507 }
14481508
14491509 /// Bot sender: is_bot flag propagated correctly.
14501510 #[ test]
14511511 fn build_sender_context_bot_sender ( ) {
1452- let ctx = build_sender_context ( "bot1" , "mybot" , "MyBot" , "ch" , Some ( "parent" ) , true ) ;
1512+ let ctx = build_sender_context ( "bot1" , "mybot" , "MyBot" , "ch" , Some ( "parent" ) , true , "2026-05-01T00:00:00Z" ) ;
14531513 assert ! ( ctx. is_bot) ;
14541514 assert_eq ! ( ctx. channel_id, "parent" ) ;
14551515 assert_eq ! ( ctx. thread_id, Some ( "ch" . to_string( ) ) ) ;
0 commit comments