Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ LOG_LEVEL=info # error | warn | info | debug
# (docker-compose.dev.yml: `${AUTO_START_SESSIONS:-true}`), which is its intended convenience —
# uncommenting the line here turns that off again.
# AUTO_START_SESSIONS=false
# Fetch active statuses that predate a connection as soon as a session reaches READY. Disabled by
# default because the eager status@broadcast read can make WhatsApp revoke some freshly paired
# companions at their first scheduled Web reload. Live status events are unaffected.
STATUS_SEED_ON_READY=false
# 0 = unlimited. Set a positive integer to cap concurrently running/initializing sessions.
# A FAILED session is evicted by design (no live engine), so it does not hold a concurrency slot.
MAX_CONCURRENT_SESSIONS=0
Expand Down
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ services:
# "true", so a blank forward (nothing set) keeps the default off. Without this line the value in
# .env never reaches the container and the flag silently does nothing.
- AUTO_START_SESSIONS=${AUTO_START_SESSIONS:-}
# Fetching status@broadcast immediately after a fresh pairing can make affected accounts lose
# the companion at WhatsApp Web's first scheduled reload. Disabled by default; live statuses
# still arrive normally. Opt in only after validating the account.
- STATUS_SEED_ON_READY=${STATUS_SEED_ON_READY:-false}
# Storage. Blank-forwarded (see Database note) so a dashboard local↔S3 switch applies; a real
# host value pins. Credentials use the canonical S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY names the
# app and dashboard write; the legacy S3_ACCESS_KEY / S3_SECRET_KEY are ALSO forwarded (and read
Expand Down
20 changes: 20 additions & 0 deletions docs/12-troubleshooting-faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,26 @@ is nothing to SIGKILL). A browser left wedged by an earlier teardown is reaped a
next `start()` (an orphan sweep keyed on the session's browser marker runs at every engine launch),
so the stop → start sequence alone is sufficient once the engine is gone.

### Issue: Freshly paired session logs out at the first five-minute reload

> **Engine:** This issue applies to the `whatsapp-web.js` engine only.

**Symptoms:** The session reaches `ready` normally, then at almost exactly five minutes WhatsApp Web
reloads, briefly returns to `CONNECTED`/`hasSynced`, and navigates to
`?post_logout=1&logout_reason=0`. The phone silently removes the companion from Linked devices and
OpenWA returns to a fresh QR because `whatsapp-web.js` deletes LocalAuth credentials on `LOGOUT`.

**Cause:** OpenWA used to backfill active WhatsApp Status posts immediately in its `ready` callback by
fetching `status@broadcast`. Some accounts tolerate that request, but affected accounts have the new
companion revoked at WhatsApp Web's first scheduled reload. A minimal `whatsapp-web.js` client using
the same container, browser, account and Web build remains linked when it does not perform this eager
fetch.

**Fix:** Status backfill-on-ready is disabled by default. Keep `STATUS_SEED_ON_READY=false` for
affected accounts. Live status events still work; only the one-time history backfill of statuses that
predate the connection is skipped. Operators who have tested their accounts and need the backfill can
opt in with `STATUS_SEED_ON_READY=true`.

### Issue: Session stuck at `action_required` ("What's new" onboarding modal)

> **Engine:** This issue applies to the `whatsapp-web.js` engine only (Chromium/Puppeteer-based). It does not affect `ENGINE_TYPE=baileys`.
Expand Down
17 changes: 16 additions & 1 deletion src/modules/session/session-engine-lifecycle.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ import { SessionEngineLeafEvents } from './session-engine-leaf-events';
import { SessionEngineEventWiring, SessionEngineWiringHost } from './session-engine-event-wiring';
import { SessionEngineControls } from './session-engine-controls';

/**
* Eager status-history reads are opt-in. On affected freshly paired whatsapp-web.js accounts,
* fetching status@broadcast before WhatsApp Web's first scheduled reload makes WhatsApp revoke the
* companion at that reload. Live status events remain unaffected when this backfill is disabled.
*/
const isStatusSeedOnReadyEnabled = (): boolean =>
['true', '1', 'yes', 'on'].includes((process.env.STATUS_SEED_ON_READY ?? 'false').trim().toLowerCase());

// Message types that carry downloadable media. Any persisted row of these types must have a media
// marker in metadata — never NULL — or the dashboard renders an empty bubble (no placeholder) and the
// by-type stats filter skips the row. Sources that lack the payload (wwjs own-send echo, media-free
Expand Down Expand Up @@ -597,7 +605,14 @@ export class SessionEngineLifecycle {
// Best-effort snapshot of the account's own contacts' currently-active statuses. Live status
// posts arrive through onMessage below; this just backfills what was already up before we
// connected. Not awaited — onReady must not block on it.
void this.seedStatuses(id, engine);
if (isStatusSeedOnReadyEnabled()) {
void this.seedStatuses(id, engine);
} else {
this.logger.debug('Status backfill on session ready is disabled', {
sessionId: id,
action: 'status_seed_on_ready_disabled',
});
}
}

/**
Expand Down
16 changes: 16 additions & 0 deletions src/modules/session/session.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ describe('SessionService', () => {
let mockEngine: Record<string, jest.Mock>;

beforeEach(async () => {
delete process.env.STATUS_SEED_ON_READY;
repository = {
count: jest.fn(),
find: jest.fn(),
Expand Down Expand Up @@ -3473,7 +3474,18 @@ describe('SessionService', () => {
expect(auth[0][2]).toMatchObject({ sessionId: 'sess-uuid-1', phone: '628123', pushName: 'Alice' });
});

it('does not fetch status history on ready by default', async () => {
const callbacks = await startAndCaptureCallbacks();

callbacks.onReady!('628123', 'Alice');
await flush();

expect(mockEngine.getChatHistory).not.toHaveBeenCalled();
expect(statusStore.ingest).not.toHaveBeenCalled();
});

it('seeds the status store from the status-broadcast chat history when the engine reports ready', async () => {
process.env.STATUS_SEED_ON_READY = 'true';
const callbacks = await startAndCaptureCallbacks();
const nowSec = Math.floor(Date.now() / 1000);
mockEngine.getChatHistory.mockResolvedValue([
Expand Down Expand Up @@ -3569,6 +3581,7 @@ describe('SessionService', () => {
});

it('seeds status media downloaded with the history so it renders like a live post', async () => {
process.env.STATUS_SEED_ON_READY = 'true';
const callbacks = await startAndCaptureCallbacks();
const media = { mimetype: 'image/png', data: 'QUJD' };
mockEngine.getChatHistory.mockResolvedValue([
Expand Down Expand Up @@ -3596,6 +3609,7 @@ describe('SessionService', () => {
});

it('skips the account’s own (fromMe) statuses and statuses older than 24h when seeding', async () => {
process.env.STATUS_SEED_ON_READY = 'true';
const callbacks = await startAndCaptureCallbacks();
const nowSec = Math.floor(Date.now() / 1000);
mockEngine.getChatHistory.mockResolvedValue([
Expand Down Expand Up @@ -3630,6 +3644,7 @@ describe('SessionService', () => {
});

it('keeps seeding the remaining statuses when one item’s ingest fails', async () => {
process.env.STATUS_SEED_ON_READY = 'true';
const callbacks = await startAndCaptureCallbacks();
const nowSec = Math.floor(Date.now() / 1000);
const seedItem = (id: string, author: string) =>
Expand Down Expand Up @@ -3663,6 +3678,7 @@ describe('SessionService', () => {
});

it('swallows a status-history failure on ready (e.g. Baileys has no status chat) without throwing', async () => {
process.env.STATUS_SEED_ON_READY = 'true';
const callbacks = await startAndCaptureCallbacks();
mockEngine.getChatHistory.mockRejectedValue(new Error('not supported'));

Expand Down