---
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 a2146e9c537289077643f80e38d658e3ed53cea2 Mon Sep 17 00:00:00 2001
From: Bruno Eduardo Santos
Date: Fri, 24 Jul 2026 08:55:15 -0300
Subject: [PATCH 5/5] fix: trata DisconnectReason.connectionReplaced e evita
reconexoes concorrentes
Instancias especificas desconectavam diariamente sem intervencao do
usuario. Causa raiz: DisconnectReason.connectionReplaced (440) nao
estava na lista de codigos que interrompem a reconexao automatica,
entao o socket tentava reconectar com credenciais ja invalidadas pela
troca de sessao, gerando um ciclo close -> reconnect -> close.
Alem disso, connectToWhatsapp/createClient nao tinham nenhum lock
contra chamadas concorrentes: se uma reconexao manual (ex. via
Chatwoot) disparasse enquanto a reconexao automatica do listener de
'close' ja estava em andamento, dois sockets Baileys podiam competir
pela mesma sessao - um dos gatilhos conhecidos do proprio
connectionReplaced.
- Adiciona connectionReplaced a lista de codigos que nao devem
reconectar automaticamente, com log explicito exigindo novo QR.
- Adiciona lock de instancia (isConnecting) em createClient para
impedir sockets concorrentes na mesma sessao.
---
.../whatsapp/whatsapp.baileys.service.ts | 278 ++++++++++--------
1 file changed, 161 insertions(+), 117 deletions(-)
diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
index 60e857fcc1..8d20dc83aa 100644
--- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
+++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
@@ -251,6 +251,11 @@ export class BaileysStartupService extends ChannelStartupService {
private endSession = false;
private logBaileys = this.configService.get('LOG').BAILEYS;
private eventProcessingQueue: Promise = Promise.resolve();
+ // Prevents concurrent connectToWhatsapp/createClient calls from racing on the
+ // same session (e.g. Chatwoot-triggered manual reconnect firing while the
+ // automatic reconnect from a 'close' event is already in flight), which can
+ // itself cause Baileys to emit DisconnectReason.connectionReplaced (440).
+ private isConnecting = false;
// Cache TTL constants (in seconds)
private readonly MESSAGE_CACHE_TTL_SECONDS = 5 * 60; // 5 minutes - avoid duplicate message processing
@@ -425,8 +430,29 @@ export class BaileysStartupService extends ChannelStartupService {
if (connection === 'close') {
const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode;
- const codesToNotReconnect = [DisconnectReason.loggedOut, DisconnectReason.forbidden, 402, 406];
+ // connectionReplaced (440) means the session was taken over elsewhere (another
+ // device/tab, or a concurrent connection attempt on our own side). Auto-reconnecting
+ // here would retry with credentials that the takeover may have already invalidated,
+ // producing a close -> reconnect -> close loop. Treat it like the other terminal
+ // codes: stop, surface a clear signal, and require an explicit new connection/QR scan.
+ const codesToNotReconnect = [
+ DisconnectReason.loggedOut,
+ DisconnectReason.forbidden,
+ DisconnectReason.connectionReplaced,
+ 402,
+ 406,
+ ];
const shouldReconnect = !codesToNotReconnect.includes(statusCode);
+
+ if (statusCode === DisconnectReason.connectionReplaced) {
+ this.logger.warn(
+ `Instance ${this.instance.name}: connection replaced (DisconnectReason.connectionReplaced/440). ` +
+ 'This session was opened elsewhere (another device/tab, or a concurrent connection attempt) ' +
+ 'and will NOT be auto-reconnected with the current credentials. A new QR Code scan (or explicit ' +
+ 'reconnect) will be required.',
+ );
+ }
+
if (shouldReconnect) {
await this.connectToWhatsapp(this.phoneNumber);
} else {
@@ -574,150 +600,168 @@ export class BaileysStartupService extends ChannelStartupService {
}
private async createClient(number?: string): Promise {
- this.instance.authState = await this.defineAuthState();
+ // Guard against concurrent connection attempts on the same instance (e.g. a
+ // manual reconnect from Chatwoot/the API firing while the automatic reconnect
+ // triggered by a 'close' event is still in flight). Two Baileys sockets racing
+ // on the same session is a known trigger for DisconnectReason.connectionReplaced.
+ if (this.isConnecting) {
+ this.logger.warn(
+ `Instance ${this.instance.name}: a connection attempt is already in progress; ` +
+ 'skipping this concurrent createClient call to avoid racing sockets on the same session.',
+ );
+ return this.client;
+ }
- const session = this.configService.get('CONFIG_SESSION_PHONE');
+ this.isConnecting = true;
+ try {
+ this.instance.authState = await this.defineAuthState();
- let browserOptions = {};
+ const session = this.configService.get('CONFIG_SESSION_PHONE');
- if (number || this.phoneNumber) {
- this.phoneNumber = number;
+ let browserOptions = {};
- this.logger.info(`Phone number: ${number}`);
- } else {
- const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()];
- browserOptions = { browser };
+ if (number || this.phoneNumber) {
+ this.phoneNumber = number;
- this.logger.info(`Browser: ${browser}`);
- }
+ this.logger.info(`Phone number: ${number}`);
+ } else {
+ const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()];
+ browserOptions = { browser };
- const baileysVersion = await fetchLatestWaWebVersion({});
- const version = baileysVersion.version;
- const log = `Baileys version: ${version.join('.')}`;
+ this.logger.info(`Browser: ${browser}`);
+ }
- this.logger.info(log);
+ const baileysVersion = await fetchLatestWaWebVersion({});
+ const version = baileysVersion.version;
+ const log = `Baileys version: ${version.join('.')}`;
- this.logger.info(`Group Ignore: ${this.localSettings.groupsIgnore}`);
+ this.logger.info(log);
- let options;
+ this.logger.info(`Group Ignore: ${this.localSettings.groupsIgnore}`);
- if (this.localProxy?.enabled) {
- this.logger.info('Proxy enabled: ' + this.localProxy?.host);
+ let options;
- if (this.localProxy?.host?.includes('proxyscrape')) {
- try {
- const response = await axios.get(this.localProxy?.host);
- const text = response.data;
- const proxyUrls = text.split('\r\n');
- const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length));
- const proxyUrl = 'http://' + proxyUrls[rand];
- options = { agent: makeProxyAgent(proxyUrl), fetchAgent: makeProxyAgentUndici(proxyUrl) };
- } catch {
- this.localProxy.enabled = false;
+ if (this.localProxy?.enabled) {
+ this.logger.info('Proxy enabled: ' + this.localProxy?.host);
+
+ if (this.localProxy?.host?.includes('proxyscrape')) {
+ try {
+ const response = await axios.get(this.localProxy?.host);
+ const text = response.data;
+ const proxyUrls = text.split('\r\n');
+ const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length));
+ const proxyUrl = 'http://' + proxyUrls[rand];
+ options = { agent: makeProxyAgent(proxyUrl), fetchAgent: makeProxyAgentUndici(proxyUrl) };
+ } catch {
+ this.localProxy.enabled = false;
+ }
+ } else {
+ options = {
+ agent: makeProxyAgent({
+ host: this.localProxy.host,
+ port: this.localProxy.port,
+ protocol: this.localProxy.protocol,
+ username: this.localProxy.username,
+ password: this.localProxy.password,
+ }),
+ fetchAgent: makeProxyAgentUndici({
+ host: this.localProxy.host,
+ port: this.localProxy.port,
+ protocol: this.localProxy.protocol,
+ username: this.localProxy.username,
+ password: this.localProxy.password,
+ }),
+ };
}
- } else {
- options = {
- agent: makeProxyAgent({
- host: this.localProxy.host,
- port: this.localProxy.port,
- protocol: this.localProxy.protocol,
- username: this.localProxy.username,
- password: this.localProxy.password,
- }),
- fetchAgent: makeProxyAgentUndici({
- host: this.localProxy.host,
- port: this.localProxy.port,
- protocol: this.localProxy.protocol,
- username: this.localProxy.username,
- password: this.localProxy.password,
- }),
- };
}
- }
- const socketConfig: UserFacingSocketConfig = {
- ...options,
- version,
- logger: P({ level: this.logBaileys }),
- printQRInTerminal: false,
- auth: {
- creds: this.instance.authState.state.creds,
- keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' }) as any),
- },
- msgRetryCounterCache: this.msgRetryCounterCache,
- generateHighQualityLinkPreview: true,
- getMessage: async (key) => (await this.getMessage(key)) as Promise,
- ...browserOptions,
- markOnlineOnConnect: this.localSettings.alwaysOnline,
- retryRequestDelayMs: 350,
- maxMsgRetryCount: 4,
- fireInitQueries: true,
- connectTimeoutMs: 30_000,
- keepAliveIntervalMs: 30_000,
- qrTimeout: 45_000,
- emitOwnEvents: false,
- shouldIgnoreJid: (jid) => {
- if (this.localSettings.syncFullHistory && isJidGroup(jid)) {
- return false;
- }
+ const socketConfig: UserFacingSocketConfig = {
+ ...options,
+ version,
+ logger: P({ level: this.logBaileys }),
+ printQRInTerminal: false,
+ auth: {
+ creds: this.instance.authState.state.creds,
+ keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' }) as any),
+ },
+ msgRetryCounterCache: this.msgRetryCounterCache,
+ generateHighQualityLinkPreview: true,
+ getMessage: async (key) => (await this.getMessage(key)) as Promise,
+ ...browserOptions,
+ markOnlineOnConnect: this.localSettings.alwaysOnline,
+ retryRequestDelayMs: 350,
+ maxMsgRetryCount: 4,
+ fireInitQueries: true,
+ connectTimeoutMs: 30_000,
+ keepAliveIntervalMs: 30_000,
+ qrTimeout: 45_000,
+ emitOwnEvents: false,
+ shouldIgnoreJid: (jid) => {
+ if (this.localSettings.syncFullHistory && isJidGroup(jid)) {
+ return false;
+ }
- const isGroupJid = this.localSettings.groupsIgnore && isJidGroup(jid);
- const isBroadcast = !this.localSettings.readStatus && isJidBroadcast(jid);
- const isNewsletter = isJidNewsletter(jid);
+ const isGroupJid = this.localSettings.groupsIgnore && isJidGroup(jid);
+ const isBroadcast = !this.localSettings.readStatus && isJidBroadcast(jid);
+ const isNewsletter = isJidNewsletter(jid);
- return isGroupJid || isBroadcast || isNewsletter;
- },
- syncFullHistory: this.localSettings.syncFullHistory,
- shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => {
- return this.historySyncNotification(msg);
- },
- cachedGroupMetadata: this.getGroupMetadataCache,
- userDevicesCache: this.userDevicesCache,
- transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 3000 },
- patchMessageBeforeSending(message) {
- if (
- message.deviceSentMessage?.message?.listMessage?.listType === proto.Message.ListMessage.ListType.PRODUCT_LIST
- ) {
- message = JSON.parse(JSON.stringify(message));
+ return isGroupJid || isBroadcast || isNewsletter;
+ },
+ syncFullHistory: this.localSettings.syncFullHistory,
+ shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => {
+ return this.historySyncNotification(msg);
+ },
+ cachedGroupMetadata: this.getGroupMetadataCache,
+ userDevicesCache: this.userDevicesCache,
+ transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 3000 },
+ patchMessageBeforeSending(message) {
+ if (
+ message.deviceSentMessage?.message?.listMessage?.listType ===
+ proto.Message.ListMessage.ListType.PRODUCT_LIST
+ ) {
+ message = JSON.parse(JSON.stringify(message));
- message.deviceSentMessage.message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT;
- }
+ message.deviceSentMessage.message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT;
+ }
- if (message.listMessage?.listType == proto.Message.ListMessage.ListType.PRODUCT_LIST) {
- message = JSON.parse(JSON.stringify(message));
+ if (message.listMessage?.listType == proto.Message.ListMessage.ListType.PRODUCT_LIST) {
+ message = JSON.parse(JSON.stringify(message));
- message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT;
- }
+ message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT;
+ }
- return message;
- },
- };
+ return message;
+ },
+ };
- this.endSession = false;
+ this.endSession = false;
- this.client = makeWASocket(socketConfig);
+ this.client = makeWASocket(socketConfig);
- if (this.localSettings.wavoipToken && this.localSettings.wavoipToken.length > 0) {
- useVoiceCallsBaileys(this.localSettings.wavoipToken, this.client, this.connectionStatus.state as any, true);
- }
+ if (this.localSettings.wavoipToken && this.localSettings.wavoipToken.length > 0) {
+ useVoiceCallsBaileys(this.localSettings.wavoipToken, this.client, this.connectionStatus.state as any, true);
+ }
- this.eventHandler();
+ this.eventHandler();
- this.client.ws.on('CB:call', (packet) => {
- console.log('CB:call', packet);
- const payload = { event: 'CB:call', packet: packet };
- this.sendDataWebhook(Events.CALL, payload, true, ['websocket']);
- });
+ this.client.ws.on('CB:call', (packet) => {
+ console.log('CB:call', packet);
+ const payload = { event: 'CB:call', packet: packet };
+ this.sendDataWebhook(Events.CALL, payload, true, ['websocket']);
+ });
- this.client.ws.on('CB:ack,class:call', (packet) => {
- console.log('CB:ack,class:call', packet);
- const payload = { event: 'CB:ack,class:call', packet: packet };
- this.sendDataWebhook(Events.CALL, payload, true, ['websocket']);
- });
+ this.client.ws.on('CB:ack,class:call', (packet) => {
+ console.log('CB:ack,class:call', packet);
+ const payload = { event: 'CB:ack,class:call', packet: packet };
+ this.sendDataWebhook(Events.CALL, payload, true, ['websocket']);
+ });
- this.phoneNumber = number;
+ this.phoneNumber = number;
- return this.client;
+ return this.client;
+ } finally {
+ this.isConnecting = false;
+ }
}
public async connectToWhatsapp(number?: string): Promise {