Bug 1 — Watchdog doesn't track followers:
lastPollAt = Date.now() moved before leader check. All poll()
invocations update the timestamp, so if a follower's loop dies
the watchdog fires after WATCHDOG_THRESHOLD_MS and restarts it.
Bug 2 — Non-atomic GetDel for cross-instance recovery:
Replaced GET + DEL with atomic GETDEL (Redis 6.2+, ioredis v5).
Two instances can no longer both recover the same callback reply.
Bug 3 — Binding codes stored in per-process memory:
generateBindingCode() now async; stores in Redis:
wecom:bindcode:{CODE} → instanceId (TTL 15min)
wecom:bindcode:inst:{instId} → CODE (reverse lookup)
resolveBindCode() uses GETDEL atomically, then deletes reverse key.
Falls back to in-memory Map when Redis is unavailable.
Old code for same instance is revoked on regenerate.
handleMessage updated: resolveBindCode() replaces Map.get();
6-char hex pattern with no match now returns expired-code hint.
Controller wecomGenerateBindCode now awaits generateBindingCode().
Bug 4 — enter_session events not deduplicated:
handleEnterSession now receives msgId from the event.
redisDedup(msgId) called before sending welcome message — prevents
duplicate welcomes on WeCom retransmission or cursor reset.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>