@@ -476,6 +476,98 @@ impl Guest for WhatsAppChannel {
476476
477477 fn on_status ( _update : StatusUpdate ) { }
478478
479+ fn on_message_persisted ( metadata_json : String ) -> Result < ( ) , String > {
480+ channel_host:: log (
481+ channel_host:: LogLevel :: Debug ,
482+ "on_message_persisted callback invoked" ,
483+ ) ;
484+
485+ // Parse metadata to get message_id and phone_number_id
486+ let metadata: WhatsAppMessageMetadata = match serde_json:: from_str ( & metadata_json) {
487+ Ok ( m) => m,
488+ Err ( e) => {
489+ channel_host:: log (
490+ channel_host:: LogLevel :: Warn ,
491+ & format ! ( "Failed to parse metadata in on_message_persisted: {}" , e) ,
492+ ) ;
493+ // Don't fail the ACK - just log and return
494+ return Ok ( ( ) ) ;
495+ }
496+ } ;
497+
498+ // Read api_version from workspace (set during on_start), fallback to default
499+ let api_version = channel_host:: workspace_read ( "channels/whatsapp/api_version" )
500+ . filter ( |s| !s. is_empty ( ) )
501+ . unwrap_or_else ( || "v18.0" . to_string ( ) ) ;
502+
503+ // Build WhatsApp mark_as_read API URL
504+ let url = format ! (
505+ "https://graph.facebook.com/{}/{}/messages" ,
506+ api_version, metadata. phone_number_id
507+ ) ;
508+
509+ // Build mark_as_read payload
510+ let payload = serde_json:: json!( {
511+ "messaging_product" : "whatsapp" ,
512+ "status" : "read" ,
513+ "message_id" : metadata. message_id
514+ } ) ;
515+
516+ let payload_bytes = serde_json:: to_vec ( & payload)
517+ . map_err ( |e| format ! ( "Failed to serialize mark_as_read payload: {}" , e) ) ?;
518+
519+ // Headers with Bearer token placeholder
520+ // Host will inject the actual access token
521+ let headers = serde_json:: json!( {
522+ "Content-Type" : "application/json" ,
523+ "Authorization" : "Bearer {WHATSAPP_ACCESS_TOKEN}"
524+ } ) ;
525+
526+ channel_host:: log (
527+ channel_host:: LogLevel :: Debug ,
528+ & format ! ( "Calling mark_as_read for message: {}" , metadata. message_id) ,
529+ ) ;
530+
531+ let result = channel_host:: http_request (
532+ "POST" ,
533+ & url,
534+ & headers. to_string ( ) ,
535+ Some ( & payload_bytes) ,
536+ None ,
537+ ) ;
538+
539+ match result {
540+ Ok ( http_response) => {
541+ if http_response. status >= 200 && http_response. status < 300 {
542+ channel_host:: log (
543+ channel_host:: LogLevel :: Debug ,
544+ & format ! ( "Marked message {} as read" , metadata. message_id) ,
545+ ) ;
546+ Ok ( ( ) )
547+ } else {
548+ let body_str = String :: from_utf8_lossy ( & http_response. body ) ;
549+ channel_host:: log (
550+ channel_host:: LogLevel :: Warn ,
551+ & format ! (
552+ "mark_as_read API error: {} - {}" ,
553+ http_response. status, body_str
554+ ) ,
555+ ) ;
556+ // Don't fail the ACK - mark_as_read is best-effort
557+ Ok ( ( ) )
558+ }
559+ }
560+ Err ( e) => {
561+ channel_host:: log (
562+ channel_host:: LogLevel :: Warn ,
563+ & format ! ( "mark_as_read HTTP request failed: {}" , e) ,
564+ ) ;
565+ // Don't fail the ACK - mark_as_read is best-effort
566+ Ok ( ( ) )
567+ }
568+ }
569+ }
570+
479571 fn on_shutdown ( ) {
480572 channel_host:: log (
481573 channel_host:: LogLevel :: Info ,
@@ -652,6 +744,11 @@ fn handle_message(
652744 return ;
653745 }
654746
747+ // Note: mark_as_read is now handled by the host after DB persistence.
748+ // The host will call the WhatsApp API directly after receiving the ACK
749+ // from the agent loop. This ensures the webhook returns 200 OK only after
750+ // the message is durably stored.
751+
655752 // Build metadata for response routing
656753 // This is critical - the response handler uses this to know where to send
657754 let metadata = WhatsAppMessageMetadata {
@@ -681,10 +778,6 @@ fn handle_message(
681778 ) ;
682779}
683780
684- // ============================================================================
685- // Utilities
686- // ============================================================================
687-
688781// ============================================================================
689782// Permission & Pairing
690783// ============================================================================
0 commit comments