openclaw-lark-multi-agent 0.1.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.
@@ -0,0 +1 @@
1
+ export declare function startApp(configPath?: string): Promise<void>;
package/dist/index.js ADDED
@@ -0,0 +1,52 @@
1
+ import { loadConfig } from "./config.js";
2
+ import { OpenClawClient } from "./openclaw-client.js";
3
+ import { MessageStore } from "./message-store.js";
4
+ import { FeishuBot } from "./feishu-bot.js";
5
+ import { mkdirSync } from "fs";
6
+ import { dirname, resolve } from "path";
7
+ import { fileURLToPath } from "url";
8
+ export async function startApp(configPath) {
9
+ const config = loadConfig(configPath);
10
+ console.log("=== OpenClaw Lark Multi-Agent ===");
11
+ console.log(`OpenClaw: ${config.openclaw.baseUrl}`);
12
+ console.log(`Bots: ${config.bots.map((b) => `${b.name}(${b.model})`).join(", ")}`);
13
+ console.log("");
14
+ // Init data dir & store. Runtime state should live next to config by default,
15
+ // not next to deployable program files. Override with LMA_DATA_DIR if needed.
16
+ const resolvedConfigPath = configPath ? resolve(configPath) : resolve(process.cwd(), "config.json");
17
+ const dataDir = process.env.LMA_DATA_DIR
18
+ ? resolve(process.env.LMA_DATA_DIR)
19
+ : resolve(dirname(resolvedConfigPath), "data");
20
+ mkdirSync(dataDir, { recursive: true });
21
+ console.log(`Data dir: ${dataDir}`);
22
+ const store = new MessageStore(resolve(dataDir, "messages.db"));
23
+ // Connect to OpenClaw Gateway via WebSocket
24
+ const openclawClient = new OpenClawClient(config.openclaw);
25
+ await openclawClient.connect();
26
+ // Two-phase startup: register all bots first, then start WS connections
27
+ const bots = [];
28
+ for (const botConfig of config.bots) {
29
+ const bot = new FeishuBot(botConfig, openclawClient, store, config.adminOpenId);
30
+ bots.push(bot);
31
+ bot.register(); // Phase 1: register to allBots map
32
+ }
33
+ for (const bot of bots) {
34
+ await bot.start(); // Phase 2: start WS connections
35
+ }
36
+ console.log(`\nAll ${bots.length} bots started. Waiting for messages...`);
37
+ // Graceful shutdown
38
+ const shutdown = () => {
39
+ console.log("\nShutting down...");
40
+ openclawClient.disconnect();
41
+ store.close();
42
+ process.exit(0);
43
+ };
44
+ process.on("SIGINT", shutdown);
45
+ process.on("SIGTERM", shutdown);
46
+ }
47
+ if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
48
+ startApp(process.argv[2]).catch((err) => {
49
+ console.error("Fatal error:", err);
50
+ process.exit(1);
51
+ });
52
+ }
@@ -0,0 +1,80 @@
1
+ export interface ChatInfo {
2
+ chatId: string;
3
+ chatType: "p2p" | "group";
4
+ chatName: string;
5
+ /** Comma-separated member open_ids */
6
+ members: string;
7
+ /** Comma-separated member names */
8
+ memberNames: string;
9
+ /** Which bot owns this chat (for p2p isolation) */
10
+ ownerBot: string;
11
+ /** Free discussion mode (group chat: all bots respond without @) */
12
+ freeDiscussion: boolean;
13
+ verbose: boolean;
14
+ updatedAt: number;
15
+ }
16
+ export interface ChatMessage {
17
+ id?: number;
18
+ chatId: string;
19
+ messageId: string;
20
+ senderType: "human" | "bot";
21
+ senderName: string;
22
+ content: string;
23
+ timestamp: number;
24
+ }
25
+ export declare class MessageStore {
26
+ private db;
27
+ constructor(dbPath?: string);
28
+ private init;
29
+ /**
30
+ * Insert a message. Returns the auto-increment id, or -1 if duplicate.
31
+ */
32
+ insert(msg: ChatMessage): number;
33
+ getMessageId(messageId: string): number | null;
34
+ markPendingTrigger(botName: string, chatId: string, messageRowId: number): void;
35
+ getPendingTriggerIds(botName: string, chatId: string): Set<number>;
36
+ clearPendingTriggers(botName: string, chatId: string, upToId: number): void;
37
+ hasDeliveredReply(botName: string, chatId: string, triggerMessageRowId: number): boolean;
38
+ markDeliveredReply(botName: string, chatId: string, triggerMessageRowId: number, replyMessageId?: string): void;
39
+ /**
40
+ * Get messages that haven't been synced to a bot's session yet.
41
+ * Returns messages ordered by timestamp ascending.
42
+ */
43
+ getUnsyncedMessages(botName: string, chatId: string, maxCount?: number): ChatMessage[];
44
+ /**
45
+ * Mark all messages up to (and including) the given id as synced for a bot.
46
+ */
47
+ markSynced(botName: string, chatId: string, upToId: number): void;
48
+ /**
49
+ * Get recent messages for a chat, ordered by timestamp ascending.
50
+ */
51
+ getRecent(chatId: string, maxCount?: number): ChatMessage[];
52
+ /**
53
+ * Count consecutive bot messages at the tail of a chat.
54
+ */
55
+ getBotStreak(chatId: string): number;
56
+ upsertChatInfo(info: ChatInfo): void;
57
+ setFreeDiscussion(chatId: string, on: boolean): void;
58
+ setVerbose(chatId: string, verbose: boolean): void;
59
+ setBotVerbose(botName: string, chatId: string, verbose: boolean): void;
60
+ getBotVerbose(botName: string, chatId: string): boolean;
61
+ getChatInfo(chatId: string): ChatInfo | null;
62
+ getAllChatInfo(): ChatInfo[];
63
+ /**
64
+ * Check if a message already exists in the store.
65
+ */
66
+ hasMessage(messageId: string): boolean;
67
+ /**
68
+ * Get total message count for a chat.
69
+ */
70
+ getMessageCount(chatId: string): number;
71
+ /**
72
+ * Check if a specific bot has already processed a message.
73
+ */
74
+ hasBotProcessed(botName: string, messageId: string): boolean;
75
+ /**
76
+ * Mark a message as processed by a specific bot.
77
+ */
78
+ markBotProcessed(botName: string, messageId: string): void;
79
+ close(): void;
80
+ }
@@ -0,0 +1,333 @@
1
+ import Database from "better-sqlite3";
2
+ import { resolve } from "path";
3
+ import { mkdirSync } from "fs";
4
+ export class MessageStore {
5
+ db;
6
+ constructor(dbPath) {
7
+ const path = dbPath || resolve(process.cwd(), "data", "messages.db");
8
+ const dir = resolve(path, "..");
9
+ mkdirSync(dir, { recursive: true });
10
+ this.db = new Database(path);
11
+ this.db.pragma("journal_mode = WAL");
12
+ this.init();
13
+ }
14
+ init() {
15
+ this.db.exec(`
16
+ CREATE TABLE IF NOT EXISTS messages (
17
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
18
+ chat_id TEXT NOT NULL,
19
+ message_id TEXT UNIQUE NOT NULL,
20
+ sender_type TEXT NOT NULL,
21
+ sender_name TEXT NOT NULL,
22
+ content TEXT NOT NULL,
23
+ timestamp INTEGER NOT NULL
24
+ );
25
+ CREATE INDEX IF NOT EXISTS idx_messages_chat_ts ON messages(chat_id, timestamp);
26
+
27
+ CREATE TABLE IF NOT EXISTS chat_info (
28
+ chat_id TEXT PRIMARY KEY,
29
+ chat_type TEXT NOT NULL DEFAULT 'group',
30
+ chat_name TEXT NOT NULL DEFAULT '',
31
+ members TEXT NOT NULL DEFAULT '',
32
+ member_names TEXT NOT NULL DEFAULT '',
33
+ verbose INTEGER NOT NULL DEFAULT 0,
34
+ updated_at INTEGER NOT NULL DEFAULT 0
35
+ );
36
+
37
+ -- Tracks which messages have been synced to each bot's OpenClaw session.
38
+ CREATE TABLE IF NOT EXISTS sync_state (
39
+ bot_name TEXT NOT NULL,
40
+ chat_id TEXT NOT NULL,
41
+ last_synced_msg_id INTEGER NOT NULL DEFAULT 0,
42
+ PRIMARY KEY (bot_name, chat_id)
43
+ );
44
+
45
+ -- Tracks which messages have been processed by each bot (multi-bot dedup).
46
+ CREATE TABLE IF NOT EXISTS processed_events (
47
+ bot_name TEXT NOT NULL,
48
+ message_id TEXT NOT NULL,
49
+ PRIMARY KEY (bot_name, message_id)
50
+ );
51
+
52
+ -- Tracks messages that should actively trigger a bot reply.
53
+ -- Other unsynced messages remain local context and are sent only when a trigger arrives.
54
+ CREATE TABLE IF NOT EXISTS pending_triggers (
55
+ bot_name TEXT NOT NULL,
56
+ chat_id TEXT NOT NULL,
57
+ message_row_id INTEGER NOT NULL,
58
+ PRIMARY KEY (bot_name, chat_id, message_row_id)
59
+ );
60
+
61
+ -- Tracks replies already delivered for a trigger message.
62
+ -- Prevents duplicate user-visible replies after restarts/race conditions.
63
+ CREATE TABLE IF NOT EXISTS delivered_replies (
64
+ bot_name TEXT NOT NULL,
65
+ chat_id TEXT NOT NULL,
66
+ trigger_message_row_id INTEGER NOT NULL,
67
+ delivered_at INTEGER NOT NULL,
68
+ reply_message_id TEXT NOT NULL DEFAULT '',
69
+ PRIMARY KEY (bot_name, chat_id, trigger_message_row_id)
70
+ );
71
+
72
+ -- Per-bot, per-chat settings. A group can contain multiple bots, so settings
73
+ -- like verbose must not be shared globally at chat level.
74
+ CREATE TABLE IF NOT EXISTS bot_chat_settings (
75
+ bot_name TEXT NOT NULL,
76
+ chat_id TEXT NOT NULL,
77
+ verbose INTEGER NOT NULL DEFAULT 0,
78
+ updated_at INTEGER NOT NULL DEFAULT 0,
79
+ PRIMARY KEY (bot_name, chat_id)
80
+ );
81
+ `);
82
+ // Migration: add verbose column if missing
83
+ try {
84
+ this.db.exec(`ALTER TABLE chat_info ADD COLUMN verbose INTEGER NOT NULL DEFAULT 0`);
85
+ }
86
+ catch {
87
+ // Column already exists
88
+ }
89
+ // Migration: add owner_bot column if missing
90
+ try {
91
+ this.db.exec(`ALTER TABLE chat_info ADD COLUMN owner_bot TEXT NOT NULL DEFAULT ''`);
92
+ }
93
+ catch {
94
+ // Column already exists
95
+ }
96
+ // Migration: add free_discussion column if missing
97
+ try {
98
+ this.db.exec(`ALTER TABLE chat_info ADD COLUMN free_discussion INTEGER NOT NULL DEFAULT 0`);
99
+ }
100
+ catch {
101
+ // Column already exists
102
+ }
103
+ }
104
+ /**
105
+ * Insert a message. Returns the auto-increment id, or -1 if duplicate.
106
+ */
107
+ insert(msg) {
108
+ try {
109
+ const result = this.db.prepare(`
110
+ INSERT INTO messages (chat_id, message_id, sender_type, sender_name, content, timestamp)
111
+ VALUES (?, ?, ?, ?, ?, ?)
112
+ `).run(msg.chatId, msg.messageId, msg.senderType, msg.senderName, msg.content, msg.timestamp);
113
+ return Number(result.lastInsertRowid);
114
+ }
115
+ catch (err) {
116
+ if (err.code === "SQLITE_CONSTRAINT_UNIQUE")
117
+ return -1;
118
+ throw err;
119
+ }
120
+ }
121
+ getMessageId(messageId) {
122
+ const row = this.db.prepare(`SELECT id FROM messages WHERE message_id = ?`).get(messageId);
123
+ return row?.id || null;
124
+ }
125
+ markPendingTrigger(botName, chatId, messageRowId) {
126
+ this.db.prepare(`
127
+ INSERT OR IGNORE INTO pending_triggers (bot_name, chat_id, message_row_id)
128
+ VALUES (?, ?, ?)
129
+ `).run(botName, chatId, messageRowId);
130
+ }
131
+ getPendingTriggerIds(botName, chatId) {
132
+ const rows = this.db.prepare(`
133
+ SELECT message_row_id FROM pending_triggers
134
+ WHERE bot_name = ? AND chat_id = ?
135
+ ORDER BY message_row_id ASC
136
+ `).all(botName, chatId);
137
+ return new Set(rows.map((r) => Number(r.message_row_id)));
138
+ }
139
+ clearPendingTriggers(botName, chatId, upToId) {
140
+ this.db.prepare(`
141
+ DELETE FROM pending_triggers
142
+ WHERE bot_name = ? AND chat_id = ? AND message_row_id <= ?
143
+ `).run(botName, chatId, upToId);
144
+ }
145
+ hasDeliveredReply(botName, chatId, triggerMessageRowId) {
146
+ const row = this.db.prepare(`
147
+ SELECT 1 FROM delivered_replies
148
+ WHERE bot_name = ? AND chat_id = ? AND trigger_message_row_id = ?
149
+ `).get(botName, chatId, triggerMessageRowId);
150
+ return !!row;
151
+ }
152
+ markDeliveredReply(botName, chatId, triggerMessageRowId, replyMessageId = '') {
153
+ this.db.prepare(`
154
+ INSERT OR IGNORE INTO delivered_replies (bot_name, chat_id, trigger_message_row_id, delivered_at, reply_message_id)
155
+ VALUES (?, ?, ?, ?, ?)
156
+ `).run(botName, chatId, triggerMessageRowId, Date.now(), replyMessageId);
157
+ }
158
+ /**
159
+ * Get messages that haven't been synced to a bot's session yet.
160
+ * Returns messages ordered by timestamp ascending.
161
+ */
162
+ getUnsyncedMessages(botName, chatId, maxCount = 50) {
163
+ const row = this.db.prepare(`
164
+ SELECT last_synced_msg_id FROM sync_state
165
+ WHERE bot_name = ? AND chat_id = ?
166
+ `).get(botName, chatId);
167
+ const lastId = row?.last_synced_msg_id || 0;
168
+ const rows = this.db.prepare(`
169
+ SELECT * FROM messages
170
+ WHERE chat_id = ? AND id > ?
171
+ ORDER BY timestamp ASC
172
+ LIMIT ?
173
+ `).all(chatId, lastId, maxCount);
174
+ return rows.map((r) => ({
175
+ id: r.id,
176
+ chatId: r.chat_id,
177
+ messageId: r.message_id,
178
+ senderType: r.sender_type,
179
+ senderName: r.sender_name,
180
+ content: r.content,
181
+ timestamp: r.timestamp,
182
+ }));
183
+ }
184
+ /**
185
+ * Mark all messages up to (and including) the given id as synced for a bot.
186
+ */
187
+ markSynced(botName, chatId, upToId) {
188
+ this.db.prepare(`
189
+ INSERT INTO sync_state (bot_name, chat_id, last_synced_msg_id)
190
+ VALUES (?, ?, ?)
191
+ ON CONFLICT (bot_name, chat_id) DO UPDATE SET last_synced_msg_id = MAX(sync_state.last_synced_msg_id, excluded.last_synced_msg_id)
192
+ `).run(botName, chatId, upToId);
193
+ }
194
+ /**
195
+ * Get recent messages for a chat, ordered by timestamp ascending.
196
+ */
197
+ getRecent(chatId, maxCount = 50) {
198
+ const rows = this.db.prepare(`
199
+ SELECT * FROM messages
200
+ WHERE chat_id = ?
201
+ ORDER BY timestamp DESC
202
+ LIMIT ?
203
+ `).all(chatId, maxCount);
204
+ return rows.reverse().map((r) => ({
205
+ id: r.id,
206
+ chatId: r.chat_id,
207
+ messageId: r.message_id,
208
+ senderType: r.sender_type,
209
+ senderName: r.sender_name,
210
+ content: r.content,
211
+ timestamp: r.timestamp,
212
+ }));
213
+ }
214
+ /**
215
+ * Count consecutive bot messages at the tail of a chat.
216
+ */
217
+ getBotStreak(chatId) {
218
+ const rows = this.db.prepare(`
219
+ SELECT sender_type FROM messages
220
+ WHERE chat_id = ?
221
+ ORDER BY timestamp DESC
222
+ LIMIT 20
223
+ `).all(chatId);
224
+ let count = 0;
225
+ for (const r of rows) {
226
+ if (r.sender_type === "bot")
227
+ count++;
228
+ else
229
+ break;
230
+ }
231
+ return count;
232
+ }
233
+ // --- Chat info ---
234
+ upsertChatInfo(info) {
235
+ this.db.prepare(`
236
+ INSERT INTO chat_info (chat_id, chat_type, chat_name, members, member_names, verbose, free_discussion, owner_bot, updated_at)
237
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
238
+ ON CONFLICT (chat_id) DO UPDATE SET
239
+ chat_type = excluded.chat_type,
240
+ chat_name = excluded.chat_name,
241
+ members = excluded.members,
242
+ member_names = excluded.member_names,
243
+ verbose = excluded.verbose,
244
+ free_discussion = excluded.free_discussion,
245
+ owner_bot = CASE WHEN excluded.owner_bot != '' THEN excluded.owner_bot ELSE chat_info.owner_bot END,
246
+ updated_at = excluded.updated_at
247
+ `).run(info.chatId, info.chatType, info.chatName, info.members, info.memberNames, info.verbose ? 1 : 0, info.freeDiscussion ? 1 : 0, info.ownerBot || '', info.updatedAt);
248
+ }
249
+ setFreeDiscussion(chatId, on) {
250
+ this.db.prepare(`UPDATE chat_info SET free_discussion = ? WHERE chat_id = ?`).run(on ? 1 : 0, chatId);
251
+ }
252
+ setVerbose(chatId, verbose) {
253
+ this.db.prepare(`
254
+ UPDATE chat_info SET verbose = ? WHERE chat_id = ?
255
+ `).run(verbose ? 1 : 0, chatId);
256
+ }
257
+ setBotVerbose(botName, chatId, verbose) {
258
+ this.db.prepare(`
259
+ INSERT INTO bot_chat_settings (bot_name, chat_id, verbose, updated_at)
260
+ VALUES (?, ?, ?, ?)
261
+ ON CONFLICT (bot_name, chat_id) DO UPDATE SET
262
+ verbose = excluded.verbose,
263
+ updated_at = excluded.updated_at
264
+ `).run(botName, chatId, verbose ? 1 : 0, Date.now());
265
+ }
266
+ getBotVerbose(botName, chatId) {
267
+ const row = this.db.prepare(`
268
+ SELECT verbose FROM bot_chat_settings
269
+ WHERE bot_name = ? AND chat_id = ?
270
+ `).get(botName, chatId);
271
+ return !!row?.verbose;
272
+ }
273
+ getChatInfo(chatId) {
274
+ const row = this.db.prepare(`SELECT * FROM chat_info WHERE chat_id = ?`).get(chatId);
275
+ if (!row)
276
+ return null;
277
+ return {
278
+ chatId: row.chat_id,
279
+ chatType: row.chat_type,
280
+ chatName: row.chat_name,
281
+ members: row.members,
282
+ memberNames: row.member_names,
283
+ ownerBot: row.owner_bot || '',
284
+ freeDiscussion: !!row.free_discussion,
285
+ verbose: !!row.verbose,
286
+ updatedAt: row.updated_at,
287
+ };
288
+ }
289
+ getAllChatInfo() {
290
+ const rows = this.db.prepare(`SELECT * FROM chat_info ORDER BY updated_at DESC`).all();
291
+ return rows.map((r) => ({
292
+ chatId: r.chat_id,
293
+ chatType: r.chat_type,
294
+ chatName: r.chat_name,
295
+ members: r.members,
296
+ memberNames: r.member_names,
297
+ ownerBot: r.owner_bot || '',
298
+ freeDiscussion: !!r.free_discussion,
299
+ verbose: !!r.verbose,
300
+ updatedAt: r.updated_at,
301
+ }));
302
+ }
303
+ /**
304
+ * Check if a message already exists in the store.
305
+ */
306
+ hasMessage(messageId) {
307
+ const row = this.db.prepare(`SELECT 1 FROM messages WHERE message_id = ?`).get(messageId);
308
+ return !!row;
309
+ }
310
+ /**
311
+ * Get total message count for a chat.
312
+ */
313
+ getMessageCount(chatId) {
314
+ const row = this.db.prepare(`SELECT COUNT(*) as cnt FROM messages WHERE chat_id = ?`).get(chatId);
315
+ return row?.cnt || 0;
316
+ }
317
+ /**
318
+ * Check if a specific bot has already processed a message.
319
+ */
320
+ hasBotProcessed(botName, messageId) {
321
+ const row = this.db.prepare(`SELECT 1 FROM processed_events WHERE bot_name = ? AND message_id = ?`).get(botName, messageId);
322
+ return !!row;
323
+ }
324
+ /**
325
+ * Mark a message as processed by a specific bot.
326
+ */
327
+ markBotProcessed(botName, messageId) {
328
+ this.db.prepare(`INSERT OR IGNORE INTO processed_events (bot_name, message_id) VALUES (?, ?)`).run(botName, messageId);
329
+ }
330
+ close() {
331
+ this.db.close();
332
+ }
333
+ }
@@ -0,0 +1,111 @@
1
+ import { OpenClawConfig } from "./config.js";
2
+ import { ChatMessage } from "./message-store.js";
3
+ export type ChatAttachment = {
4
+ type?: string;
5
+ mimeType?: string;
6
+ fileName?: string;
7
+ content: string;
8
+ };
9
+ /**
10
+ * OpenClaw Gateway WebSocket client.
11
+ * Full agent pipeline — tools, memory, skills, context management by OpenClaw.
12
+ */
13
+ export declare class OpenClawClient {
14
+ private config;
15
+ private ws;
16
+ private pending;
17
+ private connected;
18
+ private connectPromise;
19
+ private agentEvents;
20
+ private reconnectDelay;
21
+ private maxReconnectDelay;
22
+ private shouldReconnect;
23
+ /** Callbacks for tool events (verbose mode) */
24
+ private toolEventCallbacks;
25
+ private sessionMessageCallbacks;
26
+ /** Session keys that should be re-subscribed on reconnect */
27
+ private subscribedKeys;
28
+ /** Session keys with active chatSend — suppress proactive message delivery */
29
+ private suppressedSessions;
30
+ constructor(config: OpenClawConfig);
31
+ connect(): Promise<void>;
32
+ private _doConnect;
33
+ private scheduleReconnect;
34
+ private rpc;
35
+ /**
36
+ * Collect agent reply by polling accumulated events.
37
+ * Matches by initial runId OR sessionKey to handle multi-turn tool calling
38
+ * where OpenClaw creates new runIds for each tool-call round.
39
+ * No aggressive timeout — waits for lifecycle end as the source of truth.
40
+ * 30-minute safety net only for catastrophic WS disconnection.
41
+ */
42
+ private collectReply;
43
+ createSession(params: {
44
+ key: string;
45
+ model: string;
46
+ label?: string;
47
+ }): Promise<any>;
48
+ patchSession(params: {
49
+ key: string;
50
+ model?: string;
51
+ label?: string;
52
+ }): Promise<any>;
53
+ getSessionStatus(key: string): Promise<any>;
54
+ /**
55
+ * Get session info (model, tokens, etc.) for status display.
56
+ */
57
+ getSessionInfo(sessionKey: string): Promise<any>;
58
+ /**
59
+ * Ensure session is using the expected model. If not, patch it back.
60
+ * Returns true if a correction was made.
61
+ */
62
+ ensureModel(sessionKey: string, expectedModel: string): Promise<boolean>;
63
+ deleteSession(key: string, deleteTranscript?: boolean): Promise<any>;
64
+ resetSession(key: string): Promise<any>;
65
+ compactSession(key: string): Promise<any>;
66
+ /**
67
+ * Send a message to a session and get the agent reply.
68
+ * deliver=false prevents OpenClaw from auto-posting to channels.
69
+ */
70
+ abortChat(sessionKey: string, runId: string): Promise<any>;
71
+ chatSend(params: {
72
+ sessionKey: string;
73
+ message: string;
74
+ attachments?: ChatAttachment[];
75
+ deliver?: boolean;
76
+ timeoutMs?: number;
77
+ }): Promise<string>;
78
+ /**
79
+ * Build and send a context catch-up message followed by the actual message.
80
+ *
81
+ * Batches unsynced messages into a single context block to minimize agent runs.
82
+ * Format:
83
+ * [Context: messages you missed]
84
+ * [Alice 00:30]: blah
85
+ * [GPT 00:31]: blah
86
+ * ---
87
+ * Now respond to: <actual message>
88
+ *
89
+ * If there are no unsynced messages, just sends the actual message directly.
90
+ */
91
+ chatSendWithContext(params: {
92
+ sessionKey: string;
93
+ unsyncedMessages: ChatMessage[];
94
+ currentMessage: string;
95
+ currentSenderName: string;
96
+ deliver?: boolean;
97
+ timeoutMs?: number;
98
+ }): Promise<string>;
99
+ private extractImageAttachments;
100
+ disconnect(): Promise<void>;
101
+ /**
102
+ * Subscribe to message events for a session.
103
+ * When the agent proactively produces a message, the callback fires.
104
+ */
105
+ subscribeSession(sessionKey: string, onMessage: (text: string) => void): Promise<void>;
106
+ unsubscribeSession(sessionKey: string): Promise<void>;
107
+ /**
108
+ * Register a callback for tool call events on a session.
109
+ */
110
+ onToolEvent(sessionKey: string, callback: (toolName: string, toolInput: string, toolOutput: string) => void): void;
111
+ }