@mastra/discord 1.3.0-alpha.0 → 1.3.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/index.cjs +203 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -17
- package/dist/index.d.ts +18 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +166 -17
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -138,9 +138,9 @@ interface DiscordProviderConfig {
|
|
|
138
138
|
*/
|
|
139
139
|
waitUntil?: WaitUntilFn;
|
|
140
140
|
/**
|
|
141
|
-
* Start the Gateway WebSocket (
|
|
142
|
-
* and reactions in addition to slash
|
|
143
|
-
*
|
|
141
|
+
* Start the Gateway WebSocket (provider-owned reconnect loop with backoff)
|
|
142
|
+
* so the bot receives DMs, @mentions, and reactions in addition to slash
|
|
143
|
+
* commands. Set `false` for interactions-only serverless deployments.
|
|
144
144
|
*
|
|
145
145
|
* @default true
|
|
146
146
|
*/
|
|
@@ -308,8 +308,10 @@ interface DiscordInstallation {
|
|
|
308
308
|
* Resolve the per-adapter config the provider applies to the Discord entry in
|
|
309
309
|
* `AgentChannels.adapters`. Discord has native embeds + action-row buttons, so
|
|
310
310
|
* `toolDisplay` defaults to `'cards'` (unlike Telegram's `'text'`); `streaming`
|
|
311
|
-
* post-and-edits the interaction followup; `gateway` (default `true`) makes
|
|
312
|
-
*
|
|
311
|
+
* post-and-edits the interaction followup; `gateway` (default `true`) makes the
|
|
312
|
+
* **provider** run the DM/mention Gateway reconnection loop. (The adapter entry
|
|
313
|
+
* handed to `AgentChannels` always carries `gateway: false` — core's loop must
|
|
314
|
+
* never start; see `#createAgentChannels`.)
|
|
313
315
|
*/
|
|
314
316
|
declare function resolveDiscordAdapterConfig(config: Pick<DiscordProviderConfig, 'streaming' | 'typingStatus' | 'toolDisplay' | 'gateway'>): {
|
|
315
317
|
streaming: StreamingConfig;
|
|
@@ -331,11 +333,15 @@ declare function resolveDiscordAdapterConfig(config: Pick<DiscordProviderConfig,
|
|
|
331
333
|
* member) or lazily off the first inbound interaction's authoritative `guild_id`
|
|
332
334
|
* (see {@link activateGuild}).
|
|
333
335
|
*
|
|
334
|
-
* **Gateway is
|
|
335
|
-
* HTTP Interactions (PING, slash commands, buttons). DMs / @mentions /
|
|
336
|
-
* arrive over the Gateway WebSocket, whose reconnection loop
|
|
337
|
-
*
|
|
338
|
-
*
|
|
336
|
+
* **Gateway is the provider's job.** The mounted interactions route only ever
|
|
337
|
+
* receives HTTP Interactions (PING, slash commands, buttons). DMs / @mentions /
|
|
338
|
+
* reactions arrive over the Gateway WebSocket, whose reconnection loop the
|
|
339
|
+
* provider owns ({@link #startGatewayLoop} → `./gateway-loop`): one abortable
|
|
340
|
+
* loop per installation, exponential backoff on failed connects, parked on a
|
|
341
|
+
* revoked token. Core's `AgentChannels` loop is explicitly disabled
|
|
342
|
+
* (`gateway: false` on the adapter entry) — it reconnects with no backoff and
|
|
343
|
+
* cannot be stopped, which is how a failing IDENTIFY once tripped Discord's
|
|
344
|
+
* 1000-connect abuse limit and got the bot token force-reset.
|
|
339
345
|
*
|
|
340
346
|
* Implemented (issues `mastra-discord-13x.2` + `.3`): the app-config +
|
|
341
347
|
* guild-keyed install store, `connect()`/`disconnect()`, OAuth2 invite-URL
|
|
@@ -399,13 +405,8 @@ declare class DiscordProvider implements ChannelProvider {
|
|
|
399
405
|
*/
|
|
400
406
|
activateGuild(webhookId: string, guildId: string): Promise<DiscordInstallation | null>;
|
|
401
407
|
/**
|
|
402
|
-
* Disconnect an agent from Discord:
|
|
403
|
-
* adapter entry.
|
|
404
|
-
*
|
|
405
|
-
* **Limitation:** the Gateway loop is owned by core (it calls
|
|
406
|
-
* `startGatewayListener` itself; there is no `stopGatewayListener`), so
|
|
407
|
-
* disconnect cannot kill an in-flight gateway window — it lapses at the next
|
|
408
|
-
* duration boundary. Contrast Telegram's clean `stopPolling()`.
|
|
408
|
+
* Disconnect an agent from Discord: abort its Gateway loop, remove its
|
|
409
|
+
* installation row, and drop the adapter entry.
|
|
409
410
|
*/
|
|
410
411
|
disconnect(agentId: string): Promise<void>;
|
|
411
412
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -138,9 +138,9 @@ interface DiscordProviderConfig {
|
|
|
138
138
|
*/
|
|
139
139
|
waitUntil?: WaitUntilFn;
|
|
140
140
|
/**
|
|
141
|
-
* Start the Gateway WebSocket (
|
|
142
|
-
* and reactions in addition to slash
|
|
143
|
-
*
|
|
141
|
+
* Start the Gateway WebSocket (provider-owned reconnect loop with backoff)
|
|
142
|
+
* so the bot receives DMs, @mentions, and reactions in addition to slash
|
|
143
|
+
* commands. Set `false` for interactions-only serverless deployments.
|
|
144
144
|
*
|
|
145
145
|
* @default true
|
|
146
146
|
*/
|
|
@@ -308,8 +308,10 @@ interface DiscordInstallation {
|
|
|
308
308
|
* Resolve the per-adapter config the provider applies to the Discord entry in
|
|
309
309
|
* `AgentChannels.adapters`. Discord has native embeds + action-row buttons, so
|
|
310
310
|
* `toolDisplay` defaults to `'cards'` (unlike Telegram's `'text'`); `streaming`
|
|
311
|
-
* post-and-edits the interaction followup; `gateway` (default `true`) makes
|
|
312
|
-
*
|
|
311
|
+
* post-and-edits the interaction followup; `gateway` (default `true`) makes the
|
|
312
|
+
* **provider** run the DM/mention Gateway reconnection loop. (The adapter entry
|
|
313
|
+
* handed to `AgentChannels` always carries `gateway: false` — core's loop must
|
|
314
|
+
* never start; see `#createAgentChannels`.)
|
|
313
315
|
*/
|
|
314
316
|
declare function resolveDiscordAdapterConfig(config: Pick<DiscordProviderConfig, 'streaming' | 'typingStatus' | 'toolDisplay' | 'gateway'>): {
|
|
315
317
|
streaming: StreamingConfig;
|
|
@@ -331,11 +333,15 @@ declare function resolveDiscordAdapterConfig(config: Pick<DiscordProviderConfig,
|
|
|
331
333
|
* member) or lazily off the first inbound interaction's authoritative `guild_id`
|
|
332
334
|
* (see {@link activateGuild}).
|
|
333
335
|
*
|
|
334
|
-
* **Gateway is
|
|
335
|
-
* HTTP Interactions (PING, slash commands, buttons). DMs / @mentions /
|
|
336
|
-
* arrive over the Gateway WebSocket, whose reconnection loop
|
|
337
|
-
*
|
|
338
|
-
*
|
|
336
|
+
* **Gateway is the provider's job.** The mounted interactions route only ever
|
|
337
|
+
* receives HTTP Interactions (PING, slash commands, buttons). DMs / @mentions /
|
|
338
|
+
* reactions arrive over the Gateway WebSocket, whose reconnection loop the
|
|
339
|
+
* provider owns ({@link #startGatewayLoop} → `./gateway-loop`): one abortable
|
|
340
|
+
* loop per installation, exponential backoff on failed connects, parked on a
|
|
341
|
+
* revoked token. Core's `AgentChannels` loop is explicitly disabled
|
|
342
|
+
* (`gateway: false` on the adapter entry) — it reconnects with no backoff and
|
|
343
|
+
* cannot be stopped, which is how a failing IDENTIFY once tripped Discord's
|
|
344
|
+
* 1000-connect abuse limit and got the bot token force-reset.
|
|
339
345
|
*
|
|
340
346
|
* Implemented (issues `mastra-discord-13x.2` + `.3`): the app-config +
|
|
341
347
|
* guild-keyed install store, `connect()`/`disconnect()`, OAuth2 invite-URL
|
|
@@ -399,13 +405,8 @@ declare class DiscordProvider implements ChannelProvider {
|
|
|
399
405
|
*/
|
|
400
406
|
activateGuild(webhookId: string, guildId: string): Promise<DiscordInstallation | null>;
|
|
401
407
|
/**
|
|
402
|
-
* Disconnect an agent from Discord:
|
|
403
|
-
* adapter entry.
|
|
404
|
-
*
|
|
405
|
-
* **Limitation:** the Gateway loop is owned by core (it calls
|
|
406
|
-
* `startGatewayListener` itself; there is no `stopGatewayListener`), so
|
|
407
|
-
* disconnect cannot kill an in-flight gateway window — it lapses at the next
|
|
408
|
-
* duration boundary. Contrast Telegram's clean `stopPolling()`.
|
|
408
|
+
* Disconnect an agent from Discord: abort its Gateway loop, remove its
|
|
409
|
+
* installation row, and drop the adapter entry.
|
|
409
410
|
*/
|
|
410
411
|
disconnect(agentId: string): Promise<void>;
|
|
411
412
|
/**
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/discord-provider.ts","../src/install-store.ts","../src/discord-client.ts","../src/commands.ts","../src/crypto.ts"],"mappings":";;;;;;;;;;;;cAgBa;;cAGA;;;;;;;;cASA;WACX;WACA;WACA;WACA;WACA;WACA;WACA;WACA;;;;;;;;;cAUW;;cAWA;;;;;;UAOI;;EAEf;;EAEA;;;KAIU;EAAiC;EAAc;;;;;;;;UAQ1C;;EAEf;;EAEA;;EAEA;;;;;;;;;;UAWe;;;;;;;;;EASf,MAAM,QAAQ;;;;;EAKd;;;;;;EAMA,UAAU;;;;;EAKV;;;;;;;EAOA;;;;;EAKA;;;;;;EAMA,WAAW;;;;;;;;;;EAUX;;;;;EAKA,YAAY;;;;;;;;EAQZ;;;;;EAKA;;;;;;EAMA,mBAAmB;;EAEnB,SAAS;;;;;;;;EAQT,YAAY;;;;;;EAMZ;;EASA,WAAW;;EAEX,cAAc;;EAEd,cAAc;;EAEd,QAAQ;;EAER,gBAAgB;;EAEhB,cAAc;;EAEd,oBAAoB;;EAEpB,mBAAmB;;EAEnB,OAAO;;EAEP,cAAc;;;;;;;;EAQd,cAAc;;;;;;;EAOd,QAAQ;;EAER,aAAa,cAAc,+BAA+B;;;UAI3C;;;;;;;EAOf;;EAEA;;;;;EAKA,WAAW;;EAEX;;;;;;;;;UAUe;;EAEf;;EAEA;;;;;EAKA;;;;;EAKA;;EAEA;;EAEA;;EAEA,WAAW;;;;;;EAMX,kBAAkB;;EAElB,aAAa;;;;;;;;;;;EAWb;;;;;;;;;EASA,aAAa;;;;;;;;EAQb;;;;;;;;EAQA
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/discord-provider.ts","../src/install-store.ts","../src/discord-client.ts","../src/commands.ts","../src/crypto.ts"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -300,6 +300,88 @@ function hashCommands(commands) {
|
|
|
300
300
|
}));
|
|
301
301
|
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex").slice(0, 16);
|
|
302
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* A session that ends sooner than this is treated as a failed connect. The
|
|
305
|
+
* adapter resolves login failures as if the session ended cleanly, so elapsed
|
|
306
|
+
* time is the only signal that distinguishes "ran fine until the duration
|
|
307
|
+
* elapsed / Discord closed us" from "IDENTIFY was rejected instantly".
|
|
308
|
+
*/
|
|
309
|
+
const HEALTHY_SESSION_MS = 60 * 1e3;
|
|
310
|
+
/** First retry delay after a failed connect; doubles up to {@link MAX_BACKOFF_MS}. */
|
|
311
|
+
const INITIAL_BACKOFF_MS = 5 * 1e3;
|
|
312
|
+
/** Retry delay ceiling. */
|
|
313
|
+
const MAX_BACKOFF_MS = 900 * 1e3;
|
|
314
|
+
/** Pause between healthy sessions — a scheduled rollover, not a failure. */
|
|
315
|
+
const RECONNECT_DELAY_MS = 1e3;
|
|
316
|
+
/**
|
|
317
|
+
* Run Gateway sessions back-to-back until `signal` aborts or the bot token is
|
|
318
|
+
* confirmed revoked. Resolves when the loop stops; never rejects.
|
|
319
|
+
*/
|
|
320
|
+
async function runGatewayLoop(deps, signal) {
|
|
321
|
+
const sessionMs = deps.sessionMs ?? 864e5;
|
|
322
|
+
let backoffMs = INITIAL_BACKOFF_MS;
|
|
323
|
+
let consecutiveFailures = 0;
|
|
324
|
+
while (!signal.aborted) {
|
|
325
|
+
const startedAt = Date.now();
|
|
326
|
+
try {
|
|
327
|
+
let settle;
|
|
328
|
+
let fail;
|
|
329
|
+
const done = new Promise((resolve, reject) => {
|
|
330
|
+
settle = resolve;
|
|
331
|
+
fail = reject;
|
|
332
|
+
});
|
|
333
|
+
let sessionDelivered = false;
|
|
334
|
+
const response = await deps.startSession({ waitUntil: (promise) => {
|
|
335
|
+
sessionDelivered = true;
|
|
336
|
+
promise.then(() => settle(), (err) => fail(err));
|
|
337
|
+
} }, sessionMs, signal);
|
|
338
|
+
if (!response.ok || !sessionDelivered) throw new Error(`Gateway session refused (status ${response.status})`);
|
|
339
|
+
await done;
|
|
340
|
+
} catch (err) {
|
|
341
|
+
deps.log("error", `Gateway session error: ${err instanceof Error ? err.message : String(err)}`);
|
|
342
|
+
}
|
|
343
|
+
if (signal.aborted) return;
|
|
344
|
+
const elapsedMs = Date.now() - startedAt;
|
|
345
|
+
if (elapsedMs >= 6e4) {
|
|
346
|
+
backoffMs = INITIAL_BACKOFF_MS;
|
|
347
|
+
consecutiveFailures = 0;
|
|
348
|
+
deps.log("info", "Gateway session ended; reconnecting");
|
|
349
|
+
await sleep(RECONNECT_DELAY_MS, signal);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
consecutiveFailures += 1;
|
|
353
|
+
let check;
|
|
354
|
+
try {
|
|
355
|
+
check = await deps.checkToken();
|
|
356
|
+
} catch {
|
|
357
|
+
check = "unreachable";
|
|
358
|
+
}
|
|
359
|
+
if (signal.aborted) return;
|
|
360
|
+
if (check === "invalid") {
|
|
361
|
+
deps.log("error", "Discord rejected the bot token (401); Gateway reconnection is parked until configure() supplies new credentials. Reset tokens must be rotated in the Discord Developer Portal and updated wherever this provider gets its credentials.");
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (consecutiveFailures === 3) deps.log("error", `Gateway sessions keep ending within ${HEALTHY_SESSION_MS / 1e3}s of starting. The token checks out over REST, so the usual cause is a rejected IDENTIFY: enable the "Message Content Intent" (a privileged intent) for this app in the Discord Developer Portal, or set gateway: false for an interactions-only deployment.`);
|
|
365
|
+
deps.log("warn", `Gateway session ended after ${Math.round(elapsedMs / 1e3)}s; retrying in ${Math.round(backoffMs / 1e3)}s`);
|
|
366
|
+
await sleep(backoffMs, signal);
|
|
367
|
+
backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/** Abortable sleep: resolves early (never rejects) when the signal fires. */
|
|
371
|
+
function sleep(ms, signal) {
|
|
372
|
+
if (signal.aborted) return Promise.resolve();
|
|
373
|
+
return new Promise((resolve) => {
|
|
374
|
+
const onAbort = () => {
|
|
375
|
+
clearTimeout(timer);
|
|
376
|
+
resolve();
|
|
377
|
+
};
|
|
378
|
+
const timer = setTimeout(() => {
|
|
379
|
+
signal.removeEventListener("abort", onAbort);
|
|
380
|
+
resolve();
|
|
381
|
+
}, ms);
|
|
382
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
383
|
+
});
|
|
384
|
+
}
|
|
303
385
|
//#endregion
|
|
304
386
|
//#region src/crypto.ts
|
|
305
387
|
/**
|
|
@@ -586,8 +668,10 @@ function planReconcile(input) {
|
|
|
586
668
|
* Resolve the per-adapter config the provider applies to the Discord entry in
|
|
587
669
|
* `AgentChannels.adapters`. Discord has native embeds + action-row buttons, so
|
|
588
670
|
* `toolDisplay` defaults to `'cards'` (unlike Telegram's `'text'`); `streaming`
|
|
589
|
-
* post-and-edits the interaction followup; `gateway` (default `true`) makes
|
|
590
|
-
*
|
|
671
|
+
* post-and-edits the interaction followup; `gateway` (default `true`) makes the
|
|
672
|
+
* **provider** run the DM/mention Gateway reconnection loop. (The adapter entry
|
|
673
|
+
* handed to `AgentChannels` always carries `gateway: false` — core's loop must
|
|
674
|
+
* never start; see `#createAgentChannels`.)
|
|
591
675
|
*/
|
|
592
676
|
function resolveDiscordAdapterConfig(config) {
|
|
593
677
|
return {
|
|
@@ -611,11 +695,15 @@ function resolveDiscordAdapterConfig(config) {
|
|
|
611
695
|
* member) or lazily off the first inbound interaction's authoritative `guild_id`
|
|
612
696
|
* (see {@link activateGuild}).
|
|
613
697
|
*
|
|
614
|
-
* **Gateway is
|
|
615
|
-
* HTTP Interactions (PING, slash commands, buttons). DMs / @mentions /
|
|
616
|
-
* arrive over the Gateway WebSocket, whose reconnection loop
|
|
617
|
-
*
|
|
618
|
-
*
|
|
698
|
+
* **Gateway is the provider's job.** The mounted interactions route only ever
|
|
699
|
+
* receives HTTP Interactions (PING, slash commands, buttons). DMs / @mentions /
|
|
700
|
+
* reactions arrive over the Gateway WebSocket, whose reconnection loop the
|
|
701
|
+
* provider owns ({@link #startGatewayLoop} → `./gateway-loop`): one abortable
|
|
702
|
+
* loop per installation, exponential backoff on failed connects, parked on a
|
|
703
|
+
* revoked token. Core's `AgentChannels` loop is explicitly disabled
|
|
704
|
+
* (`gateway: false` on the adapter entry) — it reconnects with no backoff and
|
|
705
|
+
* cannot be stopped, which is how a failing IDENTIFY once tripped Discord's
|
|
706
|
+
* 1000-connect abuse limit and got the bot token force-reset.
|
|
619
707
|
*
|
|
620
708
|
* Implemented (issues `mastra-discord-13x.2` + `.3`): the app-config +
|
|
621
709
|
* guild-keyed install store, `connect()`/`disconnect()`, OAuth2 invite-URL
|
|
@@ -629,6 +717,15 @@ var DiscordProvider = class {
|
|
|
629
717
|
#store;
|
|
630
718
|
/** Live adapters, keyed by installation id (one per agent; all share the app's bot token). */
|
|
631
719
|
#adapters = /* @__PURE__ */ new Map();
|
|
720
|
+
/**
|
|
721
|
+
* Live Gateway reconnect loops, keyed by installation id. Aborting a
|
|
722
|
+
* controller ends the loop and destroys its discord.js client. Exactly one
|
|
723
|
+
* loop may exist per installation — {@link #startGatewayLoop} aborts any
|
|
724
|
+
* predecessor, so credential rotations and adapter rebuilds can't leak loops
|
|
725
|
+
* that keep IDENTIFYing with stale tokens (Discord force-resets the bot
|
|
726
|
+
* token past ~1000 connects in a short window).
|
|
727
|
+
*/
|
|
728
|
+
#gatewayLoops = /* @__PURE__ */ new Map();
|
|
632
729
|
/** Cached sync view of whether the app is configured (for {@link getInfo}). */
|
|
633
730
|
#configured = false;
|
|
634
731
|
#initPromise = null;
|
|
@@ -675,6 +772,7 @@ var DiscordProvider = class {
|
|
|
675
772
|
this.#storeIsFallback = false;
|
|
676
773
|
}
|
|
677
774
|
if (isNewInstance) {
|
|
775
|
+
this.#stopAllGatewayLoops();
|
|
678
776
|
this.#adapters.clear();
|
|
679
777
|
this.#configured = this.#hasSuppliedCredentials();
|
|
680
778
|
}
|
|
@@ -772,6 +870,7 @@ var DiscordProvider = class {
|
|
|
772
870
|
app: void 0
|
|
773
871
|
};
|
|
774
872
|
this.#configured = false;
|
|
873
|
+
this.#stopAllGatewayLoops();
|
|
775
874
|
this.#adapters.clear();
|
|
776
875
|
this.#initPromise = null;
|
|
777
876
|
return;
|
|
@@ -807,6 +906,7 @@ var DiscordProvider = class {
|
|
|
807
906
|
if (merged.botToken && merged.publicKey && merged.applicationId) await store.saveAppConfig(merged);
|
|
808
907
|
}
|
|
809
908
|
const wasInitialized = this.#initPromise !== null;
|
|
909
|
+
this.#stopAllGatewayLoops();
|
|
810
910
|
this.#adapters.clear();
|
|
811
911
|
this.#initPromise = null;
|
|
812
912
|
if (wasInitialized) await this.initialize();
|
|
@@ -964,19 +1064,15 @@ var DiscordProvider = class {
|
|
|
964
1064
|
return installation;
|
|
965
1065
|
}
|
|
966
1066
|
/**
|
|
967
|
-
* Disconnect an agent from Discord:
|
|
968
|
-
* adapter entry.
|
|
969
|
-
*
|
|
970
|
-
* **Limitation:** the Gateway loop is owned by core (it calls
|
|
971
|
-
* `startGatewayListener` itself; there is no `stopGatewayListener`), so
|
|
972
|
-
* disconnect cannot kill an in-flight gateway window — it lapses at the next
|
|
973
|
-
* duration boundary. Contrast Telegram's clean `stopPolling()`.
|
|
1067
|
+
* Disconnect an agent from Discord: abort its Gateway loop, remove its
|
|
1068
|
+
* installation row, and drop the adapter entry.
|
|
974
1069
|
*/
|
|
975
1070
|
async disconnect(agentId) {
|
|
976
1071
|
const store = await this.#getStore();
|
|
977
1072
|
const existing = await store.getByAgent(agentId);
|
|
978
1073
|
if (!existing) throw new Error(`No Discord installation found for agent "${agentId}"`);
|
|
979
1074
|
await this.#enqueueWebhookTask(existing.webhookId, async () => {
|
|
1075
|
+
this.#stopGatewayLoop(existing.id);
|
|
980
1076
|
this.#adapters.delete(existing.id);
|
|
981
1077
|
await store.deleteByAgent(agentId);
|
|
982
1078
|
});
|
|
@@ -1110,6 +1206,7 @@ var DiscordProvider = class {
|
|
|
1110
1206
|
if (!channels || channels.adapters["discord"] !== adapter) {
|
|
1111
1207
|
channels = this.#createAgentChannels(agent, adapter);
|
|
1112
1208
|
await channels.initialize(this.#mastra);
|
|
1209
|
+
this.#startGatewayLoop(installation, adapter, app);
|
|
1113
1210
|
}
|
|
1114
1211
|
try {
|
|
1115
1212
|
return scheduleActivation(await channels.handleWebhookEvent(PLATFORM, c.req.raw, waitUntil ? { waitUntil } : void 0));
|
|
@@ -1186,13 +1283,64 @@ var DiscordProvider = class {
|
|
|
1186
1283
|
if (!app) return;
|
|
1187
1284
|
const agent = this.#resolveAgent(installation.agentId);
|
|
1188
1285
|
const adapter = this.#getOrCreateAdapter(installation, app);
|
|
1189
|
-
if (agent && this.#mastra)
|
|
1286
|
+
if (agent && this.#mastra) {
|
|
1287
|
+
await this.#createAgentChannels(agent, adapter).initialize(this.#mastra);
|
|
1288
|
+
this.#startGatewayLoop(installation, adapter, app);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Start (or restart) the Gateway reconnect loop for an installation. Any
|
|
1293
|
+
* previous loop for the same installation is aborted first, so adapter
|
|
1294
|
+
* rebuilds and credential rotations replace the loop instead of stacking
|
|
1295
|
+
* another one. No-op when the provider is configured `gateway: false`.
|
|
1296
|
+
*
|
|
1297
|
+
* The loop itself lives in {@link runGatewayLoop}; see that module for why
|
|
1298
|
+
* the provider owns this instead of core's `AgentChannels` loop.
|
|
1299
|
+
*/
|
|
1300
|
+
#startGatewayLoop(installation, adapter, app) {
|
|
1301
|
+
if (!resolveDiscordAdapterConfig(this.#config).gateway) return;
|
|
1302
|
+
this.#stopGatewayLoop(installation.id);
|
|
1303
|
+
const controller = new AbortController();
|
|
1304
|
+
this.#gatewayLoops.set(installation.id, controller);
|
|
1305
|
+
const apiBaseUrl = this.#apiBaseUrl();
|
|
1306
|
+
runGatewayLoop({
|
|
1307
|
+
startSession: (options, durationMs, signal) => adapter.startGatewayListener(options, durationMs, signal),
|
|
1308
|
+
checkToken: async () => {
|
|
1309
|
+
try {
|
|
1310
|
+
const response = await discordRequest(app.botToken, "GET", "/applications/@me", apiBaseUrl);
|
|
1311
|
+
return response.status === 401 ? "invalid" : response.ok ? "valid" : "unreachable";
|
|
1312
|
+
} catch {
|
|
1313
|
+
return "unreachable";
|
|
1314
|
+
}
|
|
1315
|
+
},
|
|
1316
|
+
log: (level, message) => {
|
|
1317
|
+
const line = `[Discord] [gateway:${installation.agentId}] ${message}`;
|
|
1318
|
+
if (level === "error") console.error(line);
|
|
1319
|
+
else if (level === "warn") console.warn(line);
|
|
1320
|
+
else console.info(line);
|
|
1321
|
+
}
|
|
1322
|
+
}, controller.signal).finally(() => {
|
|
1323
|
+
if (this.#gatewayLoops.get(installation.id) === controller) this.#gatewayLoops.delete(installation.id);
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
/** Abort an installation's Gateway loop (ends the session, destroys the client). */
|
|
1327
|
+
#stopGatewayLoop(installationId) {
|
|
1328
|
+
const controller = this.#gatewayLoops.get(installationId);
|
|
1329
|
+
if (!controller) return;
|
|
1330
|
+
this.#gatewayLoops.delete(installationId);
|
|
1331
|
+
controller.abort();
|
|
1332
|
+
}
|
|
1333
|
+
/** Abort every Gateway loop — credential revocation or Mastra re-attach. */
|
|
1334
|
+
#stopAllGatewayLoops() {
|
|
1335
|
+
for (const installationId of [...this.#gatewayLoops.keys()]) this.#stopGatewayLoop(installationId);
|
|
1190
1336
|
}
|
|
1191
1337
|
/**
|
|
1192
1338
|
* Create AgentChannels for an agent with the Discord adapter, preserving any
|
|
1193
1339
|
* adapters/config the agent author already configured (mirrors `@mastra/slack`
|
|
1194
|
-
* / `@mastra/telegram`). `gateway:
|
|
1195
|
-
*
|
|
1340
|
+
* / `@mastra/telegram`). The entry always carries `gateway: false`: core's
|
|
1341
|
+
* built-in gateway loop reconnects with no backoff and no abort handle, so
|
|
1342
|
+
* the provider owns the Gateway lifecycle instead ({@link #startGatewayLoop},
|
|
1343
|
+
* gated on the provider-level `gateway` config).
|
|
1196
1344
|
*/
|
|
1197
1345
|
#createAgentChannels(agent, adapter) {
|
|
1198
1346
|
const existingConfig = agent.getChannels()?.channelConfig;
|
|
@@ -1200,6 +1348,7 @@ var DiscordProvider = class {
|
|
|
1200
1348
|
const entry = {
|
|
1201
1349
|
adapter,
|
|
1202
1350
|
...resolveDiscordAdapterConfig(cfg),
|
|
1351
|
+
gateway: false,
|
|
1203
1352
|
...cfg.cors !== void 0 ? { cors: cfg.cors } : {},
|
|
1204
1353
|
...cfg.formatError !== void 0 ? { formatError: cfg.formatError } : {}
|
|
1205
1354
|
};
|