From 2293a0275ad1525d903758762e0cc4b094be4340 Mon Sep 17 00:00:00 2001 From: rever-tecnologia Date: Mon, 15 Dec 2025 09:44:03 -0300 Subject: [PATCH] fix(chat): melhora confiabilidade da deteccao de novas mensagens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implementa deteccao dual: timestamp (lastActivityAt) + contador - Adiciona persistencia de estado em ~/.local/share/Raven/chat-state.json - Corrige race condition no servidor com refetch antes do patch - Adiciona campo lastAgentMessageAt no schema do Convex - Adiciona logs de diagnostico detalhados 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/desktop/src-tauri/Cargo.lock | 113 +++++++++++++++- apps/desktop/src-tauri/Cargo.toml | 1 + apps/desktop/src-tauri/src/chat.rs | 210 ++++++++++++++++++++++++++--- convex/schema.ts | 1 + convex/tickets.ts | 15 ++- 5 files changed, 310 insertions(+), 30 deletions(-) diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index a3a293a..f5d4b76 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -63,6 +63,7 @@ dependencies = [ "base64 0.22.1", "chrono", "convex", + "dirs 5.0.1", "futures-util", "get_if_addrs", "hostname", @@ -938,13 +939,34 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", ] [[package]] @@ -955,7 +977,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.2", "windows-sys 0.61.2", ] @@ -3629,6 +3651,17 @@ dependencies = [ "bitflags 2.9.4", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -4516,7 +4549,7 @@ dependencies = [ "anyhow", "bytes", "cookie", - "dirs", + "dirs 6.0.0", "dunce", "embed_plist", "getrandom 0.3.3", @@ -4566,7 +4599,7 @@ checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08" dependencies = [ "anyhow", "cargo_toml", - "dirs", + "dirs 6.0.0", "glob", "heck 0.5.0", "json-patch", @@ -4788,7 +4821,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b" dependencies = [ "base64 0.22.1", - "dirs", + "dirs 6.0.0", "flate2", "futures-util", "http", @@ -5324,7 +5357,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0d92153331e7d02ec09137538996a7786fe679c629c279e82a6be762b7e6fe2" dependencies = [ "crossbeam-channel", - "dirs", + "dirs 6.0.0", "libappindicator", "muda", "objc2 0.6.3", @@ -6105,6 +6138,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -6156,6 +6198,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -6213,6 +6270,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -6231,6 +6294,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -6249,6 +6318,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -6279,6 +6354,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -6297,6 +6378,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -6315,6 +6402,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -6333,6 +6426,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -6395,7 +6494,7 @@ dependencies = [ "block2 0.6.2", "cookie", "crossbeam-channel", - "dirs", + "dirs 6.0.0", "dpi", "dunce", "gdkx11", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 944e0d3..8e26952 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -43,6 +43,7 @@ base64 = "0.22" sha2 = "0.10" convex = "0.10.2" uuid = { version = "1", features = ["v4"] } +dirs = "5" # SSE usa reqwest com stream, nao precisa de websocket [target.'cfg(windows)'.dependencies] diff --git a/apps/desktop/src-tauri/src/chat.rs b/apps/desktop/src-tauri/src/chat.rs index d2e52f3..691ac99 100644 --- a/apps/desktop/src-tauri/src/chat.rs +++ b/apps/desktop/src-tauri/src/chat.rs @@ -11,9 +11,11 @@ use parking_lot::Mutex; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; +use std::fs; +use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; -use std::time::{Duration, Instant}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use tauri::async_runtime::JoinHandle; use tauri::{Emitter, Manager, WebviewWindowBuilder, WebviewUrl}; use tauri_plugin_notification::NotificationExt; @@ -100,6 +102,77 @@ pub struct SessionStartedEvent { pub session: ChatSession, } +// ============================================================================ +// PERSISTENCIA DE ESTADO +// ============================================================================ + +/// Estado persistido do chat para sobreviver a restarts +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ChatPersistedState { + last_unread_count: u32, + sessions: Vec, + saved_at: u64, // Unix timestamp em ms +} + +const STATE_FILE_NAME: &str = "chat-state.json"; +const STATE_MAX_AGE_MS: u64 = 3600_000; // 1 hora - ignorar estados mais antigos + +fn get_state_file_path() -> Option { + dirs::data_local_dir().map(|p| p.join("Raven").join(STATE_FILE_NAME)) +} + +fn save_chat_state(last_unread: u32, sessions: &[ChatSession]) { + let Some(path) = get_state_file_path() else { + return; + }; + + // Criar diretorio se nao existir + if let Some(parent) = path.parent() { + let _ = fs::create_dir_all(parent); + } + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0); + + let state = ChatPersistedState { + last_unread_count: last_unread, + sessions: sessions.to_vec(), + saved_at: now, + }; + + if let Ok(json) = serde_json::to_string_pretty(&state) { + let _ = fs::write(&path, json); + crate::log_info!("[CHAT] Estado persistido: unread={}, sessions={}", last_unread, sessions.len()); + } +} + +fn load_chat_state() -> Option { + let path = get_state_file_path()?; + + let json = fs::read_to_string(&path).ok()?; + let state: ChatPersistedState = serde_json::from_str(&json).ok()?; + + // Verificar se estado nao esta muito antigo + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0); + + if now.saturating_sub(state.saved_at) > STATE_MAX_AGE_MS { + crate::log_info!("[CHAT] Estado persistido ignorado (muito antigo)"); + return None; + } + + crate::log_info!( + "[CHAT] Estado restaurado: unread={}, sessions={}", + state.last_unread_count, state.sessions.len() + ); + Some(state) +} + // ============================================================================ // HTTP CLIENT // ============================================================================ @@ -462,10 +535,16 @@ pub struct ChatRuntime { impl ChatRuntime { pub fn new() -> Self { + // Tentar restaurar estado persistido + let (sessions, unread) = match load_chat_state() { + Some(state) => (state.sessions, state.last_unread_count), + None => (Vec::new(), 0), + }; + Self { inner: Arc::new(Mutex::new(None)), - last_sessions: Arc::new(Mutex::new(Vec::new())), - last_unread_count: Arc::new(Mutex::new(0)), + last_sessions: Arc::new(Mutex::new(sessions)), + last_unread_count: Arc::new(Mutex::new(unread)), is_connected: Arc::new(AtomicBool::new(false)), } } @@ -510,7 +589,9 @@ impl ChatRuntime { let is_connected = self.is_connected.clone(); let join_handle = tauri::async_runtime::spawn(async move { - crate::log_info!("Chat iniciando (Convex realtime + fallback por polling)"); + crate::log_info!("[CHAT DEBUG] Iniciando sistema de chat"); + crate::log_info!("[CHAT DEBUG] Convex URL: {}", convex_clone); + crate::log_info!("[CHAT DEBUG] API Base URL: {}", base_clone); let mut backoff_ms: u64 = 1_000; let max_backoff_ms: u64 = 30_000; @@ -522,12 +603,16 @@ impl ChatRuntime { break; } + crate::log_info!("[CHAT DEBUG] Tentando conectar ao Convex..."); let client_result = ConvexClient::new(&convex_clone).await; let mut client = match client_result { - Ok(c) => c, + Ok(c) => { + crate::log_info!("[CHAT DEBUG] Cliente Convex criado com sucesso"); + c + } Err(err) => { is_connected.store(false, Ordering::Relaxed); - crate::log_warn!("Falha ao criar cliente Convex: {err:?}"); + crate::log_warn!("[CHAT DEBUG] FALHA ao criar cliente Convex: {err:?}"); if last_poll.elapsed() >= poll_interval { poll_and_process_chat_update( @@ -550,16 +635,18 @@ impl ChatRuntime { let mut args = BTreeMap::new(); args.insert("machineToken".to_string(), token_clone.clone().into()); + crate::log_info!("[CHAT DEBUG] Assinando liveChat:checkMachineUpdates..."); let subscribe_result = client.subscribe("liveChat:checkMachineUpdates", args).await; let mut subscription = match subscribe_result { Ok(sub) => { is_connected.store(true, Ordering::Relaxed); backoff_ms = 1_000; + crate::log_info!("[CHAT DEBUG] CONECTADO ao Convex WebSocket com sucesso!"); sub } Err(err) => { is_connected.store(false, Ordering::Relaxed); - crate::log_warn!("Falha ao assinar liveChat:checkMachineUpdates: {err:?}"); + crate::log_warn!("[CHAT DEBUG] FALHA ao assinar checkMachineUpdates: {err:?}"); if last_poll.elapsed() >= poll_interval { poll_and_process_chat_update( @@ -579,8 +666,12 @@ impl ChatRuntime { } }; + crate::log_info!("[CHAT DEBUG] Entrando no loop de escuta WebSocket..."); + let mut update_count: u64 = 0; while let Some(next) = subscription.next().await { + update_count += 1; if stop_clone.load(Ordering::Relaxed) { + crate::log_info!("[CHAT DEBUG] Stop flag detectado, saindo do loop"); break; } match next { @@ -601,6 +692,11 @@ impl ChatRuntime { }) .unwrap_or(0); + crate::log_info!( + "[CHAT DEBUG] UPDATE #{} recebido via WebSocket: hasActive={}, totalUnread={}", + update_count, has_active, total_unread + ); + process_chat_update( &base_clone, &token_clone, @@ -613,13 +709,13 @@ impl ChatRuntime { .await; } FunctionResult::ConvexError(err) => { - crate::log_warn!("Convex error em checkMachineUpdates: {err:?}"); + crate::log_warn!("[CHAT DEBUG] Convex error em checkMachineUpdates: {err:?}"); } FunctionResult::ErrorMessage(msg) => { - crate::log_warn!("Erro em checkMachineUpdates: {msg}"); + crate::log_warn!("[CHAT DEBUG] Erro em checkMachineUpdates: {msg}"); } FunctionResult::Value(other) => { - crate::log_warn!("Payload inesperado em checkMachineUpdates: {other:?}"); + crate::log_warn!("[CHAT DEBUG] Payload inesperado em checkMachineUpdates: {other:?}"); } } } @@ -627,10 +723,11 @@ impl ChatRuntime { is_connected.store(false, Ordering::Relaxed); if stop_clone.load(Ordering::Relaxed) { + crate::log_info!("[CHAT DEBUG] Stop flag detectado apos loop"); break; } - crate::log_warn!("Chat realtime desconectado; aplicando fallback e tentando reconectar"); + crate::log_warn!("[CHAT DEBUG] WebSocket DESCONECTADO! Aplicando fallback e tentando reconectar..."); if last_poll.elapsed() >= poll_interval { poll_and_process_chat_update( &base_clone, @@ -684,8 +781,13 @@ async fn poll_and_process_chat_update( last_sessions: &Arc>>, last_unread_count: &Arc>, ) { + crate::log_info!("[CHAT DEBUG] Executando fallback HTTP polling..."); match poll_chat_updates(base_url, token, None).await { Ok(result) => { + crate::log_info!( + "[CHAT DEBUG] Polling OK: hasActive={}, totalUnread={}", + result.has_active_sessions, result.total_unread + ); process_chat_update( base_url, token, @@ -698,7 +800,7 @@ async fn poll_and_process_chat_update( .await; } Err(err) => { - crate::log_warn!("Chat fallback poll falhou: {err}"); + crate::log_warn!("[CHAT DEBUG] Fallback poll FALHOU: {err}"); } } } @@ -712,10 +814,18 @@ async fn process_chat_update( has_active_sessions: bool, total_unread: u32, ) { + crate::log_info!( + "[CHAT DEBUG] process_chat_update: hasActive={}, totalUnread={}", + has_active_sessions, total_unread + ); + // Buscar sessoes completas para ter dados corretos let mut current_sessions = if has_active_sessions { - fetch_sessions(base_url, token).await.unwrap_or_default() + let sessions = fetch_sessions(base_url, token).await.unwrap_or_default(); + crate::log_info!("[CHAT DEBUG] Buscou {} sessoes ativas", sessions.len()); + sessions } else { + crate::log_info!("[CHAT DEBUG] Sem sessoes ativas"); Vec::new() }; @@ -776,14 +886,58 @@ async fn process_chat_update( } } - // Atualizar cache de sessoes - *last_sessions.lock() = current_sessions.clone(); + // ========================================================================= + // DETECCAO ROBUSTA DE NOVAS MENSAGENS + // Usa DUAS estrategias: timestamp E contador (belt and suspenders) + // ========================================================================= - // Verificar mensagens nao lidas let prev_unread = *last_unread_count.lock(); - let new_messages = total_unread > prev_unread; + + // Estrategia 1: Detectar por lastActivityAt de cada sessao + // Se alguma sessao teve atividade mais recente E tem mensagens nao lidas -> nova mensagem + let mut detected_by_activity = false; + let mut activity_details = String::new(); + + for session in ¤t_sessions { + let prev_activity = prev_sessions + .iter() + .find(|s| s.session_id == session.session_id) + .map(|s| s.last_activity_at) + .unwrap_or(0); + + // Se lastActivityAt aumentou E ha mensagens nao lidas -> nova mensagem do agente + if session.last_activity_at > prev_activity && session.unread_count > 0 { + detected_by_activity = true; + activity_details = format!( + "sessao={} activity: {} -> {} unread={}", + session.ticket_id, prev_activity, session.last_activity_at, session.unread_count + ); + break; + } + } + + // Estrategia 2: Fallback por contador total (metodo original) + let detected_by_count = total_unread > prev_unread; + + // Nova mensagem se QUALQUER estrategia detectar + let new_messages = detected_by_activity || detected_by_count; + + // Log detalhado para diagnostico + crate::log_info!( + "[CHAT] Deteccao: by_activity={} by_count={} (prev={} curr={}) resultado={}", + detected_by_activity, detected_by_count, prev_unread, total_unread, new_messages + ); + if detected_by_activity { + crate::log_info!("[CHAT] Detectado por atividade: {}", activity_details); + } + + // Atualizar caches APOS deteccao (importante: manter ordem) + *last_sessions.lock() = current_sessions.clone(); *last_unread_count.lock() = total_unread; + // Persistir estado para sobreviver a restarts + save_chat_state(total_unread, ¤t_sessions); + // Sempre emitir unread-update let _ = app.emit( "raven://chat/unread-update", @@ -795,9 +949,17 @@ async fn process_chat_update( // Notificar novas mensagens - mostrar chat minimizado com badge if new_messages && total_unread > 0 { - let new_count = total_unread - prev_unread; + let new_count = if total_unread > prev_unread { + total_unread - prev_unread + } else { + 1 // Se detectou por activity mas contador nao mudou, assumir 1 nova + }; - crate::log_info!("Chat: {} novas mensagens (total={})", new_count, total_unread); + crate::log_info!( + "[CHAT] NOVAS MENSAGENS! count={}, total={}, metodo={}", + new_count, total_unread, + if detected_by_activity { "activity" } else { "count" } + ); let _ = app.emit( "raven://chat/new-message", @@ -885,6 +1047,16 @@ async fn process_chat_update( .title(notification_title) .body(¬ification_body) .show(); + } else { + // Log para debug quando NAO ha novas mensagens + if total_unread == 0 { + crate::log_info!("[CHAT DEBUG] Sem mensagens nao lidas (total=0)"); + } else if !new_messages { + crate::log_info!( + "[CHAT DEBUG] Sem novas mensagens (prev={} >= total={})", + prev_unread, total_unread + ); + } } } diff --git a/convex/schema.ts b/convex/schema.ts index 3402484..a327224 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -478,6 +478,7 @@ export default defineSchema({ startedAt: v.number(), endedAt: v.optional(v.number()), lastActivityAt: v.number(), + lastAgentMessageAt: v.optional(v.number()), // Timestamp da ultima mensagem do agente (para deteccao confiavel) unreadByMachine: v.optional(v.number()), unreadByAgent: v.optional(v.number()), }) diff --git a/convex/tickets.ts b/convex/tickets.ts index e2c5602..2a7af9f 100644 --- a/convex/tickets.ts +++ b/convex/tickets.ts @@ -3734,6 +3734,8 @@ export const postChatMessage = mutation({ await ctx.db.patch(ticketId, { updatedAt: now }) // Se o autor for um agente (ADMIN, MANAGER, AGENT), incrementar unreadByMachine na sessao de chat ativa + // IMPORTANTE: Buscar sessao IMEDIATAMENTE antes do patch para evitar race conditions + // O Convex faz retry automatico em caso de OCC conflict const actorRole = participant.role?.toUpperCase() ?? "" if (["ADMIN", "MANAGER", "AGENT"].includes(actorRole)) { const activeSession = await ctx.db @@ -3743,10 +3745,15 @@ export const postChatMessage = mutation({ .first() if (activeSession) { - await ctx.db.patch(activeSession._id, { - unreadByMachine: (activeSession.unreadByMachine ?? 0) + 1, - lastActivityAt: now, - }) + // Refetch para garantir valor mais recente (OCC protection) + const freshSession = await ctx.db.get(activeSession._id) + if (freshSession) { + await ctx.db.patch(activeSession._id, { + unreadByMachine: (freshSession.unreadByMachine ?? 0) + 1, + lastActivityAt: now, + lastAgentMessageAt: now, // Novo: timestamp da ultima mensagem do agente + }) + } } }