whatsapp: fix history sync + session corruption, Baileys 7.0.0-rc14 (v7 / 2.1.0)

Diagnosed from the live server (two users, two accounts, one shared log).

- history sync was gated off entirely on Baileys 6.7.x: the library derives
  shouldSyncHistoryMessage from syncFullHistory when unset, and the connector
  shipped syncFullHistory: false. Now both are passed explicitly, so behaviour
  no longer depends on the resolved Baileys version. browser -> ['Mac OS', ...]
  because PLATFORM_MAP only grants a desktop-grade sync to Mac OS / Windows.
- reconnects leaked their socket, leaving two writers over auth/: no end(), no
  removeAllListeners(). ~15 reconnects/day produced 1422 Bad MAC lines, all on
  the account's own LID device 0. Added teardownSock(), per-socket generation
  guards, and a single-slot reconnect timer with exponential backoff.
- undecryptable messages (stubType CIPHERTEXT) were stored with empty text and
  rendered as blank transcript lines; now labelled and counted. Text-less
  protocol frames are dropped.
- fetchLatestBaileysVersion() ran on every reconnect; now cached for 6h.
- libsignal's direct console.error spam collapsed into one counted line, log
  lines tagged with the linked account, console.log redirected off stdout.

Baileys pinned ^6.7.9 -> 7.0.0-rc14 (the range resolved to 6.7.24 for one user
and 6.17.16 for the other). RC is ESM-only, so index.js is now ESM.

Verified on skald-runtime:v4 (Node 20.19.2): initialize, tools/list, status,
list_chats, version fetch, QR, and logout -> teardown -> single reconnect.

Remaining gap: the store is still in-memory only, so synced history is lost on
the next process restart.
This commit is contained in:
Daniele
2026-08-23 18:15:56 +01:00
parent 040a0b2320
commit a2bbedad40
6 changed files with 270 additions and 87 deletions
+22
View File
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## 2026-08-23
### Fixed
- **whatsapp: history sync silently disabled, and Signal sessions corrupted by reconnects (v7 / 2.1.0)** — diagnosed from the live server, where two users (two separate accounts, separate `auth/` dirs) share one log file. Four distinct defects, plus a dependency upgrade.
- **The connector processed no history at all on Baileys 6.7.x.** `Socket/index.js` derives the history gate from the sync flag when the caller leaves it unset: `if (config.shouldSyncHistoryMessage === undefined) newConfig.shouldSyncHistoryMessage = () => !!newConfig.syncFullHistory;`. The connector passed neither, and shipped `syncFullHistory: false` — so the gate evaluated to `() => false`, `shouldProcessHistoryMsg` was permanently false, and the history-sync blob was discarded. `list_chats` / `get_messages` only ever saw messages arriving live after startup, which is what "no chats known yet" really meant. The derivation does not exist in 6.17.x or 7.x, where the default is `() => true` — so the *same connector* behaved differently per user depending on which version npm had resolved.
- Fix: `shouldSyncHistoryMessage: () => true` is now passed **explicitly**, making behaviour identical on every Baileys version, and `syncFullHistory: true`. The `browser` identity had to change too: `getWebInfo` only requests a desktop-grade sync when `browser[0]` is `'Mac OS'` or `'Windows'` (`PLATFORM_MAP` has exactly those two keys) — with the old `['Skald', 'Chrome', …]` the sub-platform stayed `WEB_BROWSER` and `syncFullHistory` would have been inert. Now `['Mac OS', 'Chrome', '121.0.0']`; the trade-off is that the phone lists the device as "Mac OS Chrome" rather than "Skald".
- **Every reconnect leaked its socket, giving `auth/` two concurrent writers.** The `connection === 'close'` branch only set `starting = false` and re-entered `startSock()`: no `end()`, no `removeAllListeners()`. Each pass called `useMultiFileAuthState(AUTH_DIR)` again, building a fresh key cache, while the previous socket stayed alive with its own `creds.update → saveCreds` handler bound to the *old* snapshot. At ~15 reconnects/day (106× 428 `connectionClosed`, 42× 500 `badSession`, 34× 503, 9× 405 over two weeks) that is the standard route to inconsistent Signal state — and the logs showed the symptom: 1422 `Bad MAC` lines and 51× `Closing open session in favor of incoming prekey bundle`. Every failing session address was the account's **own LID at device 0** (its own phone): the own-device sync stream, not a remote contact.
- Fix: `teardownSock()` detaches listeners and closes the socket before a new one is built, each socket carries a **generation number** so events from a superseded socket are dropped, and reconnects go through a single-slot timer with exponential backoff (1.5s → 60s, reset on `open`) instead of a fixed 1.5s re-entry. Disconnects are now logged by name (`428 connectionClosed`) rather than bare code.
- **Undecryptable messages were stored as blank lines.** A message Signal cannot open arrives with no `message` payload and `messageStubType = CIPHERTEXT (2)`; `textOf()` returned `''` and `ingestMessage` stored it anyway, so `get_messages` rendered `[timestamp] name: ` and the agent could not distinguish a silent gap from an empty message. They are now labelled `[undecryptable message]`, counted, and surfaced in `status`. Text-less protocol frames (reactions, receipts, key distribution) are dropped instead of padding transcripts with blanks.
- **`fetchLatestBaileysVersion()` ran on every reconnect** — ~15 outbound calls/day, each able to hand the socket a protocol version the installed library cannot speak (the plausible source of the 9× 405 closes). Now fetched once and cached for 6h, falling back to the last good value, then to the library default.
- **Log noise made the shared file unreadable.** `libsignal` bypasses the Baileys logger and calls `console.error` directly — one `Failed to decrypt…` line plus a full stack trace *per candidate session* — which is how ~30 real failures became 1422 lines. `console.error` is now wrapped to collapse the burst into one counted line, and every connector line carries the linked account (`[whatsapp_mcp Daniele]`) so the two users' entries are separable. `console.log`/`info`/`warn` are redirected to stderr as well: stdout is the JSON-RPC channel and a dependency printing there would corrupt the framing.
### Changed
- **whatsapp: Baileys `^6.7.9` → pinned `7.0.0-rc14`, connector converted to ESM (v7 / 2.1.0)** — the caret range was resolving to different versions per install: 6.7.24 for one user, **6.17.16** for the other (that version sorts above 6.7.24 by semver but sits outside the maintained 6.7.x line; npm's `legacy` dist-tag points at 6.7.24, `latest` at 7.0.0-rc14). The user on 6.17.16 logged 681 decrypt failures against the other's 30. Both `@whiskeysockets/baileys` and `qrcode` are now pinned exactly, matching the convention already used by playwright/http-fetch/firecrawl.
- 7.0.0-rc14 is a release candidate — there is no stable 7.x — chosen deliberately: its defaults already do the right thing (`syncFullHistory: true`, no derived history gate) and the API surface the connector uses is unchanged (`makeWASocket`, `useMultiFileAuthState`, `DisconnectReason`, `fetchLatestBaileysVersion`, `jidNormalizedUser`, plus `WAMessageStubType`).
- It ships **ESM-only** (`"type": "module"`, engines `node >= 20`), so `index.js` moved from CommonJS to ESM rather than leaning on Node's `require(esm)` bridge; `__dirname` is derived from `import.meta.url`. `package.json` gains `"type": "module"` and `engines.node >= 20` (the `skald-runtime:v4` image runs 20.19.2).
- Verified on the real runtime image, not locally: ESM import resolves under Node 20.19.2, `initialize` + `tools/list` (7 tools) + `status` + `list_chats` answer correctly, protocol version fetch and QR generation work, and the `logout` path exercises teardown → single reconnect → fresh QR with no duplicate socket ✅
- Re-aligned manifest↔fragment versions to `7` / `2.1.0` (they had drifted to 3/2.0.3 vs 6/2.0.6; skald reads `installed_version` from the manifest, so the update badge would not have appeared).
- ⚠️ Still outstanding: the chat/contact/message store is **in-memory only**. History sync arrives at login, not on every reconnect, so whatever the full sync delivers is lost at the next process restart. Persisting the store is the remaining piece and was left out of this change.
## 2026-08-20
### Fixed