agent-messenger 2.12.1 → 2.12.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 (32) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/package.json +1 -1
  3. package/dist/src/platforms/kakaotalk/client.d.ts +22 -0
  4. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  5. package/dist/src/platforms/kakaotalk/client.js +93 -7
  6. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  7. package/dist/src/platforms/kakaotalk/listener.d.ts +4 -7
  8. package/dist/src/platforms/kakaotalk/listener.d.ts.map +1 -1
  9. package/dist/src/platforms/kakaotalk/listener.js +43 -72
  10. package/dist/src/platforms/kakaotalk/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-wechatbot/SKILL.md +1 -1
  26. package/skills/agent-whatsapp/SKILL.md +1 -1
  27. package/skills/agent-whatsappbot/SKILL.md +1 -1
  28. package/src/platforms/kakaotalk/client-listener-integration.test.ts +411 -0
  29. package/src/platforms/kakaotalk/client.test.ts +3 -0
  30. package/src/platforms/kakaotalk/client.ts +108 -9
  31. package/src/platforms/kakaotalk/listener.test.ts +155 -149
  32. package/src/platforms/kakaotalk/listener.ts +46 -80
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-slack
3
3
  description: Interact with Slack workspaces - send messages, read channels, manage reactions
4
- version: 2.12.1
4
+ version: 2.12.2
5
5
  allowed-tools: Bash(agent-slack:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-slackbot
3
3
  description: Interact with Slack workspaces using bot tokens - send messages, read channels, manage reactions
4
- version: 2.12.1
4
+ version: 2.12.2
5
5
  allowed-tools: Bash(agent-slackbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-teams
3
3
  description: Interact with Microsoft Teams - send messages, read channels, manage reactions
4
- version: 2.12.1
4
+ version: 2.12.2
5
5
  allowed-tools: Bash(agent-teams:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-telegram
3
3
  description: Interact with Telegram through TDLib - authenticate, inspect chats, and send messages
4
- version: 2.12.1
4
+ version: 2.12.2
5
5
  allowed-tools: Bash(agent-telegram:*)
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-telegrambot
3
3
  description: Interact with Telegram using bot tokens - send messages, read chats, manage reactions
4
- version: 2.12.1
4
+ version: 2.12.2
5
5
  allowed-tools: Bash(agent-telegrambot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-webex
3
3
  description: Interact with Cisco Webex - send messages, read spaces, manage memberships
4
- version: 2.12.1
4
+ version: 2.12.2
5
5
  allowed-tools: Bash(agent-webex:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-wechatbot
3
3
  description: Interact with WeChat Official Account using API credentials - send messages, manage templates, list followers
4
- version: 2.12.1
4
+ version: 2.12.2
5
5
  allowed-tools: Bash(agent-wechatbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-whatsapp
3
3
  description: Interact with WhatsApp - send messages, read chats, manage conversations
4
- version: 2.12.1
4
+ version: 2.12.2
5
5
  allowed-tools: Bash(agent-whatsapp:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-whatsappbot
3
3
  description: Interact with WhatsApp using Cloud API credentials - send messages, manage templates
4
- version: 2.12.1
4
+ version: 2.12.2
5
5
  allowed-tools: Bash(agent-whatsappbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -0,0 +1,411 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, it } from 'bun:test'
2
+
3
+ import { KakaoTalkClient } from './client'
4
+ import { KakaoTalkListener } from './listener'
5
+ import type { LocoPacket } from './protocol/types'
6
+ import type { KakaoTalkPushMessageEvent } from './types'
7
+
8
+ const sessions: MockLocoSession[] = []
9
+ const loginCalls: Array<{
10
+ oauthToken: string
11
+ userId: string
12
+ deviceUuid: string
13
+ syncState: unknown
14
+ deviceType: string
15
+ }> = []
16
+
17
+ class MockLocoSession {
18
+ pushHandler: ((packet: LocoPacket) => void) | null = null
19
+ closeHandler: (() => void) | null = null
20
+ closed = false
21
+ loginResult: Record<string, unknown> = {
22
+ chatDatas: [],
23
+ lastTokenId: { low: 0, high: 0 },
24
+ lastChatId: { low: 0, high: 0 },
25
+ eof: true,
26
+ }
27
+
28
+ loginImpl: (
29
+ oauthToken: string,
30
+ userId: string,
31
+ deviceUuid: string,
32
+ syncState: unknown,
33
+ deviceType: string,
34
+ ) => Promise<unknown> = async (oauthToken, userId, deviceUuid, syncState, deviceType) => {
35
+ loginCalls.push({ oauthToken, userId, deviceUuid, syncState, deviceType })
36
+ return this.loginResult
37
+ }
38
+
39
+ sendMessageImpl: (chatId: unknown, text: string) => Promise<unknown> = async () => ({
40
+ statusCode: 0,
41
+ body: { logId: { low: 1, high: 0 }, sendAt: 1 },
42
+ })
43
+
44
+ getChatLogsImpl: () => Promise<unknown> = async () => ({
45
+ body: { status: 0, chatLogs: [], eof: true },
46
+ })
47
+
48
+ constructor() {
49
+ sessions.push(this)
50
+ }
51
+
52
+ login(
53
+ oauthToken: string,
54
+ userId: string,
55
+ deviceUuid: string,
56
+ syncState: unknown,
57
+ deviceType: string,
58
+ ): Promise<unknown> {
59
+ return this.loginImpl(oauthToken, userId, deviceUuid, syncState, deviceType)
60
+ }
61
+
62
+ sendMessage(chatId: unknown, text: string): Promise<unknown> {
63
+ return this.sendMessageImpl(chatId, text)
64
+ }
65
+
66
+ getChatLogs(): Promise<unknown> {
67
+ return this.getChatLogsImpl()
68
+ }
69
+
70
+ getChatList(): Promise<unknown> {
71
+ return Promise.resolve({ body: { chatDatas: [], lastTokenId: { low: 0, high: 0 }, eof: true } })
72
+ }
73
+
74
+ getChatInfo(): Promise<unknown> {
75
+ return Promise.resolve({ body: { l: { low: 0, high: 0 } } })
76
+ }
77
+
78
+ syncMessages(): Promise<unknown> {
79
+ return Promise.resolve({ body: { chatLogs: [], isOK: true } })
80
+ }
81
+
82
+ onPush(handler: (packet: LocoPacket) => void): void {
83
+ this.pushHandler = handler
84
+ }
85
+
86
+ onClose(handler: () => void): void {
87
+ this.closeHandler = handler
88
+ }
89
+
90
+ close(): void {
91
+ if (this.closed) return
92
+ this.closed = true
93
+ this.closeHandler?.()
94
+ }
95
+
96
+ simulatePush(method: string, body: Record<string, unknown> = {}): void {
97
+ this.pushHandler?.({ packetId: 0, statusCode: 0, method, bodyType: 0, body })
98
+ }
99
+
100
+ simulateRemoteClose(): void {
101
+ if (this.closed) return
102
+ this.closed = true
103
+ this.closeHandler?.()
104
+ }
105
+ }
106
+
107
+ mock.module('./protocol/session', () => ({ LocoSession: MockLocoSession }))
108
+
109
+ const CREDS = {
110
+ oauthToken: 'tok',
111
+ userId: 'user1',
112
+ deviceUuid: 'device-uuid-1',
113
+ deviceType: 'tablet' as const,
114
+ }
115
+
116
+ function currentSession(): MockLocoSession {
117
+ return sessions[sessions.length - 1]!
118
+ }
119
+
120
+ describe('KakaoTalkClient + KakaoTalkListener integration (shared LOCO session)', () => {
121
+ beforeEach(() => {
122
+ sessions.length = 0
123
+ loginCalls.length = 0
124
+ })
125
+
126
+ afterEach(() => {
127
+ sessions.length = 0
128
+ loginCalls.length = 0
129
+ })
130
+
131
+ it('opens exactly ONE LocoSession when a client and listener are used together', async () => {
132
+ // given — a client logged in and a listener attached
133
+ const client = await new KakaoTalkClient().login(CREDS)
134
+ const listener = new KakaoTalkListener(client)
135
+
136
+ // when — listener starts AND client makes API calls
137
+ await listener.start()
138
+ await client.sendMessage('100', 'hi')
139
+ await client.getChats()
140
+
141
+ // then — only one LocoSession was constructed; only one LOGINLIST sent
142
+ expect(sessions.length).toBe(1)
143
+ expect(loginCalls.length).toBe(1)
144
+ expect(loginCalls[0].deviceUuid).toBe(CREDS.deviceUuid)
145
+
146
+ listener.stop()
147
+ client.close()
148
+ })
149
+
150
+ it('never sends a duplicate LOGINLIST with the same duuid while a session is alive', async () => {
151
+ // given — a strict guard: any second login while the first session is still open is a self-KICKOUT.
152
+ const client = await new KakaoTalkClient().login(CREDS)
153
+ const listener = new KakaoTalkListener(client)
154
+ await listener.start()
155
+
156
+ // when — heavy interleaving of outbound calls and inbound pushes
157
+ for (let i = 0; i < 10; i++) {
158
+ await client.sendMessage(String(100 + i), `msg-${i}`)
159
+ currentSession().simulatePush('MSG', {
160
+ chatId: { low: 100 + i, high: 0 },
161
+ chatLog: { logId: { low: i, high: 0 }, authorId: 1, message: `pushed-${i}`, type: 1, sendAt: i },
162
+ })
163
+ }
164
+
165
+ // then — still exactly one LOGINLIST, never a second one against the live session
166
+ const liveSessions = sessions.filter((s) => !s.closed)
167
+ expect(liveSessions.length).toBe(1)
168
+ expect(loginCalls.length).toBe(1)
169
+
170
+ listener.stop()
171
+ client.close()
172
+ })
173
+
174
+ it('listener still receives push events when only the listener is used (no API calls)', async () => {
175
+ const client = await new KakaoTalkClient().login(CREDS)
176
+ const listener = new KakaoTalkListener(client)
177
+
178
+ const messages: KakaoTalkPushMessageEvent[] = []
179
+ listener.on('message', (event) => messages.push(event))
180
+
181
+ await listener.start()
182
+ expect(sessions.length).toBe(1)
183
+
184
+ currentSession().simulatePush('MSG', {
185
+ chatId: { low: 100, high: 0 },
186
+ chatLog: {
187
+ logId: { low: 1, high: 0 },
188
+ authorId: 42,
189
+ message: 'hello',
190
+ type: 1,
191
+ sendAt: 1700000000,
192
+ },
193
+ })
194
+
195
+ expect(messages.length).toBe(1)
196
+ expect(messages[0].chat_id).toBe('100')
197
+ expect(messages[0].message).toBe('hello')
198
+
199
+ listener.stop()
200
+ client.close()
201
+ })
202
+
203
+ it('client-only usage still works (no listener)', async () => {
204
+ const client = await new KakaoTalkClient().login(CREDS)
205
+ const result = await client.sendMessage('100', 'hi')
206
+
207
+ expect(result.success).toBe(true)
208
+ expect(sessions.length).toBe(1)
209
+
210
+ client.close()
211
+ })
212
+
213
+ it('real KICKOUT propagates to listener and closes the session', async () => {
214
+ // given — client + listener sharing a session
215
+ const client = await new KakaoTalkClient().login(CREDS)
216
+ const listener = new KakaoTalkListener(client)
217
+
218
+ const errors: Error[] = []
219
+ listener.on('error', (err) => errors.push(err))
220
+
221
+ await listener.start()
222
+ expect(sessions.length).toBe(1)
223
+ const sharedSession = currentSession()
224
+
225
+ // when — server pushes a KICKOUT (a different real device logged in)
226
+ sharedSession.simulatePush('KICKOUT', {})
227
+
228
+ // then — listener emits the canonical error and stops itself
229
+ expect(errors.length).toBe(1)
230
+ expect(errors[0].message).toContain('kicked')
231
+ expect((listener as unknown as { running: boolean }).running).toBe(false)
232
+
233
+ listener.stop()
234
+ client.close()
235
+ })
236
+
237
+ it('reconnect after a TCP-level disconnect produces ONE replacement session shared by both halves', async () => {
238
+ // given — listener + client on a shared session
239
+ const client = await new KakaoTalkClient().login(CREDS)
240
+ const listener = new KakaoTalkListener(client)
241
+
242
+ const disconnects: number[] = []
243
+ const connects: Array<{ userId: string }> = []
244
+ listener.on('disconnected', () => disconnects.push(Date.now()))
245
+ listener.on('connected', (info) => connects.push(info))
246
+
247
+ await listener.start()
248
+ expect(sessions.length).toBe(1)
249
+ expect(connects.length).toBe(1)
250
+
251
+ // when — the underlying socket dies (not a KICKOUT)
252
+ currentSession().simulateRemoteClose()
253
+
254
+ // then — listener observed the disconnect
255
+ expect(disconnects.length).toBe(1)
256
+
257
+ // and — the next API call transparently opens exactly ONE replacement session
258
+ await client.sendMessage('100', 'after-reconnect')
259
+
260
+ expect(sessions.length).toBe(2)
261
+ // and — the listener was re-attached to that replacement session (i.e. push fan-out works again)
262
+ const messages: KakaoTalkPushMessageEvent[] = []
263
+ listener.on('message', (event) => messages.push(event))
264
+ currentSession().simulatePush('MSG', {
265
+ chatId: { low: 100, high: 0 },
266
+ chatLog: { logId: { low: 9, high: 0 }, authorId: 1, message: 'after-reconnect-push', type: 1, sendAt: 1 },
267
+ })
268
+ expect(messages.length).toBe(1)
269
+ expect(connects.length).toBe(2)
270
+
271
+ listener.stop()
272
+ client.close()
273
+ })
274
+
275
+ it('CHANGESVR triggers an active session migration', async () => {
276
+ // given — a listener attached to a shared session
277
+ const client = await new KakaoTalkClient().login(CREDS)
278
+ const listener = new KakaoTalkListener(client)
279
+
280
+ const disconnects: number[] = []
281
+ const connects: Array<{ userId: string }> = []
282
+ const generic: Array<{ type: string }> = []
283
+ listener.on('disconnected', () => disconnects.push(1))
284
+ listener.on('connected', (info) => connects.push(info))
285
+ listener.on('kakaotalk_event', (event) => generic.push(event))
286
+
287
+ await listener.start()
288
+ expect(sessions.length).toBe(1)
289
+ expect(connects.length).toBe(1)
290
+
291
+ // when — the server pushes CHANGESVR (asking us to migrate to a new gateway)
292
+ sessions[0]!.simulatePush('CHANGESVR', {})
293
+
294
+ // then — the client actively migrates: old session closed, new one opened
295
+ await new Promise((r) => setTimeout(r, 0))
296
+ expect(sessions.length).toBe(2)
297
+ expect(sessions[0]!.closed).toBe(true)
298
+ expect(disconnects.length).toBe(1)
299
+ expect(connects.length).toBe(2)
300
+
301
+ // and — the listener re-attached to the replacement session for push events
302
+ const messages: KakaoTalkPushMessageEvent[] = []
303
+ listener.on('message', (event) => messages.push(event))
304
+ sessions[1]!.simulatePush('MSG', {
305
+ chatId: { low: 100, high: 0 },
306
+ chatLog: { logId: { low: 7, high: 0 }, authorId: 1, message: 'after-changesvr', type: 1, sendAt: 1 },
307
+ })
308
+ expect(messages.length).toBe(1)
309
+
310
+ // and — CHANGESVR was still surfaced as a generic event for observers that care
311
+ expect(generic.some((e) => e.type === 'CHANGESVR')).toBe(true)
312
+
313
+ listener.stop()
314
+ client.close()
315
+ })
316
+
317
+ it('does not open duplicate LOGINLIST when concurrent calls trigger executeWithReconnect retry', async () => {
318
+ // given — a client+listener with a slow LOGINLIST so concurrent reconnects can collide
319
+ let inflight = 0
320
+ let peakInflight = 0
321
+ const originalLogin = MockLocoSession.prototype.login
322
+ MockLocoSession.prototype.login = async function (oauthToken, userId, deviceUuid, syncState, deviceType) {
323
+ inflight++
324
+ peakInflight = Math.max(peakInflight, inflight)
325
+ try {
326
+ await new Promise((r) => setTimeout(r, 20))
327
+ return await originalLogin.call(this, oauthToken, userId, deviceUuid, syncState, deviceType)
328
+ } finally {
329
+ inflight--
330
+ }
331
+ }
332
+
333
+ try {
334
+ const client = await new KakaoTalkClient().login(CREDS)
335
+ await client.sendMessage('100', 'prime')
336
+ expect(sessions.length).toBe(1)
337
+ expect(loginCalls.length).toBe(1)
338
+
339
+ // Make the live session's sendMessage drop the socket and then throw — modeling a
340
+ // mid-flight TCP reset where the remote-close handler nulls this.state synchronously
341
+ // before the operation rejection bubbles up to executeWithReconnect's catch block.
342
+ // This is the precise window the executeWithReconnect retry path was designed for.
343
+ const dead = sessions[0]!
344
+ dead.sendMessageImpl = async function () {
345
+ dead.simulateRemoteClose()
346
+ throw new Error('socket closed')
347
+ }
348
+
349
+ // when — three concurrent sendMessage calls all hit the dead session and retry
350
+ await Promise.all([
351
+ client.sendMessage('100', 'a'),
352
+ client.sendMessage('100', 'b'),
353
+ client.sendMessage('100', 'c'),
354
+ ])
355
+
356
+ // then — exactly ONE replacement LOGINLIST regardless of retry collisions
357
+ expect(peakInflight).toBe(1)
358
+ expect(loginCalls.length).toBe(2)
359
+ expect(sessions.length).toBe(2)
360
+
361
+ client.close()
362
+ } finally {
363
+ MockLocoSession.prototype.login = originalLogin
364
+ }
365
+ })
366
+
367
+ it('drops pushes from a session that has not been adopted as the active state', async () => {
368
+ // given — a listener subscribed to push events, before any session is opened
369
+ const client = await new KakaoTalkClient().login(CREDS)
370
+ const listener = new KakaoTalkListener(client)
371
+ const messages: KakaoTalkPushMessageEvent[] = []
372
+ listener.on('message', (event) => messages.push(event))
373
+
374
+ // Slow down login so we can fire a push DURING the connect()/login window,
375
+ // when LocoSession has been constructed but this.state is still null.
376
+ let preAdoptionPushFired = false
377
+ const originalLogin = MockLocoSession.prototype.login
378
+ MockLocoSession.prototype.login = async function (oauthToken, userId, deviceUuid, syncState, deviceType) {
379
+ if (!preAdoptionPushFired) {
380
+ preAdoptionPushFired = true
381
+ this.simulatePush('MSG', {
382
+ chatId: { low: 1, high: 0 },
383
+ chatLog: { logId: { low: 1, high: 0 }, authorId: 1, message: 'too-early', type: 1, sendAt: 1 },
384
+ })
385
+ }
386
+ return originalLogin.call(this, oauthToken, userId, deviceUuid, syncState, deviceType)
387
+ }
388
+
389
+ try {
390
+ // when — start the listener, which triggers connect() and a pre-adoption push
391
+ await listener.start()
392
+ expect(preAdoptionPushFired).toBe(true)
393
+
394
+ // then — the pre-adoption push must NOT have leaked to subscribers
395
+ expect(messages.length).toBe(0)
396
+
397
+ // and — once the session is adopted, fresh pushes flow normally
398
+ sessions[0]!.simulatePush('MSG', {
399
+ chatId: { low: 1, high: 0 },
400
+ chatLog: { logId: { low: 2, high: 0 }, authorId: 1, message: 'fresh', type: 1, sendAt: 2 },
401
+ })
402
+ expect(messages.length).toBe(1)
403
+ expect(messages[0].message).toBe('fresh')
404
+
405
+ listener.stop()
406
+ client.close()
407
+ } finally {
408
+ MockLocoSession.prototype.login = originalLogin
409
+ }
410
+ })
411
+ })
@@ -11,6 +11,7 @@ const mockSyncMessages = mock(() => Promise.resolve({}))
11
11
  const mockSendMessage = mock(() => Promise.resolve({}))
12
12
  const mockClose = mock(() => {})
13
13
  const mockOnClose = mock((_handler: () => void) => {})
14
+ const mockOnPush = mock((_handler: (packet: unknown) => void) => {})
14
15
 
15
16
  mock.module('./protocol/session', () => ({
16
17
  LocoSession: class MockLocoSession {
@@ -22,6 +23,7 @@ mock.module('./protocol/session', () => ({
22
23
  sendMessage = mockSendMessage
23
24
  close = mockClose
24
25
  onClose = mockOnClose
26
+ onPush = mockOnPush
25
27
  },
26
28
  }))
27
29
 
@@ -38,6 +40,7 @@ function resetAllMocks() {
38
40
  mockSendMessage.mockReset()
39
41
  mockClose.mockReset()
40
42
  mockOnClose.mockReset()
43
+ mockOnPush.mockReset()
41
44
  }
42
45
 
43
46
  // LOCO protocol uses plain numbers for chat.c, but Long-like objects for logIds/cursors