---
CONTRIBUTING.md | 2 +-
README.md | 6 +++---
evolution-manager-v2 | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 595600cc05..8aa5df1c7f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -12,7 +12,7 @@ Harassment, discrimination, or abusive behavior will not be tolerated.
### Reporting Bugs
-1. Check existing [issues](https://github.com/EvolutionAPI/evolution-api/issues)
+1. Check existing [issues](https://github.com/evolution-foundation/evolution-api/issues)
to avoid duplicates
2. Open a new issue with:
- Clear, descriptive title
diff --git a/README.md b/README.md
index 98218c5f8a..4586e4028a 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -37,7 +37,7 @@ Evolution API began as a WhatsApp controller API based on [CodeChat](https://git
## Part of the Evolution Foundation ecosystem
-Evolution API is one of the messaging engines maintained by Evolution Foundation. It is used as a WhatsApp provider by the [Evo CRM Community](https://github.com/EvolutionAPI/evo-crm-community) and other projects in the ecosystem.
+Evolution API is one of the messaging engines maintained by Evolution Foundation. It is used as a WhatsApp provider by the [Evo CRM Community](https://github.com/evolution-foundation/evo-crm-community) and other projects in the ecosystem.
---
@@ -80,7 +80,7 @@ Evolution API integrates natively with many platforms:
### Installation
```bash
-git clone git@github.com:EvolutionAPI/evolution-api.git
+git clone git@github.com:evolution-foundation/evolution-api.git
cd evolution-api
# Install dependencies
diff --git a/evolution-manager-v2 b/evolution-manager-v2
index f054b9bc28..3137df4695 160000
--- a/evolution-manager-v2
+++ b/evolution-manager-v2
@@ -1 +1 @@
-Subproject commit f054b9bc28083152d4948f835e3346fd0add39db
+Subproject commit 3137df469504ce211c68e7b35f0706497ac1b95f
From fa09d37892cdbb1d65a250155d293d92230c5b30 Mon Sep 17 00:00:00 2001
From: Davidson Gomes
Date: Wed, 6 May 2026 14:38:27 -0300
Subject: [PATCH 4/5] docs(org): update nested submodule URLs from EvolutionAPI
to evolution-foundation
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.gitmodules | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.gitmodules b/.gitmodules
index ef5b3e63cf..54a177465b 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
[submodule "evolution-manager-v2"]
path = evolution-manager-v2
- url = https://github.com/EvolutionAPI/evolution-manager-v2.git
+ url = https://github.com/evolution-foundation/evolution-manager-v2.git
From c043dd0f8d4f06936f573696a00bff53c4fd8697 Mon Sep 17 00:00:00 2001
From: Bruno Eduardo Santos
Date: Fri, 24 Jul 2026 08:58:12 -0300
Subject: [PATCH 5/5] fix: notifica falhas silenciosas de QR no Chatwoot e
reconcilia estado zumbi
Dois problemas distintos, mas relacionados:
1) QR solicitado pelo Chatwoot as vezes simplesmente nao aparecia na
conversa - mesmo com a instancia genuinamente desconectada, nao so
em cenario de estado "zumbi". Causa: falhas engolidas em silencio.
receiveWebhook (comando 'init') e o handler de qrcode.updated
estavam em try/catch que so logava no servidor, sem nenhum feedback
ao operador no Chatwoot. Havia tambem um bug de optional chaining
(body?.qrcode.base64) que podia lancar TypeError silencioso se
body.qrcode viesse undefined.
2) Instancia aparecia "conectada" na Evolution mesmo com o WhatsApp
real caido. Causa: o status de conexao e 100% orientado a evento
(connection.update); se esse evento nunca dispara (ex. socket
morrendo sem RST/FIN, comum sob CPU steal alto), o valor cacheado
em memoria/banco fica preso em 'open' para sempre. Nao havia
nenhuma sonda ativa (confirmado: zero ocorrencias de setInterval em
todo o src/).
Mudancas:
- receiveWebhook: erro ao conectar agora notifica a conversa
(cw.inbox.qrError); catch geral tambem notifica erro generico
(cw.inbox.requestError) alem de logar.
- Corrigido o optional chaining (body?.qrcode?.base64) com guarda
explicita e notificacao de erro.
- Handler de qrcode.updated: catch notifica cw.inbox.qrError, mas so
quando o QR ainda nao tinha sido postado com sucesso (flag
qrPosted) - evita notificar erro depois que a imagem do QR ja
chegou na conversa.
- monitor.service.ts: health check periodico (30s) que confirma via
client.ws.isOpen se instancias marcadas 'open' realmente tem socket
ativo; se nao, reconcilia o estado para 'close' (banco + webhook
CONNECTION_UPDATE), reaproveitando os mesmos campos ja usados no
fechamento real da conexao.
- instance.controller.ts: connectionState reconcilia sob demanda antes
de responder, alem do health check periodico.
- Novas chaves de traducao cw.inbox.qrError/cw.inbox.requestError em
pt-BR/en/es.
---
src/api/controllers/instance.controller.ts | 9 ++
.../chatwoot/services/chatwoot.service.ts | 40 ++++++++-
src/api/services/monitor.service.ts | 90 ++++++++++++++++++-
src/utils/translations/en.json | 2 +
src/utils/translations/es.json | 2 +
src/utils/translations/pt-BR.json | 2 +
6 files changed, 142 insertions(+), 3 deletions(-)
diff --git a/src/api/controllers/instance.controller.ts b/src/api/controllers/instance.controller.ts
index 6a69106881..a79ae21e61 100644
--- a/src/api/controllers/instance.controller.ts
+++ b/src/api/controllers/instance.controller.ts
@@ -391,6 +391,15 @@ export class InstanceController {
}
public async connectionState({ instanceName }: InstanceDto) {
+ // Cross-check the cached state against the real WebSocket before answering, so a
+ // "zombie open" instance (state cached as 'open' after the socket already dropped)
+ // is reconciled here too, not only on the next periodic health check.
+ try {
+ await this.waMonitor.reconcileInstanceConnection(instanceName);
+ } catch (error) {
+ this.logger.error(error);
+ }
+
return {
instance: {
instanceName: instanceName,
diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
index 906fff1881..408785724f 100644
--- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
+++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
@@ -1381,7 +1381,12 @@ export class ChatwootService {
if (state !== 'open') {
const number = command.split(':')[1];
- await waInstance.connectToWhatsapp(number);
+ try {
+ await waInstance.connectToWhatsapp(number);
+ } catch (connectError) {
+ this.logger.error(connectError);
+ await this.createBotMessage(instance, i18next.t('cw.inbox.qrError'), 'incoming');
+ }
} else {
await this.createBotMessage(
instance,
@@ -1601,6 +1606,13 @@ export class ChatwootService {
return { message: 'bot' };
} catch (error) {
this.logger.error(error);
+ this.logger.error(error?.stack ?? error);
+
+ try {
+ await this.createBotMessage(instance, i18next.t('cw.inbox.requestError'), 'incoming');
+ } catch (notifyError) {
+ this.logger.error(notifyError);
+ }
return { message: 'bot' };
}
@@ -1948,6 +1960,10 @@ export class ChatwootService {
}
public async eventWhatsapp(event: string, instance: InstanceDto, body: any) {
+ // Tracks whether createBotQr already succeeded, so a later failure in this same
+ // handler (e.g. posting the accompanying text message) doesn't also send a
+ // confusing "QR failed" notification into a conversation that just received the QR.
+ let qrPosted = false;
try {
const waInstance = this.waMonitor.waInstances[instance.instanceName];
@@ -2493,7 +2509,17 @@ export class ChatwootService {
const erroQRcode = `🚨 ${i18next.t('qrlimitreached')}`;
return await this.createBotMessage(instance, erroQRcode, 'incoming');
} else {
- const fileData = Buffer.from(body?.qrcode.base64.replace('data:image/png;base64,', ''), 'base64');
+ const qrBase64 = body?.qrcode?.base64;
+
+ if (!qrBase64) {
+ this.logger.error(
+ `qrcode.updated event received without a valid qrcode.base64 payload for instance ${instance?.instanceName}`,
+ );
+ await this.createBotMessage(instance, i18next.t('cw.inbox.qrError'), 'incoming');
+ return;
+ }
+
+ const fileData = Buffer.from(qrBase64.replace('data:image/png;base64,', ''), 'base64');
const fileStream = new Readable();
fileStream._read = () => {};
@@ -2507,6 +2533,7 @@ export class ChatwootService {
fileStream,
`${instance.instanceName}.png`,
);
+ qrPosted = true;
let msgQrCode = `⚡️${i18next.t('qrgeneratedsuccesfully')}\n\n${i18next.t('scanqr')}`;
@@ -2524,6 +2551,15 @@ export class ChatwootService {
}
} catch (error) {
this.logger.error(error);
+ this.logger.error(error?.stack ?? error);
+
+ if (event === 'qrcode.updated' && !qrPosted) {
+ try {
+ await this.createBotMessage(instance, i18next.t('cw.inbox.qrError'), 'incoming');
+ } catch (notifyError) {
+ this.logger.error(notifyError);
+ }
+ }
}
}
diff --git a/src/api/services/monitor.service.ts b/src/api/services/monitor.service.ts
index 438530b57e..23b92c1448 100644
--- a/src/api/services/monitor.service.ts
+++ b/src/api/services/monitor.service.ts
@@ -2,7 +2,7 @@ import { InstanceDto } from '@api/dto/instance.dto';
import { ProviderFiles } from '@api/provider/sessions';
import { PrismaRepository } from '@api/repository/repository.service';
import { channelController } from '@api/server.module';
-import { Events, Integration } from '@api/types/wa.types';
+import { Events, Integration, wa } from '@api/types/wa.types';
import { CacheConf, Chatwoot, ConfigService, Database, DelInstance, ProviderSession } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { INSTANCE_DIR, STORE_DIR } from '@config/path.config';
@@ -26,6 +26,7 @@ export class WAMonitoringService {
) {
this.removeInstance();
this.noConnection();
+ this.startConnectionHealthCheck();
Object.assign(this.db, configService.get('DATABASE'));
Object.assign(this.redis, configService.get('CACHE'));
@@ -42,6 +43,12 @@ export class WAMonitoringService {
private readonly providerSession: ProviderSession;
+ // Health check: how often (ms) we probe instances cached as 'open' to confirm the
+ // underlying WebSocket is really still connected (mitigates the "zombie open" state,
+ // since connectionStatus is otherwise 100% event-driven from connection.update).
+ private readonly HEALTH_CHECK_INTERVAL_MS = 30_000;
+ private healthCheckInterval: NodeJS.Timeout;
+
public delInstanceTime(instance: string) {
const time = this.configService.get('DEL_INSTANCE');
if (typeof time === 'number' && time > 0) {
@@ -83,6 +90,87 @@ export class WAMonitoringService {
}
}
+ /**
+ * Starts the periodic reconciliation of cached connection state ("zombie open" mitigation).
+ * connectionStatus.state is otherwise only ever written from inside the Baileys
+ * 'connection.update' handler, so if that event is ever missed (process hiccup, socket
+ * torn down outside Baileys' own reconnection flow, etc.) an instance can stay marked
+ * 'open' in memory/DB forever even though the WebSocket is gone.
+ */
+ private startConnectionHealthCheck() {
+ this.healthCheckInterval = setInterval(() => {
+ this.checkInstancesConnection();
+ }, this.HEALTH_CHECK_INTERVAL_MS);
+ }
+
+ private async checkInstancesConnection() {
+ for (const instanceName of Object.keys(this.waInstances)) {
+ try {
+ await this.reconcileInstanceConnection(instanceName);
+ } catch (error) {
+ // A failure checking one instance must never abort the loop or affect the others.
+ this.logger.error(`Health check failed for instance "${instanceName}": ${error}`);
+ }
+ }
+ }
+
+ /**
+ * Confirms that an instance cached as 'open' still has a real, open WebSocket
+ * (client.ws.isOpen, per Baileys' AbstractSocketClient). If it does not, forces
+ * stateConnection to 'close', persists it via the same Prisma fields used by
+ * BaileysStartupService.connectionUpdate() on a genuine close, and re-emits
+ * CONNECTION_UPDATE through the instance's own sendDataWebhook (no duplicated logic).
+ *
+ * Used both by the periodic health check above and on-demand by
+ * InstanceController.connectionState() before answering a status request.
+ */
+ public async reconcileInstanceConnection(instanceName: string): Promise {
+ const waInstance = this.waInstances[instanceName];
+
+ if (!waInstance || waInstance.connectionStatus?.state !== 'open') {
+ return waInstance?.connectionStatus;
+ }
+
+ const client = waInstance.client;
+
+ // Only instances with an initialized Baileys client expose ws.isOpen; instances
+ // without one (still connecting, non-Baileys channel, etc.) are left untouched.
+ if (!client?.ws || client.ws.isOpen) {
+ return waInstance.connectionStatus;
+ }
+
+ this.logger.warn(
+ `Instance "${instanceName}" is cached as 'open' but its WebSocket is not open (isOpen=false). Reconciling status to 'close'.`,
+ );
+
+ waInstance.stateConnection = { state: 'close', statusReason: 428 };
+
+ try {
+ await this.prismaRepository.instance.update({
+ where: { id: waInstance.instanceId },
+ data: {
+ connectionStatus: 'close',
+ disconnectionAt: new Date(),
+ disconnectionReasonCode: 428,
+ disconnectionObject: JSON.stringify({ reason: 'health-check: websocket not open' }),
+ },
+ });
+ } catch (error) {
+ this.logger.error(`Failed to persist reconciled connection state for "${instanceName}": ${error}`);
+ }
+
+ try {
+ await waInstance.sendDataWebhook?.(Events.CONNECTION_UPDATE, {
+ instance: instanceName,
+ ...waInstance.stateConnection,
+ });
+ } catch (error) {
+ this.logger.error(`Failed to emit CONNECTION_UPDATE for "${instanceName}": ${error}`);
+ }
+
+ return waInstance.connectionStatus;
+ }
+
public async instanceInfo(instanceNames?: string[]): Promise {
if (instanceNames && instanceNames.length > 0) {
const inexistentInstances = instanceNames ? instanceNames.filter((instance) => !this.waInstances[instance]) : [];
diff --git a/src/utils/translations/en.json b/src/utils/translations/en.json
index 43dfd8d213..0a31dc1b34 100644
--- a/src/utils/translations/en.json
+++ b/src/utils/translations/en.json
@@ -2,6 +2,8 @@
"qrgeneratedsuccesfully": "QRCode successfully generated!",
"scanqr": "Scan this QR code within the next 40 seconds.",
"qrlimitreached": "QRCode generation limit reached, to generate a new QRCode, send the 'init' message again.",
+ "cw.inbox.qrError": "🚨 Failed to generate the QR Code. Please try again by sending 'init' in this conversation.",
+ "cw.inbox.requestError": "⚠️ An error occurred while processing your request. If you were trying to connect via QR Code, please try again by sending 'init'.",
"cw.inbox.connected": "🚀 Connection successfully established!",
"cw.inbox.disconnect": "🚨 Disconnecting WhatsApp from inbox *{{inboxName}}*.",
"cw.inbox.alreadyConnected": "🚨 {{inboxName}} instance is connected.",
diff --git a/src/utils/translations/es.json b/src/utils/translations/es.json
index be830885ed..1bf0f5a824 100644
--- a/src/utils/translations/es.json
+++ b/src/utils/translations/es.json
@@ -2,6 +2,8 @@
"qrgeneratedsuccesfully": "Código QR generado exitosamente!",
"scanqr": "Escanea este código QR en los próximos 40 segundos.",
"qrlimitreached": "🚨 Se alcanzó el límite de generación de QRCode. Para generar un nuevo QRCode, envíe el mensaje 'init' nuevamente.",
+ "cw.inbox.qrError": "🚨 No se pudo generar el código QR. Por favor, intente nuevamente enviando 'init' en esta conversación.",
+ "cw.inbox.requestError": "⚠️ Ocurrió un error al procesar su solicitud. Si estaba intentando conectar mediante código QR, intente nuevamente enviando 'init'.",
"cw.inbox.connected": "🚀 ¡Conexión establecida exitosamente!",
"cw.inbox.disconnect": "🚨 Instancia *{{inboxName}}* desconectado de Whatsapp.",
"cw.inbox.alreadyConnected": "🚨 La instancia {{inboxName}} está conectada.",
diff --git a/src/utils/translations/pt-BR.json b/src/utils/translations/pt-BR.json
index 31bf468a9b..f55557cf4d 100644
--- a/src/utils/translations/pt-BR.json
+++ b/src/utils/translations/pt-BR.json
@@ -2,6 +2,8 @@
"qrgeneratedsuccesfully": "QRCode gerado com sucesso!",
"scanqr": "Escaneie o QRCode com o WhatsApp nos próximos 40 segundos.",
"qrlimitreached": "Limite de geração de QRCode atingido! Para gerar um novo QRCode, envie o texto 'init' nesta conversa.",
+ "cw.inbox.qrError": "🚨 Falha ao gerar o QR Code. Por favor, tente novamente enviando 'init' nesta conversa.",
+ "cw.inbox.requestError": "⚠️ Ocorreu um erro ao processar sua solicitação. Se você estava tentando conectar via QR Code, tente novamente enviando 'init'.",
"cw.inbox.connected": "🚀 Conectado com sucesso!",
"cw.inbox.disconnect": "🚨 Instância *{{inboxName}}* desconectada do WhatsApp.",
"cw.inbox.alreadyConnected": "🚨 Instância *{{inboxName}}* já está conectada.",