@mastra/discord 1.3.0-alpha.0 → 1.3.0
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 +29 -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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# @mastra/discord
|
|
2
2
|
|
|
3
|
+
## 1.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Fixed Discord installs staying "pending" forever after completing the bot invite. Discord's invite flow doesn't notify the server when it finishes, so the provider now exposes `reconcileInstallation(agentId)` — it activates a pending install when the server can attribute the newly joined guild to it, and Studio calls it when you return, so the agent shows "Connected" right away: ([#25993](https://github.com/mastra-ai/mastra/pull/25993))
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
const info = await discord.reconcileInstallation('my-agent');
|
|
11
|
+
// info?.status === 'active' once the invite finished
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
When the new guild can't be attributed safely (several invites in flight, or the bot joined more than one guild), the install stays pending and activates on its first interaction, as before. Invite attribution expires after 30 minutes and never crosses a bot credential change. `listInstallations()` is now a pure read.
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Fixed a Discord Gateway reconnect storm that could trip Discord's connection abuse limit (>1000 connects in a short window) and get the bot token force-reset. The provider now owns the Gateway reconnection loop: failed connects (invalid token, Message Content privileged intent not enabled) back off exponentially instead of retrying instantly, a revoked token parks reconnection until new credentials arrive, and exactly one loop runs per installation — credential rotations and `disconnect()` now stop the previous loop instead of leaking it. ([#26095](https://github.com/mastra-ai/mastra/pull/26095))
|
|
19
|
+
|
|
20
|
+
- Updated dependencies [[`b54fda3`](https://github.com/mastra-ai/mastra/commit/b54fda3f30330d65e52bf34802f0aa4035e30ef8), [`b0d2c38`](https://github.com/mastra-ai/mastra/commit/b0d2c387ec339229d878fdd9bbf6b6f87ec308b8), [`97644a7`](https://github.com/mastra-ai/mastra/commit/97644a78cafe8276026509c56a70108075e950b7), [`06e3dcf`](https://github.com/mastra-ai/mastra/commit/06e3dcf59aa937d8d5ab4de61b87465dfe38a62d), [`56eb894`](https://github.com/mastra-ai/mastra/commit/56eb894700575480c0e5d14a1ed7b633008610f2), [`79b3c78`](https://github.com/mastra-ai/mastra/commit/79b3c7875c511a718526020e3442bca433787199), [`8a5278a`](https://github.com/mastra-ai/mastra/commit/8a5278a8ab3fc6d4ae81073c7cef100954b4f0ef), [`7a50f76`](https://github.com/mastra-ai/mastra/commit/7a50f76900eb1488f755090651deae87b57cbab1), [`6cb981b`](https://github.com/mastra-ai/mastra/commit/6cb981bc62994e4c775864204617af70a7db3c4a), [`4cf860a`](https://github.com/mastra-ai/mastra/commit/4cf860a5a550a21fabce43010e6f1c95710e155c), [`e554c6d`](https://github.com/mastra-ai/mastra/commit/e554c6d7ff40805950f37a230ede4e2db82fc426), [`e5f53fe`](https://github.com/mastra-ai/mastra/commit/e5f53fe5965b22b274435bde05fd75f0b851e1e5), [`9c5fd7d`](https://github.com/mastra-ai/mastra/commit/9c5fd7dd5468d4b029d1015a711b328010a71484), [`dac82ea`](https://github.com/mastra-ai/mastra/commit/dac82eaa324b66acad38d468799fa4e66594107f), [`3b03b05`](https://github.com/mastra-ai/mastra/commit/3b03b054281496e07201284f686b20b4dc2c51b1), [`06496a9`](https://github.com/mastra-ai/mastra/commit/06496a961baaa86178efe24be052107ea019d649), [`616ef0f`](https://github.com/mastra-ai/mastra/commit/616ef0fa482a7724f5e93609ab4f3960e3784a17), [`bcc2ceb`](https://github.com/mastra-ai/mastra/commit/bcc2ceb951d5259d09cde558dd6b86015b096d5c), [`9d4f647`](https://github.com/mastra-ai/mastra/commit/9d4f647c52ac5701f04ff320399d01b4cc2f0942), [`cdf0d0b`](https://github.com/mastra-ai/mastra/commit/cdf0d0bcad55398a2022bbf10fe921ca801d09ac), [`7736c40`](https://github.com/mastra-ai/mastra/commit/7736c40dedd54ce840f834f7de862e64895cd3a8), [`ce51958`](https://github.com/mastra-ai/mastra/commit/ce5195800c77c90141ee38684b4b163006dd56ff), [`b9c0fe5`](https://github.com/mastra-ai/mastra/commit/b9c0fe5e4cc4bc1758a7569837ae9e76a6e35839), [`bf982e9`](https://github.com/mastra-ai/mastra/commit/bf982e91512d5fb864984b44e649f104b7a9d7a4), [`da4eac9`](https://github.com/mastra-ai/mastra/commit/da4eac96c1856b81dd132183bccb3247de1d427f), [`832f57d`](https://github.com/mastra-ai/mastra/commit/832f57da36a03e5a90bf3ccc90e9df26ecf7d59d), [`97644a7`](https://github.com/mastra-ai/mastra/commit/97644a78cafe8276026509c56a70108075e950b7), [`ed8b01a`](https://github.com/mastra-ai/mastra/commit/ed8b01a81ebf018779571de5d9af63cdc61c5693), [`a3d23f9`](https://github.com/mastra-ai/mastra/commit/a3d23f9c2ea1283001b06dffd5015f798bf75d9d), [`757b1e4`](https://github.com/mastra-ai/mastra/commit/757b1e48e8645fd99551b0af9e8ce1b415f876ea), [`edf1ce6`](https://github.com/mastra-ai/mastra/commit/edf1ce69cc703f917cd2ee06488293a1f1d45597), [`824eb7f`](https://github.com/mastra-ai/mastra/commit/824eb7fef2eb3a52a63c59c2b879c7211294e5ae), [`648a4f3`](https://github.com/mastra-ai/mastra/commit/648a4f3ec442416816173e5fd64b97efd930df8d), [`539b958`](https://github.com/mastra-ai/mastra/commit/539b958da37c302f0b8bee5d9ce2b063c63ab09a), [`847a426`](https://github.com/mastra-ai/mastra/commit/847a426fc2158fdec7c939e576e8072c7998f2e3), [`4c1bc9d`](https://github.com/mastra-ai/mastra/commit/4c1bc9d87fb5545b190e7e691331576781bffecf), [`f6fb6bc`](https://github.com/mastra-ai/mastra/commit/f6fb6bc2b0efadd6b744b6f73f07aa9800e5fc07), [`810b48d`](https://github.com/mastra-ai/mastra/commit/810b48dd77d992966a47ca5920e3c32267521b3a), [`7e63f04`](https://github.com/mastra-ai/mastra/commit/7e63f0486ea13841fc64395e3c03866afa476449), [`b0d2b33`](https://github.com/mastra-ai/mastra/commit/b0d2b336efd2a023a9f29218b442b386e42248f9), [`7a5c69e`](https://github.com/mastra-ai/mastra/commit/7a5c69e59d6f23b68c44887b15a674715e8c876f), [`53ef78f`](https://github.com/mastra-ai/mastra/commit/53ef78fa1314549de9e3ac8fd7bf57941112e316), [`9131d74`](https://github.com/mastra-ai/mastra/commit/9131d7459cfbd67037b7ea2515fcf22b60c213f3), [`196fd89`](https://github.com/mastra-ai/mastra/commit/196fd89df87b1675adcff0d4eeb1cd75965e40cb), [`b1a5896`](https://github.com/mastra-ai/mastra/commit/b1a5896196764500614cd435c48c6364a00e8726), [`0a37598`](https://github.com/mastra-ai/mastra/commit/0a375986869049865023d765337db427b6e27436), [`3acf1e3`](https://github.com/mastra-ai/mastra/commit/3acf1e36e26835caac9c22764bc87ee536ef5a62), [`7a046c6`](https://github.com/mastra-ai/mastra/commit/7a046c6a75c27d9859d695a59f6b3e8a96f6bfc8), [`9168424`](https://github.com/mastra-ai/mastra/commit/9168424453b5c0d793e0ddaa8066dceec60f619a), [`e1478fc`](https://github.com/mastra-ai/mastra/commit/e1478fc0cb9284f2e6fca7e582381c06749e2c06), [`6efbfad`](https://github.com/mastra-ai/mastra/commit/6efbfad1d763f54a2b346579d43a67ad0d92ce42), [`fb03761`](https://github.com/mastra-ai/mastra/commit/fb0376186c5fc8fc633c38d13a8dcc7c976318d8), [`1d94199`](https://github.com/mastra-ai/mastra/commit/1d94199fbb65d5acbcd0101bcbac96876e35cac4), [`018ae9d`](https://github.com/mastra-ai/mastra/commit/018ae9d2f4ebfd3bd6f267d0010171a546cb3abf), [`3e7a81b`](https://github.com/mastra-ai/mastra/commit/3e7a81b4e9b2c9de440b85b315a8297418afbaca), [`8fd2313`](https://github.com/mastra-ai/mastra/commit/8fd23138d68dd1b1b324a45db645c4968df45751), [`c3caa9a`](https://github.com/mastra-ai/mastra/commit/c3caa9a04cfa7652a9e5e214839285074eaa3f05), [`077dc71`](https://github.com/mastra-ai/mastra/commit/077dc7181a69bd473319ce1c48f7fd2fcdf95b97), [`718207d`](https://github.com/mastra-ai/mastra/commit/718207d5cc37d625bea6ff290fe25a949d3594f6), [`c498e24`](https://github.com/mastra-ai/mastra/commit/c498e249038d08a2e2fc31eed7ba4ca5e7fa1aa8), [`873b67e`](https://github.com/mastra-ai/mastra/commit/873b67e1e80e33cedf1809bf51f342cf7e9e654f), [`c96dab0`](https://github.com/mastra-ai/mastra/commit/c96dab05e69601667bc237ff2b27b9cb7d1f50c6), [`6a4f0bd`](https://github.com/mastra-ai/mastra/commit/6a4f0bd01016fba8d8dea5159a18c6a400237256), [`a4b2030`](https://github.com/mastra-ai/mastra/commit/a4b2030f6a1cb7123530f99d06f2b9e461e63932), [`2a48242`](https://github.com/mastra-ai/mastra/commit/2a48242a18f7444896bf8c7054fb59c0afae050e), [`07440af`](https://github.com/mastra-ai/mastra/commit/07440affa587b68f8348eb68e92fc1aa1817b61f), [`499f480`](https://github.com/mastra-ai/mastra/commit/499f480c86ba137356367e6b6281ba02b42d8169), [`bb57489`](https://github.com/mastra-ai/mastra/commit/bb5748958b6d404619884f7e04a0d7619fdebae7), [`045d583`](https://github.com/mastra-ai/mastra/commit/045d583852e55d0c1c518d2f5f9c33b48243cf7d), [`4ec3ccd`](https://github.com/mastra-ai/mastra/commit/4ec3ccde9924c27e7320f7bbe26c932731b7b4cd), [`3439cb2`](https://github.com/mastra-ai/mastra/commit/3439cb236f17bd248a326ff7f2c934cfb9974936), [`b8be029`](https://github.com/mastra-ai/mastra/commit/b8be0295bf88782f95702e65349a714d03a787d1)]:
|
|
21
|
+
- @mastra/core@1.75.0
|
|
22
|
+
|
|
23
|
+
## 1.3.0-alpha.1
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- Fixed a Discord Gateway reconnect storm that could trip Discord's connection abuse limit (>1000 connects in a short window) and get the bot token force-reset. The provider now owns the Gateway reconnection loop: failed connects (invalid token, Message Content privileged intent not enabled) back off exponentially instead of retrying instantly, a revoked token parks reconnection until new credentials arrive, and exactly one loop runs per installation — credential rotations and `disconnect()` now stop the previous loop instead of leaking it. ([#26095](https://github.com/mastra-ai/mastra/pull/26095))
|
|
28
|
+
|
|
29
|
+
- Updated dependencies [[`97644a7`](https://github.com/mastra-ai/mastra/commit/97644a78cafe8276026509c56a70108075e950b7), [`4cf860a`](https://github.com/mastra-ai/mastra/commit/4cf860a5a550a21fabce43010e6f1c95710e155c), [`dac82ea`](https://github.com/mastra-ai/mastra/commit/dac82eaa324b66acad38d468799fa4e66594107f), [`97644a7`](https://github.com/mastra-ai/mastra/commit/97644a78cafe8276026509c56a70108075e950b7), [`0a37598`](https://github.com/mastra-ai/mastra/commit/0a375986869049865023d765337db427b6e27436), [`018ae9d`](https://github.com/mastra-ai/mastra/commit/018ae9d2f4ebfd3bd6f267d0010171a546cb3abf), [`c498e24`](https://github.com/mastra-ai/mastra/commit/c498e249038d08a2e2fc31eed7ba4ca5e7fa1aa8), [`045d583`](https://github.com/mastra-ai/mastra/commit/045d583852e55d0c1c518d2f5f9c33b48243cf7d)]:
|
|
30
|
+
- @mastra/core@1.75.0-alpha.7
|
|
31
|
+
|
|
3
32
|
## 1.3.0-alpha.0
|
|
4
33
|
|
|
5
34
|
### Minor Changes
|
package/dist/index.cjs
CHANGED
|
@@ -109038,7 +109038,7 @@ var require_Shard = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
109038
109038
|
const path$2 = require("path");
|
|
109039
109039
|
const process$7 = require("process");
|
|
109040
109040
|
const { setTimeout: setTimeout$1, clearTimeout: clearTimeout$1 } = require("timers");
|
|
109041
|
-
const { setTimeout: sleep$
|
|
109041
|
+
const { setTimeout: sleep$2 } = require("timers/promises");
|
|
109042
109042
|
const { SHARE_ENV } = require("worker_threads");
|
|
109043
109043
|
const { DiscordjsError, ErrorCodes } = require_errors();
|
|
109044
109044
|
const ShardEvents = require_ShardEvents();
|
|
@@ -109226,7 +109226,7 @@ var require_Shard = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
109226
109226
|
*/
|
|
109227
109227
|
async respawn({ delay = 500, timeout = 3e4 } = {}) {
|
|
109228
109228
|
this.kill();
|
|
109229
|
-
if (delay > 0) await sleep$
|
|
109229
|
+
if (delay > 0) await sleep$2(delay);
|
|
109230
109230
|
return this.spawn(timeout);
|
|
109231
109231
|
}
|
|
109232
109232
|
/**
|
|
@@ -109449,7 +109449,7 @@ var require_ShardingManager = /* @__PURE__ */ __commonJSMin(((exports, module) =
|
|
|
109449
109449
|
const fs = require("fs");
|
|
109450
109450
|
const path$1 = require("path");
|
|
109451
109451
|
const process$6 = require("process");
|
|
109452
|
-
const { setTimeout: sleep } = require("timers/promises");
|
|
109452
|
+
const { setTimeout: sleep$1 } = require("timers/promises");
|
|
109453
109453
|
const { Collection } = require_dist$4();
|
|
109454
109454
|
const Shard = require_Shard();
|
|
109455
109455
|
const { DiscordjsError, DiscordjsTypeError, DiscordjsRangeError, ErrorCodes } = require_errors();
|
|
@@ -109613,7 +109613,7 @@ var require_ShardingManager = /* @__PURE__ */ __commonJSMin(((exports, module) =
|
|
|
109613
109613
|
const promises = [];
|
|
109614
109614
|
const shard = this.createShard(shardId);
|
|
109615
109615
|
promises.push(shard.spawn(timeout));
|
|
109616
|
-
if (delay > 0 && this.shards.size !== this.shardList.length) promises.push(sleep(delay));
|
|
109616
|
+
if (delay > 0 && this.shards.size !== this.shardList.length) promises.push(sleep$1(delay));
|
|
109617
109617
|
await Promise.all(promises);
|
|
109618
109618
|
}
|
|
109619
109619
|
return this.shards;
|
|
@@ -109697,7 +109697,7 @@ var require_ShardingManager = /* @__PURE__ */ __commonJSMin(((exports, module) =
|
|
|
109697
109697
|
delay: respawnDelay,
|
|
109698
109698
|
timeout
|
|
109699
109699
|
})];
|
|
109700
|
-
if (++s < this.shards.size && shardDelay > 0) promises.push(sleep(shardDelay));
|
|
109700
|
+
if (++s < this.shards.size && shardDelay > 0) promises.push(sleep$1(shardDelay));
|
|
109701
109701
|
await Promise.all(promises);
|
|
109702
109702
|
}
|
|
109703
109703
|
return this.shards;
|
|
@@ -123635,6 +123635,120 @@ function hashCommands(commands) {
|
|
|
123635
123635
|
return (0, crypto$1.createHash)("sha256").update(JSON.stringify(canonical)).digest("hex").slice(0, 16);
|
|
123636
123636
|
}
|
|
123637
123637
|
//#endregion
|
|
123638
|
+
//#region src/gateway-loop.ts
|
|
123639
|
+
/**
|
|
123640
|
+
* Provider-owned Discord Gateway reconnection loop.
|
|
123641
|
+
*
|
|
123642
|
+
* Core's `AgentChannels` has a built-in gateway loop, but the provider opts out
|
|
123643
|
+
* of it (`gateway: false` on the adapter entry) and runs this one instead,
|
|
123644
|
+
* because the core loop has two failure modes that combine into a connect
|
|
123645
|
+
* storm — severe enough that Discord's abuse tripwire (>1000 connects in a
|
|
123646
|
+
* short window) force-resets the bot token:
|
|
123647
|
+
*
|
|
123648
|
+
* - `@chat-adapter/discord` **swallows `client.login()` errors**: a session
|
|
123649
|
+
* that fails IDENTIFY (invalid/reset token, the MessageContent privileged
|
|
123650
|
+
* intent not enabled on the app) resolves "cleanly" within milliseconds.
|
|
123651
|
+
* - The core loop reconnects **immediately** on a clean session end and keeps
|
|
123652
|
+
* **no abort handle**, so a failing session becomes a zero-delay IDENTIFY
|
|
123653
|
+
* loop that nothing can stop, and every credential rotation leaks another
|
|
123654
|
+
* immortal loop still hammering with the stale token.
|
|
123655
|
+
*
|
|
123656
|
+
* This loop defends against both: a session that ends before
|
|
123657
|
+
* {@link HEALTHY_SESSION_MS} is treated as a failed connect and backed off
|
|
123658
|
+
* exponentially; a REST token check distinguishes a revoked token (park the
|
|
123659
|
+
* loop until `configure()` delivers new credentials and restarts it) from a
|
|
123660
|
+
* transient failure (keep backing off); and the `AbortSignal` lets the
|
|
123661
|
+
* provider kill the loop on disconnect, credential rotation, or re-attach.
|
|
123662
|
+
* This mirrors how `@mastra/telegram` owns its polling loop (`stopPolling()`
|
|
123663
|
+
* on reconfigure/disconnect, capped backoff on errors).
|
|
123664
|
+
*/
|
|
123665
|
+
/** How long each Gateway session runs before a scheduled reconnect. */
|
|
123666
|
+
const GATEWAY_SESSION_MS = 1440 * 60 * 1e3;
|
|
123667
|
+
/**
|
|
123668
|
+
* A session that ends sooner than this is treated as a failed connect. The
|
|
123669
|
+
* adapter resolves login failures as if the session ended cleanly, so elapsed
|
|
123670
|
+
* time is the only signal that distinguishes "ran fine until the duration
|
|
123671
|
+
* elapsed / Discord closed us" from "IDENTIFY was rejected instantly".
|
|
123672
|
+
*/
|
|
123673
|
+
const HEALTHY_SESSION_MS = 60 * 1e3;
|
|
123674
|
+
/** First retry delay after a failed connect; doubles up to {@link MAX_BACKOFF_MS}. */
|
|
123675
|
+
const INITIAL_BACKOFF_MS = 5 * 1e3;
|
|
123676
|
+
/** Retry delay ceiling. */
|
|
123677
|
+
const MAX_BACKOFF_MS = 900 * 1e3;
|
|
123678
|
+
/** Pause between healthy sessions — a scheduled rollover, not a failure. */
|
|
123679
|
+
const RECONNECT_DELAY_MS = 1e3;
|
|
123680
|
+
/** After this many consecutive failed connects, log a diagnostic hint. */
|
|
123681
|
+
const SHORT_SESSION_HINT_THRESHOLD = 3;
|
|
123682
|
+
/**
|
|
123683
|
+
* Run Gateway sessions back-to-back until `signal` aborts or the bot token is
|
|
123684
|
+
* confirmed revoked. Resolves when the loop stops; never rejects.
|
|
123685
|
+
*/
|
|
123686
|
+
async function runGatewayLoop(deps, signal) {
|
|
123687
|
+
const sessionMs = deps.sessionMs ?? 864e5;
|
|
123688
|
+
let backoffMs = INITIAL_BACKOFF_MS;
|
|
123689
|
+
let consecutiveFailures = 0;
|
|
123690
|
+
while (!signal.aborted) {
|
|
123691
|
+
const startedAt = Date.now();
|
|
123692
|
+
try {
|
|
123693
|
+
let settle;
|
|
123694
|
+
let fail;
|
|
123695
|
+
const done = new Promise((resolve, reject) => {
|
|
123696
|
+
settle = resolve;
|
|
123697
|
+
fail = reject;
|
|
123698
|
+
});
|
|
123699
|
+
let sessionDelivered = false;
|
|
123700
|
+
const response = await deps.startSession({ waitUntil: (promise) => {
|
|
123701
|
+
sessionDelivered = true;
|
|
123702
|
+
promise.then(() => settle(), (err) => fail(err));
|
|
123703
|
+
} }, sessionMs, signal);
|
|
123704
|
+
if (!response.ok || !sessionDelivered) throw new Error(`Gateway session refused (status ${response.status})`);
|
|
123705
|
+
await done;
|
|
123706
|
+
} catch (err) {
|
|
123707
|
+
deps.log("error", `Gateway session error: ${err instanceof Error ? err.message : String(err)}`);
|
|
123708
|
+
}
|
|
123709
|
+
if (signal.aborted) return;
|
|
123710
|
+
const elapsedMs = Date.now() - startedAt;
|
|
123711
|
+
if (elapsedMs >= 6e4) {
|
|
123712
|
+
backoffMs = INITIAL_BACKOFF_MS;
|
|
123713
|
+
consecutiveFailures = 0;
|
|
123714
|
+
deps.log("info", "Gateway session ended; reconnecting");
|
|
123715
|
+
await sleep(RECONNECT_DELAY_MS, signal);
|
|
123716
|
+
continue;
|
|
123717
|
+
}
|
|
123718
|
+
consecutiveFailures += 1;
|
|
123719
|
+
let check;
|
|
123720
|
+
try {
|
|
123721
|
+
check = await deps.checkToken();
|
|
123722
|
+
} catch {
|
|
123723
|
+
check = "unreachable";
|
|
123724
|
+
}
|
|
123725
|
+
if (signal.aborted) return;
|
|
123726
|
+
if (check === "invalid") {
|
|
123727
|
+
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.");
|
|
123728
|
+
return;
|
|
123729
|
+
}
|
|
123730
|
+
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.`);
|
|
123731
|
+
deps.log("warn", `Gateway session ended after ${Math.round(elapsedMs / 1e3)}s; retrying in ${Math.round(backoffMs / 1e3)}s`);
|
|
123732
|
+
await sleep(backoffMs, signal);
|
|
123733
|
+
backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
|
|
123734
|
+
}
|
|
123735
|
+
}
|
|
123736
|
+
/** Abortable sleep: resolves early (never rejects) when the signal fires. */
|
|
123737
|
+
function sleep(ms, signal) {
|
|
123738
|
+
if (signal.aborted) return Promise.resolve();
|
|
123739
|
+
return new Promise((resolve) => {
|
|
123740
|
+
const onAbort = () => {
|
|
123741
|
+
clearTimeout(timer);
|
|
123742
|
+
resolve();
|
|
123743
|
+
};
|
|
123744
|
+
const timer = setTimeout(() => {
|
|
123745
|
+
signal.removeEventListener("abort", onAbort);
|
|
123746
|
+
resolve();
|
|
123747
|
+
}, ms);
|
|
123748
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
123749
|
+
});
|
|
123750
|
+
}
|
|
123751
|
+
//#endregion
|
|
123638
123752
|
//#region src/crypto.ts
|
|
123639
123753
|
/**
|
|
123640
123754
|
* Opt-in AES-256-GCM encryption for the app's bot token at rest, with
|
|
@@ -123931,8 +124045,10 @@ function planReconcile(input) {
|
|
|
123931
124045
|
* Resolve the per-adapter config the provider applies to the Discord entry in
|
|
123932
124046
|
* `AgentChannels.adapters`. Discord has native embeds + action-row buttons, so
|
|
123933
124047
|
* `toolDisplay` defaults to `'cards'` (unlike Telegram's `'text'`); `streaming`
|
|
123934
|
-
* post-and-edits the interaction followup; `gateway` (default `true`) makes
|
|
123935
|
-
*
|
|
124048
|
+
* post-and-edits the interaction followup; `gateway` (default `true`) makes the
|
|
124049
|
+
* **provider** run the DM/mention Gateway reconnection loop. (The adapter entry
|
|
124050
|
+
* handed to `AgentChannels` always carries `gateway: false` — core's loop must
|
|
124051
|
+
* never start; see `#createAgentChannels`.)
|
|
123936
124052
|
*/
|
|
123937
124053
|
function resolveDiscordAdapterConfig(config) {
|
|
123938
124054
|
return {
|
|
@@ -123956,11 +124072,15 @@ function resolveDiscordAdapterConfig(config) {
|
|
|
123956
124072
|
* member) or lazily off the first inbound interaction's authoritative `guild_id`
|
|
123957
124073
|
* (see {@link activateGuild}).
|
|
123958
124074
|
*
|
|
123959
|
-
* **Gateway is
|
|
123960
|
-
* HTTP Interactions (PING, slash commands, buttons). DMs / @mentions /
|
|
123961
|
-
* arrive over the Gateway WebSocket, whose reconnection loop
|
|
123962
|
-
*
|
|
123963
|
-
*
|
|
124075
|
+
* **Gateway is the provider's job.** The mounted interactions route only ever
|
|
124076
|
+
* receives HTTP Interactions (PING, slash commands, buttons). DMs / @mentions /
|
|
124077
|
+
* reactions arrive over the Gateway WebSocket, whose reconnection loop the
|
|
124078
|
+
* provider owns ({@link #startGatewayLoop} → `./gateway-loop`): one abortable
|
|
124079
|
+
* loop per installation, exponential backoff on failed connects, parked on a
|
|
124080
|
+
* revoked token. Core's `AgentChannels` loop is explicitly disabled
|
|
124081
|
+
* (`gateway: false` on the adapter entry) — it reconnects with no backoff and
|
|
124082
|
+
* cannot be stopped, which is how a failing IDENTIFY once tripped Discord's
|
|
124083
|
+
* 1000-connect abuse limit and got the bot token force-reset.
|
|
123964
124084
|
*
|
|
123965
124085
|
* Implemented (issues `mastra-discord-13x.2` + `.3`): the app-config +
|
|
123966
124086
|
* guild-keyed install store, `connect()`/`disconnect()`, OAuth2 invite-URL
|
|
@@ -123974,6 +124094,15 @@ var DiscordProvider = class {
|
|
|
123974
124094
|
#store;
|
|
123975
124095
|
/** Live adapters, keyed by installation id (one per agent; all share the app's bot token). */
|
|
123976
124096
|
#adapters = /* @__PURE__ */ new Map();
|
|
124097
|
+
/**
|
|
124098
|
+
* Live Gateway reconnect loops, keyed by installation id. Aborting a
|
|
124099
|
+
* controller ends the loop and destroys its discord.js client. Exactly one
|
|
124100
|
+
* loop may exist per installation — {@link #startGatewayLoop} aborts any
|
|
124101
|
+
* predecessor, so credential rotations and adapter rebuilds can't leak loops
|
|
124102
|
+
* that keep IDENTIFYing with stale tokens (Discord force-resets the bot
|
|
124103
|
+
* token past ~1000 connects in a short window).
|
|
124104
|
+
*/
|
|
124105
|
+
#gatewayLoops = /* @__PURE__ */ new Map();
|
|
123977
124106
|
/** Cached sync view of whether the app is configured (for {@link getInfo}). */
|
|
123978
124107
|
#configured = false;
|
|
123979
124108
|
#initPromise = null;
|
|
@@ -124020,6 +124149,7 @@ var DiscordProvider = class {
|
|
|
124020
124149
|
this.#storeIsFallback = false;
|
|
124021
124150
|
}
|
|
124022
124151
|
if (isNewInstance) {
|
|
124152
|
+
this.#stopAllGatewayLoops();
|
|
124023
124153
|
this.#adapters.clear();
|
|
124024
124154
|
this.#configured = this.#hasSuppliedCredentials();
|
|
124025
124155
|
}
|
|
@@ -124117,6 +124247,7 @@ var DiscordProvider = class {
|
|
|
124117
124247
|
app: void 0
|
|
124118
124248
|
};
|
|
124119
124249
|
this.#configured = false;
|
|
124250
|
+
this.#stopAllGatewayLoops();
|
|
124120
124251
|
this.#adapters.clear();
|
|
124121
124252
|
this.#initPromise = null;
|
|
124122
124253
|
return;
|
|
@@ -124152,6 +124283,7 @@ var DiscordProvider = class {
|
|
|
124152
124283
|
if (merged.botToken && merged.publicKey && merged.applicationId) await store.saveAppConfig(merged);
|
|
124153
124284
|
}
|
|
124154
124285
|
const wasInitialized = this.#initPromise !== null;
|
|
124286
|
+
this.#stopAllGatewayLoops();
|
|
124155
124287
|
this.#adapters.clear();
|
|
124156
124288
|
this.#initPromise = null;
|
|
124157
124289
|
if (wasInitialized) await this.initialize();
|
|
@@ -124309,19 +124441,15 @@ var DiscordProvider = class {
|
|
|
124309
124441
|
return installation;
|
|
124310
124442
|
}
|
|
124311
124443
|
/**
|
|
124312
|
-
* Disconnect an agent from Discord:
|
|
124313
|
-
* adapter entry.
|
|
124314
|
-
*
|
|
124315
|
-
* **Limitation:** the Gateway loop is owned by core (it calls
|
|
124316
|
-
* `startGatewayListener` itself; there is no `stopGatewayListener`), so
|
|
124317
|
-
* disconnect cannot kill an in-flight gateway window — it lapses at the next
|
|
124318
|
-
* duration boundary. Contrast Telegram's clean `stopPolling()`.
|
|
124444
|
+
* Disconnect an agent from Discord: abort its Gateway loop, remove its
|
|
124445
|
+
* installation row, and drop the adapter entry.
|
|
124319
124446
|
*/
|
|
124320
124447
|
async disconnect(agentId) {
|
|
124321
124448
|
const store = await this.#getStore();
|
|
124322
124449
|
const existing = await store.getByAgent(agentId);
|
|
124323
124450
|
if (!existing) throw new Error(`No Discord installation found for agent "${agentId}"`);
|
|
124324
124451
|
await this.#enqueueWebhookTask(existing.webhookId, async () => {
|
|
124452
|
+
this.#stopGatewayLoop(existing.id);
|
|
124325
124453
|
this.#adapters.delete(existing.id);
|
|
124326
124454
|
await store.deleteByAgent(agentId);
|
|
124327
124455
|
});
|
|
@@ -124455,6 +124583,7 @@ var DiscordProvider = class {
|
|
|
124455
124583
|
if (!channels || channels.adapters["discord"] !== adapter) {
|
|
124456
124584
|
channels = this.#createAgentChannels(agent, adapter);
|
|
124457
124585
|
await channels.initialize(this.#mastra);
|
|
124586
|
+
this.#startGatewayLoop(installation, adapter, app);
|
|
124458
124587
|
}
|
|
124459
124588
|
try {
|
|
124460
124589
|
return scheduleActivation(await channels.handleWebhookEvent(PLATFORM, c.req.raw, waitUntil ? { waitUntil } : void 0));
|
|
@@ -124531,13 +124660,64 @@ var DiscordProvider = class {
|
|
|
124531
124660
|
if (!app) return;
|
|
124532
124661
|
const agent = this.#resolveAgent(installation.agentId);
|
|
124533
124662
|
const adapter = this.#getOrCreateAdapter(installation, app);
|
|
124534
|
-
if (agent && this.#mastra)
|
|
124663
|
+
if (agent && this.#mastra) {
|
|
124664
|
+
await this.#createAgentChannels(agent, adapter).initialize(this.#mastra);
|
|
124665
|
+
this.#startGatewayLoop(installation, adapter, app);
|
|
124666
|
+
}
|
|
124667
|
+
}
|
|
124668
|
+
/**
|
|
124669
|
+
* Start (or restart) the Gateway reconnect loop for an installation. Any
|
|
124670
|
+
* previous loop for the same installation is aborted first, so adapter
|
|
124671
|
+
* rebuilds and credential rotations replace the loop instead of stacking
|
|
124672
|
+
* another one. No-op when the provider is configured `gateway: false`.
|
|
124673
|
+
*
|
|
124674
|
+
* The loop itself lives in {@link runGatewayLoop}; see that module for why
|
|
124675
|
+
* the provider owns this instead of core's `AgentChannels` loop.
|
|
124676
|
+
*/
|
|
124677
|
+
#startGatewayLoop(installation, adapter, app) {
|
|
124678
|
+
if (!resolveDiscordAdapterConfig(this.#config).gateway) return;
|
|
124679
|
+
this.#stopGatewayLoop(installation.id);
|
|
124680
|
+
const controller = new AbortController();
|
|
124681
|
+
this.#gatewayLoops.set(installation.id, controller);
|
|
124682
|
+
const apiBaseUrl = this.#apiBaseUrl();
|
|
124683
|
+
runGatewayLoop({
|
|
124684
|
+
startSession: (options, durationMs, signal) => adapter.startGatewayListener(options, durationMs, signal),
|
|
124685
|
+
checkToken: async () => {
|
|
124686
|
+
try {
|
|
124687
|
+
const response = await discordRequest(app.botToken, "GET", "/applications/@me", apiBaseUrl);
|
|
124688
|
+
return response.status === 401 ? "invalid" : response.ok ? "valid" : "unreachable";
|
|
124689
|
+
} catch {
|
|
124690
|
+
return "unreachable";
|
|
124691
|
+
}
|
|
124692
|
+
},
|
|
124693
|
+
log: (level, message) => {
|
|
124694
|
+
const line = `[Discord] [gateway:${installation.agentId}] ${message}`;
|
|
124695
|
+
if (level === "error") console.error(line);
|
|
124696
|
+
else if (level === "warn") console.warn(line);
|
|
124697
|
+
else console.info(line);
|
|
124698
|
+
}
|
|
124699
|
+
}, controller.signal).finally(() => {
|
|
124700
|
+
if (this.#gatewayLoops.get(installation.id) === controller) this.#gatewayLoops.delete(installation.id);
|
|
124701
|
+
});
|
|
124702
|
+
}
|
|
124703
|
+
/** Abort an installation's Gateway loop (ends the session, destroys the client). */
|
|
124704
|
+
#stopGatewayLoop(installationId) {
|
|
124705
|
+
const controller = this.#gatewayLoops.get(installationId);
|
|
124706
|
+
if (!controller) return;
|
|
124707
|
+
this.#gatewayLoops.delete(installationId);
|
|
124708
|
+
controller.abort();
|
|
124709
|
+
}
|
|
124710
|
+
/** Abort every Gateway loop — credential revocation or Mastra re-attach. */
|
|
124711
|
+
#stopAllGatewayLoops() {
|
|
124712
|
+
for (const installationId of [...this.#gatewayLoops.keys()]) this.#stopGatewayLoop(installationId);
|
|
124535
124713
|
}
|
|
124536
124714
|
/**
|
|
124537
124715
|
* Create AgentChannels for an agent with the Discord adapter, preserving any
|
|
124538
124716
|
* adapters/config the agent author already configured (mirrors `@mastra/slack`
|
|
124539
|
-
* / `@mastra/telegram`). `gateway:
|
|
124540
|
-
*
|
|
124717
|
+
* / `@mastra/telegram`). The entry always carries `gateway: false`: core's
|
|
124718
|
+
* built-in gateway loop reconnects with no backoff and no abort handle, so
|
|
124719
|
+
* the provider owns the Gateway lifecycle instead ({@link #startGatewayLoop},
|
|
124720
|
+
* gated on the provider-level `gateway` config).
|
|
124541
124721
|
*/
|
|
124542
124722
|
#createAgentChannels(agent, adapter) {
|
|
124543
124723
|
const existingConfig = agent.getChannels()?.channelConfig;
|
|
@@ -124545,6 +124725,7 @@ var DiscordProvider = class {
|
|
|
124545
124725
|
const entry = {
|
|
124546
124726
|
adapter,
|
|
124547
124727
|
...resolveDiscordAdapterConfig(cfg),
|
|
124728
|
+
gateway: false,
|
|
124548
124729
|
...cfg.cors !== void 0 ? { cors: cfg.cors } : {},
|
|
124549
124730
|
...cfg.formatError !== void 0 ? { formatError: cfg.formatError } : {}
|
|
124550
124731
|
};
|