whatsapp implementation
Problem audiences
Pairing and adapter producers use Baileys Boom status fields, transport errno and the core typed instance-condition cause for account/socket availability, credential, quota and delivery state. Those conditions stay local; programming defects remain reportable. The pino bridge keeps opaque SDK protocol diagnostics local because an Error object alone does not prove a software defect. All diagnostic messages remain unchanged. The shared runTurn callback preserves the original failure and core reporting metadata. Native cause chains retain structured Baileys conditions through final-send wrappers. The typed core cause is supplied by the paired candidate's public elowen-plugin-shared/errors export.
Runtime ownership
lib/adapter.mjs owns ingress authorization, turn execution, media and outbound transport. It checks current group and sender access before passing a reply to a parked question or menu, then applies the mention gate to ordinary messages.
lib/pairing.mjs owns authentication, the current Baileys socket, QR/phone-code snapshots and the canonical connection lifetime below. The adapter supplies inbound-message and sent-message lookup callbacks; outbound transport and group tools resolve the current ready socket through this owner. Awaited disconnect cancels and joins its existing resources.
lib/menus.mjs owns command rendering, numbered pages and selection checks. lib/askFlow.mjs owns parked questions, ordered text answers and core settlement. Final submission awaits ctx.answerQuestion before removing its pending entry; a false result keeps the question available for retry, and a rejected host read propagates through the existing message handler. Only menus use the adapter's configured askTimeoutMs; questions are considered before menus. The host shared question parser, answer projection and model/reasoning helpers remain their source of truth. These modules are internal and add no platform API or persistent state.
askTimeoutMs controls only interactive picker menus, such as /model. With no override it uses six minutes. An expired menu closes locally; parked core questions have no additional adapter deadline.
Parked AskUserQuestion prompts remain answerable until the core accepts an answer or emits ask_resolved for timeout, cancellation or replacement. Channel adapters do not expire these questions locally. askTimeoutMs controls only the adapter's local interactive pickers or numbered menus; it does not change the core question deadline. Core resolution removes the pending entry and retires the prompt. Telegram accepts free text only from the original sender and permits option callbacks from that sender or an operator; WhatsApp text replies retain their original sender binding.
Native question rendering passes the selected service-message table to formatQuestionOptionLabel(option, messages, maxLabelLength) and formatQuestionPrompt(question, messages) from elowen-plugin-shared/ask. Recommendation labels and numbered reply instructions use English, Czech or Slovak without changing stored option labels or values. Non-recommended options have no suffix, and custom: false omits the custom-reply instruction. Rendering keeps the channel's markup and limits; there is no message-table fallback.
WhatsApp receives at most one image per incoming message. The removed maxImages setting had no effect and is retired through retiredConfigKeys: ["maxImages"]. The authoritative core plugin loader removes a stored value before registration using the existing configuration revision check. An installation without that key needs no cleanup. maxImageBytes still bounds inbound download size, and maxUploadImages still bounds shared images attached to an outgoing reply. WhatsApp is the consumer; no plugin-local migration or media behavior change is required.
maxImageBytes defaults to 5 MiB; an oversized inbound image becomes an attachment note. maxUploadImages defaults to four and is configurable from one to ten. Cleanup of an absent retired key needs no write, unrelated settings stay intact, and read-only loading does not persist cleanup. Shared files use the host-provided chat-files directory and retain their file names and bytes.
Canonical connection and pairing lifetime
The required adapter retry context reads/subscribes to PluginContext policy.
Subscription precedes bootstrap; revisions fence late reads, active episodes pin
policy and abort/disconnect disposes the subscription. connect installs local
ownership, not application readiness. ONE shared long-lived transport controller
owns socket start/reconnect with maxRetries:null, deterministic exponential
policy, 60000 ms cap and no jitter/second maximum. Only 60000 ms continuous OPEN
readiness earns the canonical stability reset. Explicit fresh pairing cleans up
the previous binding/controller before starting another episode.
The stable target is {kind:'platform',id:'whatsapp:paired-device'} for the one
configured device slot, never a phone/JID as an Elowen account id. Random public
authority, numeric binding generation and sequence fence stale observations and
socket/credential/message callbacks. The 20000 ms protocol handshake bound and
public Baileys connectTimeoutMs are separate from intentional QR wait: a QR
ends handshake timing, so first-auth wait may exceed host initialization timing.
Cleanup awaits public end(), close and pending credential saves within 5000 ms;
a stuck teardown rejects, is reported and cannot overlap a successor socket.
The authenticated admin-only pairing GET requires exactly:
{qrImage:string|null,code:string|null,connected:boolean,linked:boolean,connection:ConnectionState|null}.
linked means retained credentials/account, not a live socket. connected means
current OPEN plus identity readiness, and connection is null before initialization.
The public getPairing() projection marks a non-null connection.authority.kind
as server-observation, preserving the producer's authority id, generation,
sequence, retry payload and timestamps, observedAt and freshness. Null remains
null. Internal publish() and ctx.onConnectionState keep local-owner authority.
The browser receives a server observation, so it must not count down server wall
clock timestamps using its own clock; the shared status still presents the retry
progress without claiming a locally authoritative countdown.
Close immediately withdraws readiness and retired QR; outbound requireSock
rejects. An expired unregistered pairing retains credentials and its one phone-code
request; a genuine registered logout clears dead credentials and blocks
auth_required until explicit new pairing. Pair while linked/offline never wipes
that account. Serialized explicit Unpair attempts provider logout ONCE, reports
an unconfirmed outcome, then clears the requested local binding; it cannot promise
provider-side success. Only existing explicit reset/registered-logout paths delete
credentials. QR and phone codes stay in authenticated settings, never logs.
The API 64 settings/modal consume the real host ConnectionStatus, not a local
formatter or transport retry button. One serial, abortable 1500 ms GET observer
continues after OPEN, with a 10000 ms request bound and generation/sequence fencing.
Read error projects null rather than remembered connected success. A linked but
offline account retains Unpair and shared connection state, not fresh Pair/QR wait.
Pair/Unpair are guarded explicit clicks, never mount/read-error mutation replay.
Live create requires a confirmed nonempty message key, edit returns true only on fulfillment and original typed failures propagate unchanged. The shared live owner decides higher delivery; no local retry authorizes unknown-write replay or changes Baileys message resend policy. Plugin retry counts cover actual socket starts, NOT unobserved SDK protocol/resend substeps. No initialized owner means no socket work. Focused actual-component tests use a labelled external alias to the candidate host component; the ordinary shared harness alias, translated copy, real-browser and physical pairing acceptance remain separate integration gates.
Pairing, media and commands
The current artifact requires core 0.30.7 and shared API 14, and matches GET /plugins/whatsapp/pairing, POST /plugins/whatsapp/pair and POST /plugins/whatsapp/unpair as complete authenticated API patterns. Undeclared suffixes return dispatcher 404 before adapter access. The handlers receive an empty req.path; core continues to enforce administrator access and the WhatsApp grant.
Pairing retains a QR PNG data URL and phone code only for authenticated pairing settings, never terminal/log output. It writes no QR PNG file and accepts no qrPngPath constructor argument; pre-existing files are not deleted. Credentials/reconnect remain pairing-owned. Connected snapshots omit pairing material; absent QR/code events produce no new image/code. GET /plugins/whatsapp/pairing consumes the data URL.
sendImages/sendDocuments share an ordered media loop. Only the first item carries the caption and trigger quote. Each provider send is awaited; the first rejection stops the batch and propagates the original cause, without retrying an uncertain upload or attempting later items. Successful image/document payloads retain their order, bytes, MIME and filenames where appropriate. Empty lists send nothing and captions remain bounded to 1,024 characters. Shared live-message image/file callbacks propagate that rejection to the finalizer and turn owner, including after a progress-only failure, so a failed upload cannot earn the turn's done marker. Notifications use the same rejecting image callback and do not continue to text after its failure.
Pickers render numbered text pages. Parser-supported native button/list replies remain accepted inputs. Commands come lazily from ctx.chatCommands('whatsapp'); /help lists the current catalog and /stats is the status command. Stored descriptors carry no page; rendered navigation entries preserve current-page selection identity and existing permission classifications.
Service copy supports en/cs/sk. Shared display policy replaces progress with the final answer when deleteToolActivityAfterTurn is enabled. The engine requests actual deletion when discarding a message such as a superseded draft; WhatsApp may show its normal tombstone. There is no separate ephemeral-progress mode.