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
  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 {
@@ -540,6 +540,46 @@ describe('KakaoTalkClient', () => {
540
540
  client.close()
541
541
  })
542
542
 
543
+ it('normalizes Long-like chat ids from LCHATLIST pages', async () => {
544
+ const loginResult = {
545
+ ...DEFAULT_LOGIN_RESULT,
546
+ eof: false,
547
+ }
548
+ mockLogin.mockResolvedValue(loginResult)
549
+
550
+ mockGetChatList.mockResolvedValueOnce({
551
+ body: {
552
+ chatDatas: [
553
+ {
554
+ c: makeLong(300),
555
+ t: 1,
556
+ k: ['Dave'],
557
+ i: [4],
558
+ a: 1,
559
+ n: 0,
560
+ o: 1698000000,
561
+ l: { authorId: 4, message: 'from paginated chat', sendAt: 1698000000 },
562
+ ll: makeLong(100),
563
+ },
564
+ ],
565
+ lastTokenId: makeLong(1),
566
+ lastChatId: makeLong(300),
567
+ eof: true,
568
+ },
569
+ })
570
+
571
+ const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
572
+ const chats = await client.getChats({ all: true })
573
+
574
+ const paginatedChat = chats.find((chat) => chat.chat_id === '300')
575
+ expect(paginatedChat?.chat_id).toBe('300')
576
+ expect(paginatedChat?.display_name).toBe('Dave')
577
+ expect(paginatedChat?.last_message?.author_name).toBe('Dave')
578
+ expect(chats.some((chat) => chat.chat_id === '[object Object]')).toBe(false)
579
+
580
+ client.close()
581
+ })
582
+
543
583
  it('wraps errors as KakaoTalkError', async () => {
544
584
  mockLogin.mockRejectedValue(new Error('Connection refused'))
545
585
 
@@ -62,7 +62,7 @@ class MemberNameCache {
62
62
  const names = chat.k as string[] | undefined
63
63
  if (!Array.isArray(ids) || !Array.isArray(names)) continue
64
64
 
65
- const chatId = String(chat.c)
65
+ const chatId = longToString(chat.c)
66
66
  let map = this.byChatId.get(chatId)
67
67
  if (!map) {
68
68
  map = new Map()
@@ -176,7 +176,7 @@ function formatChat(chat: ChatData, title: string | null, nameCache: MemberNameC
176
176
  const memberNames = (chat.k ?? []) as string[]
177
177
  const lastLog = chat.l as Record<string, unknown> | null
178
178
  const displayName = memberNames.join(', ') || null
179
- const chatId = String(chat.c)
179
+ const chatId = longToString(chat.c)
180
180
 
181
181
  return {
182
182
  chat_id: chatId,
@@ -256,7 +256,7 @@ function findMaxLogId(logs: Array<Record<string, unknown>>, field: string): Long
256
256
 
257
257
  function collectChats(chatDatas: ChatData[], into: ChatData[], seen: Set<string>): void {
258
258
  for (const chat of chatDatas) {
259
- const id = String(chat.c)
259
+ const id = longToString(chat.c)
260
260
  if (!seen.has(id)) {
261
261
  seen.add(id)
262
262
  into.push(chat)
@@ -771,7 +771,9 @@ export class KakaoTalkClient {
771
771
  }
772
772
 
773
773
  const titles = options?.resolveTitles
774
- ? await Promise.all(results.map((chat) => this.fetchChatTitle(session, parseLong(String(chat.c)), chat)))
774
+ ? await Promise.all(
775
+ results.map((chat) => this.fetchChatTitle(session, parseLong(longToString(chat.c)), chat)),
776
+ )
775
777
  : null
776
778
 
777
779
  return results.map((chat, i) => formatChat(chat, titles ? titles[i] : null, this.nameCache))