agent-messenger 2.27.0 → 2.27.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.
Files changed (32) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/package.json +1 -1
  3. package/dist/src/platforms/discord/listener.d.ts +9 -1
  4. package/dist/src/platforms/discord/listener.d.ts.map +1 -1
  5. package/dist/src/platforms/discord/listener.js +79 -3
  6. package/dist/src/platforms/discord/listener.js.map +1 -1
  7. package/dist/src/platforms/discordbot/listener.d.ts +6 -0
  8. package/dist/src/platforms/discordbot/listener.d.ts.map +1 -1
  9. package/dist/src/platforms/discordbot/listener.js +78 -2
  10. package/dist/src/platforms/discordbot/listener.js.map +1 -1
  11. package/package.json +1 -1
  12. package/skills/agent-channeltalk/SKILL.md +1 -1
  13. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  14. package/skills/agent-discord/SKILL.md +1 -1
  15. package/skills/agent-discordbot/SKILL.md +1 -1
  16. package/skills/agent-instagram/SKILL.md +1 -1
  17. package/skills/agent-kakaotalk/SKILL.md +1 -1
  18. package/skills/agent-line/SKILL.md +1 -1
  19. package/skills/agent-slack/SKILL.md +1 -1
  20. package/skills/agent-slackbot/SKILL.md +1 -1
  21. package/skills/agent-teams/SKILL.md +1 -1
  22. package/skills/agent-telegram/SKILL.md +1 -1
  23. package/skills/agent-telegrambot/SKILL.md +1 -1
  24. package/skills/agent-webex/SKILL.md +1 -1
  25. package/skills/agent-webexbot/SKILL.md +1 -1
  26. package/skills/agent-wechatbot/SKILL.md +1 -1
  27. package/skills/agent-whatsapp/SKILL.md +1 -1
  28. package/skills/agent-whatsappbot/SKILL.md +1 -1
  29. package/src/platforms/discord/listener.test.ts +175 -143
  30. package/src/platforms/discord/listener.ts +87 -3
  31. package/src/platforms/discordbot/listener.test.ts +186 -184
  32. package/src/platforms/discordbot/listener.ts +83 -2
@@ -13,6 +13,11 @@ const RECONNECT_MAX_DELAY = 30_000
13
13
  const NON_RECOVERABLE_CLOSE_CODES = [4004, 4010, 4011, 4012, 4013, 4014]
14
14
  const SESSION_RESET_CLOSE_CODES = [4007, 4009]
15
15
 
16
+ // Hello arrives within milliseconds and READY within a few seconds on a healthy gateway;
17
+ // 15s is generous for a successful handshake while still failing fast on bad tokens,
18
+ // network stalls, or a gateway that accepts the socket but never delivers READY.
19
+ const DEFAULT_CONNECT_TIMEOUT_MS = 15_000
20
+
16
21
  const DEFAULT_INTENTS =
17
22
  DiscordIntent.Guilds |
18
23
  DiscordIntent.GuildMessages |
@@ -26,11 +31,13 @@ type EventKey = keyof DiscordBotListenerEventMap
26
31
 
27
32
  export interface DiscordBotListenerOptions {
28
33
  intents?: number
34
+ connectTimeoutMs?: number
29
35
  }
30
36
 
31
37
  export class DiscordBotListener {
32
38
  private client: DiscordBotClient
33
39
  private intents: number
40
+ private connectTimeoutMs: number
34
41
  private running = false
35
42
  private ws: WebSocket | null = null
36
43
  private emitter = new EventEmitter()
@@ -39,6 +46,7 @@ export class DiscordBotListener {
39
46
  private heartbeatJitterTimer: ReturnType<typeof setTimeout> | null = null
40
47
  private invalidSessionTimer: ReturnType<typeof setTimeout> | null = null
41
48
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null
49
+ private connectTimeoutTimer: ReturnType<typeof setTimeout> | null = null
42
50
  private reconnectAttempts = 0
43
51
  private sequence: number | null = null
44
52
  private sessionId: string | null = null
@@ -46,24 +54,91 @@ export class DiscordBotListener {
46
54
  private token: string | null = null
47
55
  private cachedUser: { id: string; username: string } | null = null
48
56
  private generation = 0
57
+ private startPromise: Promise<void> | null = null
58
+ private pendingStartReject: ((error: Error) => void) | null = null
49
59
 
50
60
  constructor(client: DiscordBotClient, options?: DiscordBotListenerOptions) {
51
61
  this.client = client
52
62
  this.intents = options?.intents ?? DEFAULT_INTENTS
63
+ this.connectTimeoutMs = options?.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS
53
64
  }
54
65
 
55
66
  async start(): Promise<void> {
67
+ if (this.startPromise) return this.startPromise
56
68
  if (this.running) return
69
+
57
70
  this.running = true
58
71
  this.reconnectAttempts = 0
59
- this.generation++
60
- await this.connect(this.generation)
72
+ const generation = ++this.generation
73
+
74
+ const ready = new Promise<void>((resolve, reject) => {
75
+ let settled = false
76
+
77
+ const cleanup = () => {
78
+ this.emitter.off('connected', onConnected)
79
+ this.emitter.off('error', onError)
80
+ this.pendingStartReject = null
81
+ if (this.connectTimeoutTimer) {
82
+ clearTimeout(this.connectTimeoutTimer)
83
+ this.connectTimeoutTimer = null
84
+ }
85
+ }
86
+
87
+ const onConnected = () => {
88
+ if (settled || !this.isCurrent(generation)) return
89
+ settled = true
90
+ cleanup()
91
+ resolve()
92
+ }
93
+
94
+ const onError = (error: Error) => {
95
+ if (settled || !this.isCurrent(generation)) return
96
+ settled = true
97
+ cleanup()
98
+ this.teardownFailedStart(generation)
99
+ reject(error)
100
+ }
101
+
102
+ this.emitter.once('connected', onConnected)
103
+ this.emitter.once('error', onError)
104
+
105
+ // Generation-agnostic on purpose: stop() invokes this to reject an in-flight start
106
+ // (after bumping generation) without leaking the once() handlers.
107
+ this.pendingStartReject = (error: Error) => {
108
+ if (settled) return
109
+ settled = true
110
+ cleanup()
111
+ reject(error)
112
+ }
113
+
114
+ this.connectTimeoutTimer = setTimeout(() => {
115
+ onError(new Error(`Discord gateway did not become ready within ${this.connectTimeoutMs}ms`))
116
+ }, this.connectTimeoutMs)
117
+ })
118
+
119
+ const run = async (): Promise<void> => {
120
+ try {
121
+ await Promise.all([this.connect(generation), ready])
122
+ } finally {
123
+ if (this.generation === generation) this.startPromise = null
124
+ }
125
+ }
126
+ const startPromise = run()
127
+ this.startPromise = startPromise
128
+ return startPromise
61
129
  }
62
130
 
63
131
  stop(): void {
64
132
  this.running = false
65
133
  this.generation++
66
134
  this.clearTimers()
135
+ if (this.connectTimeoutTimer) {
136
+ clearTimeout(this.connectTimeoutTimer)
137
+ this.connectTimeoutTimer = null
138
+ }
139
+ const rejectStart = this.pendingStartReject
140
+ this.pendingStartReject = null
141
+ this.startPromise = null
67
142
  if (this.ws) {
68
143
  this.ws.close()
69
144
  this.ws = null
@@ -73,6 +148,12 @@ export class DiscordBotListener {
73
148
  this.resumeGatewayUrl = null
74
149
  this.token = null
75
150
  this.cachedUser = null
151
+ rejectStart?.(new Error('Discord gateway start was stopped before becoming ready'))
152
+ }
153
+
154
+ private teardownFailedStart(generation: number): void {
155
+ if (!this.isCurrent(generation)) return
156
+ this.stop()
76
157
  }
77
158
 
78
159
  on<K extends EventKey>(event: K, listener: (...args: DiscordBotListenerEventMap[K]) => void): this {