agent-messenger 2.27.0 → 2.27.2

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 (37) 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/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  12. package/dist/src/platforms/kakaotalk/client.js +4 -4
  13. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  14. package/package.json +1 -1
  15. package/skills/agent-channeltalk/SKILL.md +1 -1
  16. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  17. package/skills/agent-discord/SKILL.md +1 -1
  18. package/skills/agent-discordbot/SKILL.md +1 -1
  19. package/skills/agent-instagram/SKILL.md +1 -1
  20. package/skills/agent-kakaotalk/SKILL.md +1 -1
  21. package/skills/agent-line/SKILL.md +1 -1
  22. package/skills/agent-slack/SKILL.md +1 -1
  23. package/skills/agent-slackbot/SKILL.md +1 -1
  24. package/skills/agent-teams/SKILL.md +1 -1
  25. package/skills/agent-telegram/SKILL.md +1 -1
  26. package/skills/agent-telegrambot/SKILL.md +1 -1
  27. package/skills/agent-webex/SKILL.md +1 -1
  28. package/skills/agent-webexbot/SKILL.md +1 -1
  29. package/skills/agent-wechatbot/SKILL.md +1 -1
  30. package/skills/agent-whatsapp/SKILL.md +1 -1
  31. package/skills/agent-whatsappbot/SKILL.md +1 -1
  32. package/src/platforms/discord/listener.test.ts +175 -143
  33. package/src/platforms/discord/listener.ts +87 -3
  34. package/src/platforms/discordbot/listener.test.ts +186 -184
  35. package/src/platforms/discordbot/listener.ts +83 -2
  36. package/src/platforms/kakaotalk/client.test.ts +40 -0
  37. package/src/platforms/kakaotalk/client.ts +6 -4
@@ -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
  // User (non-bot) gateway capabilities bitmask, mirroring discord.py-self's default set.
17
22
  // Capabilities shape the READY payload; bit 10 (client_state_v2) requires client_state.guild_versions.
18
23
  const USER_GATEWAY_CAPABILITIES = 16381
@@ -30,8 +35,13 @@ const USER_GATEWAY_BUILD_NUMBER = Number(process.env.AGENT_DISCORD_BUILD_NUMBER)
30
35
 
31
36
  type EventKey = keyof DiscordListenerEventMap
32
37
 
38
+ export interface DiscordListenerOptions {
39
+ connectTimeoutMs?: number
40
+ }
41
+
33
42
  export class DiscordListener {
34
43
  private client: DiscordClient
44
+ private connectTimeoutMs: number
35
45
  private running = false
36
46
  private ws: WebSocket | null = null
37
47
  private emitter = new EventEmitter()
@@ -40,6 +50,7 @@ export class DiscordListener {
40
50
  private heartbeatJitterTimer: ReturnType<typeof setTimeout> | null = null
41
51
  private invalidSessionTimer: ReturnType<typeof setTimeout> | null = null
42
52
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null
53
+ private connectTimeoutTimer: ReturnType<typeof setTimeout> | null = null
43
54
  private reconnectAttempts = 0
44
55
  private sequence: number | null = null
45
56
  private sessionId: string | null = null
@@ -47,23 +58,90 @@ export class DiscordListener {
47
58
  private token: string | null = null
48
59
  private cachedUser: { id: string; username: string } | null = null
49
60
  private generation = 0
61
+ private startPromise: Promise<void> | null = null
62
+ private pendingStartReject: ((error: Error) => void) | null = null
50
63
 
51
- constructor(client: DiscordClient) {
64
+ constructor(client: DiscordClient, options?: DiscordListenerOptions) {
52
65
  this.client = client
66
+ this.connectTimeoutMs = options?.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS
53
67
  }
54
68
 
55
69
  async start(): Promise<void> {
70
+ if (this.startPromise) return this.startPromise
56
71
  if (this.running) return
72
+
57
73
  this.running = true
58
74
  this.reconnectAttempts = 0
59
- this.generation++
60
- await this.connect(this.generation)
75
+ const generation = ++this.generation
76
+
77
+ const ready = new Promise<void>((resolve, reject) => {
78
+ let settled = false
79
+
80
+ const cleanup = () => {
81
+ this.emitter.off('connected', onConnected)
82
+ this.emitter.off('error', onError)
83
+ this.pendingStartReject = null
84
+ if (this.connectTimeoutTimer) {
85
+ clearTimeout(this.connectTimeoutTimer)
86
+ this.connectTimeoutTimer = null
87
+ }
88
+ }
89
+
90
+ const onConnected = () => {
91
+ if (settled || !this.isCurrent(generation)) return
92
+ settled = true
93
+ cleanup()
94
+ resolve()
95
+ }
96
+
97
+ const onError = (error: Error) => {
98
+ if (settled || !this.isCurrent(generation)) return
99
+ settled = true
100
+ cleanup()
101
+ this.teardownFailedStart(generation)
102
+ reject(error)
103
+ }
104
+
105
+ this.emitter.once('connected', onConnected)
106
+ this.emitter.once('error', onError)
107
+
108
+ // Generation-agnostic on purpose: stop() invokes this to reject an in-flight start
109
+ // (after bumping generation) without leaking the once() handlers.
110
+ this.pendingStartReject = (error: Error) => {
111
+ if (settled) return
112
+ settled = true
113
+ cleanup()
114
+ reject(error)
115
+ }
116
+
117
+ this.connectTimeoutTimer = setTimeout(() => {
118
+ onError(new Error(`Discord gateway did not become ready within ${this.connectTimeoutMs}ms`))
119
+ }, this.connectTimeoutMs)
120
+ })
121
+
122
+ const run = async (): Promise<void> => {
123
+ try {
124
+ await Promise.all([this.connect(generation), ready])
125
+ } finally {
126
+ if (this.generation === generation) this.startPromise = null
127
+ }
128
+ }
129
+ const startPromise = run()
130
+ this.startPromise = startPromise
131
+ return startPromise
61
132
  }
62
133
 
63
134
  stop(): void {
64
135
  this.running = false
65
136
  this.generation++
66
137
  this.clearTimers()
138
+ if (this.connectTimeoutTimer) {
139
+ clearTimeout(this.connectTimeoutTimer)
140
+ this.connectTimeoutTimer = null
141
+ }
142
+ const rejectStart = this.pendingStartReject
143
+ this.pendingStartReject = null
144
+ this.startPromise = null
67
145
  if (this.ws) {
68
146
  this.ws.close()
69
147
  this.ws = null
@@ -73,6 +151,12 @@ export class DiscordListener {
73
151
  this.resumeGatewayUrl = null
74
152
  this.token = null
75
153
  this.cachedUser = null
154
+ rejectStart?.(new Error('Discord gateway start was stopped before becoming ready'))
155
+ }
156
+
157
+ private teardownFailedStart(generation: number): void {
158
+ if (!this.isCurrent(generation)) return
159
+ this.stop()
76
160
  }
77
161
 
78
162
  on<K extends EventKey>(event: K, listener: (...args: DiscordListenerEventMap[K]) => void): this {