ADR-0031: Direct messenger integration (no middleware); Discord Gateway needs a single-owner lease
Status
Accepted (pending Phase 1 implementation)Tags
digital-worker, integration, telegram, discord, whatsapp, multi-replica, lease, chatDecision
The Digital Worker integrates directly with each messenger’s own bot API — WhatsApp (Meta Cloud API), Telegram (Bot API), Discord (Bot Gateway) — with no OpenClaw/Hermes middleware and no Bun bridge. Each provider adapter is a thin Go-owned boundary that normalizes inbound events into oneChatEvent contract and performs outbound sends via a ChatSender port.
Ingest is not uniform, and that affects multi-replica safety:
- WhatsApp & Telegram ingest via stateless webhooks — any replica can receive; the
(provider, external_message_id)idempotency key dedups retries. - Discord requires a persistent Gateway WebSocket for channel-message events (its webhooks cover only slash-command interactions). Exactly one replica owns the Discord Gateway connection via a
discord-gateway-ownerlease (the lease/advisory-lock pattern of ADR-0023); on holder death another replica takes over using Discord RESUME (session_id+seq).
Why
The original spec hedged on routing chat through OpenClaw/Hermes middleware. Going direct removes an external dependency, an extra network hop, duplicated identity/audit concerns, and the uncertainty of “which transport will the middleware expose first.” It is simpler and fully under our control — and the CRM module’s existing Meta WhatsApp client (inbound webhook + outbound Graph send) already proves direct provider integration works here. Recording the rejection matters so nobody re-proposes a middleware layer in six months. The surprising part worth recording is Discord. In an otherwise stateless, horizontally-scaled core, Discord alone needs a singleton connection with leader-election — a future reader will wonder why one provider gets special treatment. The reason: Discord has no webhook path for observing channel messages (only the Gateway deliversMESSAGE_CREATE / MESSAGE_UPDATE), and it actively rejects multiple Gateway sessions per bot token. So a per-replica connection would produce N× duplicate ingest and hit session limits. The lease confines the statefulness to Discord ingest ownership; everything downstream (triage, extraction, send) stays stateless and multi-replica.
Rejected alternatives:
- OpenClaw/Hermes middleware layer. External dependency, extra hop, duplicated identity/audit, uncertain transport. Rejected — direct is simpler and owned.
- Bun/ClawUI-owned worker. Duplicates org identity, permissions, audit, and AI billing concerns. Rejected for the product core.
- Per-replica Discord Gateway connections. N× duplicate ingest; Discord session limits / forced disconnects. Rejected.
- Pinned single-instance Discord ingest (deployment scaled to 1). Simpler than leader-election but a single point of failure until restarted. Rejected in favor of the lease.
Consequences
- A Discord failover gap that exceeds the RESUME window loses those Gateway messages — Discord offers no full backfill (logged; the human is still in the channel).
- The lease adds leader-election complexity, but scoped narrowly to Discord ingest; WhatsApp/Telegram ingest and all outbound send remain stateless.
- Each provider needs its own bot credentials, stored encrypted in
dw_worker_credentials(AES-256-GCM, per ADR’s reuse of the BYOKKeyCryptoprimitive).
Rules for agents
- Do not introduce an OpenClaw/Hermes/Bun middleware hop for chat transport; adapters are Go-owned and direct.
- Discord ingest MUST run under a single-owner lease with RESUME-based failover; never open the Gateway from every replica.
- WhatsApp/Telegram ingest MUST be stateless webhooks deduped by the idempotency key; outbound send is stateless REST for all providers.