Navigace
ELOWEN · Dokumentace

Poslední aktualizace: 20. října 2026

Procházet dokumentaci · discord implementation

Tento článek zatím není přeložen. Je dostupný v angličtině.

Developer reference

discord implementation

Problem audiences

Discord HTTP producers attach a core typed instance-condition cause for rejected credentials, missing or inaccessible destinations, rate limits and service outages. Command publication, history, interaction, message, transcription and question-delivery catches preserve typed local causes through explicit logger metadata without changing text. Invalid payload responses and programming errors remain reportable. Both message and slash-command turns forward the reporting audience supplied by shared runTurn through log(detail, { failure, reporting? }), available since core 0.30.6. This uses the paired core candidate's exported elowen-plugin-shared/errors seam.

Message and slash-command final/live delivery logs retain the complete shared FinalTextDeliveryError detail in their message and pass its ORIGINAL transport cause through the existing Logger.error extra argument (other failures pass the failure itself). Native logger formatting includes that cause in console and sink diagnostics; rendering the wrapper stack alone does not expand Error.cause. metadata.failure remains the full typed wrapper, alongside discord.turn_failed and optional local-only reporting, so audience classification is unchanged. This adds no private formatter, retry, replacement send or user notice.

Runtime ownership

lib/adapter.mjs owns the gateway connection, heartbeat acknowledgements, resume/reconnect state and inbound turns. Its existing transport methods remain the entry points used by platform tools and live message delivery. It also declares sharedFileMaxBytes (25 MiB), the agent-to-user file ceiling core applies before it copies a shared Project file to this platform; without the declaration the harness download limit stays in force.

lib/interactions.mjs owns slash registration, named slash-command and component handlers, picker pages and interaction settings writes. The common interaction entry checks guild membership, the configured guild and current role-policy access before dispatch; picker submissions still re-check operator access and current model capabilities. Shared command, display and model rules come from the host package.

lib/askFlow.mjs owns posting, paging, answer commitment, sender-bound free-text capture and retirement of parked questions. It mutates the adapter's single pending-question map; core ask_resolved events remain the only timeout authority. The final answer awaits ctx.answerQuestion; only host acceptance removes the pending entry or publishes the answered summary. While that result is pending, another final click receives a deferred acknowledgement and cannot submit again. A refused result restores the last choice for retry. handleAskText and its inbound-message caller both await the answer and message update before consuming the text. Repository failures reject to the existing platform error handler.

lib/rest.mjs owns JSON requests and the single multipart attachment transport. Images and shared files keep their MIME-by-name callbacks and existing argument order. Speech uses that same uploader with its returned per-file MIME and the distinct audio error label. Both retain bot authorization and the single finite, 429-only request owner described below.

The entry module owns registration and notification-destination projection. Role-policy helpers are imported directly from lib/format.mjs; the entry does not re-export them. Runtime admission and the platform-policy parity suite use that same owner.

Canonical bootstrap, gateway and request lifetimes

The adapter's required retry context is the real PluginContext retryPolicy()/onRetryPolicyChanged() pair. Subscription precedes the async read, newer revisions win and each active episode pins one frozen policy; lifetime abort disposes the subscription. New ordinary requests use current policy. connect({signal,onConnectionState}) initializes local ownership only, resolving without network bootstrap or a claim of availability. Await disconnect() to abort and join bootstrap, physical request cleanup and socket closure.

ONE shared long-lived-transport controller owns safe identity bootstrap GETs AND the entire gateway lifetime. Its stable target is {kind:'platform',id:'discord:configured-bot'} for the single configured binding, not an external bot/account id or decoded/hashed credential. Learned public bot/app ids never retarget that episode; host generations fence binding replacement. This always-on transport has maxRetries:null, canonical exponential timing, 60000 ms exponential cap and no jitter/hidden bootstrap maximum. Safe network, timeout, 429 and 5xx bootstrap failures retry in that owner; 401/403 block as auth_required/forbidden. Header Retry-After and validated JSON retry_after seconds are not-before minima, including values above the exponential cap. Each bootstrap request, including body read, is bounded to 30000 ms and physically settles before retry/cancellation finishes.

Bootstrap calls plugin-owned restOnce for one physical request. registerCommands(adapter, request) accepts that same seam: optional startup slash publication runs once per adapter lifetime, never again merely because the gateway reconnects. Unknown global/guild PUT outcomes are not replayed. Only confirmed publication persists its fingerprint; best-effort global cleanup logs discord.command_cleanup_failed instead of silently claiming success.

Only gateway READY/RESUMED acknowledges readiness. The 30000 ms handshake bound, heartbeat-ACK failure and actual socket close remain protocol boundaries, not another scheduler. Missing ACK closes with 4000, not session-invalidating 1000/1001. Resume retains session id, URL and sequence including zero; Invalid Session false or 4007/4009 clears them. 4004 and 4010..4014 block. Only canonical 60000 ms continuously-ready stability resets backoff; successful identity GETs or short READY flaps do not. Retired sockets cannot restore host availability.

Ordinary JSON/multipart requests instead use ONE finite request controller with maxRetries = min(policy.maxRetries, 3), preserving the four-physical-call ceiling. Only proven HTTP 429 allows replay; network, 5xx, invalid JSON and unknown writes do not. Nullable canonical server minima replace the old private one-second wait. Signals cover adapter lifetime and explicit JSON cancellation; multipart retries recreate FormData with original MIME/reply metadata. Existing local-audience wrappers preserve the exact network cause. listGuildChannels(config,retryHost) receives the real host context and retains its five-second deadline/sibling cleanup.

The live-message binding requires a confirmed nonempty create id and fulfilled edit returning true; original failures propagate, never null/false success masks. The shared live-message/turn owners alone decide higher delivery. Unknown edits cannot authorize a replacement create. No active adapter means no bootstrap/socket work. The local wire-server case (tests/discordGatewayWire.test.ts) has passed Identify/READY and socket shutdown with close code 4000. This loopback case and focused fake-network/host-runtime tests do not establish live or physical Discord acceptance.

Destinations, transport and startup state

Discord contributes destinations through registerNotificationDestinationProvider before the bot-token check. The retired /plugins/discord/channels route is absent. Enabled but unconfigured Discord offers an empty list; absent provider contributes nothing. Listing shares rest and discordApiBase with the adapter. Cache identity is API base, token and guild, with a 60-second TTL. Channels/active threads run together under a five-second deadline including rate-limit waits. Channel failure yields an empty picker; thread HTTP failure retains channels. Completion/deadline cancel unfinished siblings.

rest(adapter, method, path, body, { signal }) retains Bot authorization, JSON and null for 204. Its finite request owner uses canonical policy and Retry-After; bootstrap uses restOnce under the separate long-lived connection owner, not nested REST recovery. Multipart retries recreate FormData and retain MIME/payload/error labels.

Entries import StateStore directly from plugin-shared/stateStore. Registration awaits startup pruning, slash registration awaits its fingerprint write, and interaction handlers await every settings patch before reading the committed cache or confirming a change. A rejected interaction write produces the existing save-failure response. Authoritative registration prunes voice, fast and globalCleared before the missing-token return. commandFingerprint, model, gen, thinkingLevel and display survive. Atomic writer failure propagates and preserves the previous file; delegated registration performs no migration and unchanged state causes no rewrite. channel-state.json is the consumer.

attachmentNote(name, type) from elowen-plugin-shared/voice returns the attachment or untranscribed-audio marker for caller-normalized inputs without downloads, storage, localization or inference. Minimal use is attachmentNote(clip.name, clip.type). Callers retain MIME normalization/defaults; no attachment means no call. Discord and Telegram use it after absent core transcription, Teams for non-image notes, and WhatsApp retains its different voice note. Ship these consumers with the containing core export; manifests remain the source for release requirements.