@geminixiang/mama 0.2.0-beta.3 → 0.2.0-beta.4
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.
- package/README.md +2 -2
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +58 -72
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/shared.d.ts +48 -0
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/adapters/shared.js +111 -0
- package/dist/adapters/shared.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +2 -19
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +49 -185
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +78 -100
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +2 -0
- package/dist/agent.js.map +1 -1
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +3 -2
- package/dist/bindings.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -2
- package/dist/config.js.map +1 -1
- package/dist/fs-atomic.d.ts +10 -0
- package/dist/fs-atomic.d.ts.map +1 -0
- package/dist/fs-atomic.js +45 -0
- package/dist/fs-atomic.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +5 -7
- package/dist/main.js.map +1 -1
- package/dist/session-store.d.ts +5 -1
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +14 -9
- package/dist/session-store.js.map +1 -1
- package/dist/session-view/portal.d.ts +2 -0
- package/dist/session-view/portal.d.ts.map +1 -1
- package/dist/session-view/portal.js +35 -6
- package/dist/session-view/portal.js.map +1 -1
- package/dist/session-view/service.d.ts.map +1 -1
- package/dist/session-view/service.js +58 -22
- package/dist/session-view/service.js.map +1 -1
- package/dist/vault.d.ts.map +1 -1
- package/dist/vault.js +11 -55
- package/dist/vault.js.map +1 -1
- package/package.json +7 -8
|
@@ -1,81 +1,24 @@
|
|
|
1
1
|
import { SocketModeClient } from "@slack/socket-mode";
|
|
2
2
|
import { WebClient } from "@slack/web-api";
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync, readFileSync } from "fs";
|
|
4
4
|
import { readFile } from "fs/promises";
|
|
5
5
|
import { basename, join } from "path";
|
|
6
6
|
import * as log from "../../log.js";
|
|
7
7
|
import { PRODUCT_NAME, formatForceStopped, formatNothingRunning } from "../../ui-copy.js";
|
|
8
|
+
import { appendBotResponseLog, appendChannelLog, ChannelQueue, resolveOnlyScopedStopTarget, resolveStopTarget, withRetry, } from "../shared.js";
|
|
9
|
+
// Slack WebClient errors carry either `code: "rate_limited"` (retry-after) or
|
|
10
|
+
// the legacy `data.error === "rate_limited"` / 429 status shape.
|
|
11
|
+
function slackIsRateLimited(err) {
|
|
12
|
+
if (err.code === "rate_limited")
|
|
13
|
+
return true;
|
|
14
|
+
const data = err.data;
|
|
15
|
+
return data?.error === "rate_limited" || data?.response?.status === 429;
|
|
16
|
+
}
|
|
17
|
+
const slackRetry = (fn) => withRetry(fn, { isRateLimited: slackIsRateLimited });
|
|
8
18
|
import { createSlackAdapters } from "./context.js";
|
|
9
19
|
import { hasMaterializedSlackBranchSession } from "./branch-manager.js";
|
|
10
20
|
import { resolveSlackSessionKey } from "./session.js";
|
|
11
21
|
// ============================================================================
|
|
12
|
-
// Exponential backoff utility for Slack API calls
|
|
13
|
-
// ============================================================================
|
|
14
|
-
/**
|
|
15
|
-
* Retry a function with exponential backoff on rate limit errors.
|
|
16
|
-
*/
|
|
17
|
-
async function withRetry(fn, maxRetries = 3, baseDelayMs = 1000) {
|
|
18
|
-
let lastError;
|
|
19
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
20
|
-
try {
|
|
21
|
-
return await fn();
|
|
22
|
-
}
|
|
23
|
-
catch (err) {
|
|
24
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
25
|
-
// Check for rate limit errors
|
|
26
|
-
let isRateLimited = false;
|
|
27
|
-
// Check for rate_limited error code (Slack SDK)
|
|
28
|
-
if ("code" in lastError && lastError.code === "rate_limited") {
|
|
29
|
-
isRateLimited = true;
|
|
30
|
-
}
|
|
31
|
-
// Check for rate_limited in error response
|
|
32
|
-
if ("data" in lastError) {
|
|
33
|
-
const data = lastError
|
|
34
|
-
.data;
|
|
35
|
-
if (data?.error === "rate_limited" || data?.response?.status === 429) {
|
|
36
|
-
isRateLimited = true;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
if (isRateLimited) {
|
|
40
|
-
const delay = baseDelayMs * Math.pow(2, attempt);
|
|
41
|
-
log.logWarning(`Rate limited, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
|
|
42
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
// Non-retryable error
|
|
46
|
-
throw lastError;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
throw lastError;
|
|
50
|
-
}
|
|
51
|
-
class ChannelQueue {
|
|
52
|
-
constructor() {
|
|
53
|
-
this.queue = [];
|
|
54
|
-
this.processing = false;
|
|
55
|
-
}
|
|
56
|
-
enqueue(work) {
|
|
57
|
-
this.queue.push(work);
|
|
58
|
-
this.processNext();
|
|
59
|
-
}
|
|
60
|
-
size() {
|
|
61
|
-
return this.queue.length;
|
|
62
|
-
}
|
|
63
|
-
async processNext() {
|
|
64
|
-
if (this.processing || this.queue.length === 0)
|
|
65
|
-
return;
|
|
66
|
-
this.processing = true;
|
|
67
|
-
const work = this.queue.shift();
|
|
68
|
-
try {
|
|
69
|
-
await work();
|
|
70
|
-
}
|
|
71
|
-
catch (err) {
|
|
72
|
-
log.logWarning("Queue error", err instanceof Error ? err.message : String(err));
|
|
73
|
-
}
|
|
74
|
-
this.processing = false;
|
|
75
|
-
this.processNext();
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
// ============================================================================
|
|
79
22
|
// SlackBot
|
|
80
23
|
// ============================================================================
|
|
81
24
|
export class SlackBot {
|
|
@@ -123,18 +66,18 @@ export class SlackBot {
|
|
|
123
66
|
return Array.from(this.channels.values());
|
|
124
67
|
}
|
|
125
68
|
async postMessage(channel, text) {
|
|
126
|
-
return
|
|
69
|
+
return slackRetry(async () => {
|
|
127
70
|
const result = await this.webClient.chat.postMessage({ channel, text });
|
|
128
71
|
return result.ts;
|
|
129
72
|
});
|
|
130
73
|
}
|
|
131
74
|
async postEphemeral(channel, user, text) {
|
|
132
|
-
return
|
|
75
|
+
return slackRetry(async () => {
|
|
133
76
|
await this.webClient.chat.postEphemeral({ channel, user, text });
|
|
134
77
|
});
|
|
135
78
|
}
|
|
136
79
|
async openDirectMessage(userId) {
|
|
137
|
-
return
|
|
80
|
+
return slackRetry(async () => {
|
|
138
81
|
const result = await this.webClient.conversations.open({ users: userId });
|
|
139
82
|
const channelId = result.channel?.id;
|
|
140
83
|
if (!channelId) {
|
|
@@ -144,12 +87,12 @@ export class SlackBot {
|
|
|
144
87
|
});
|
|
145
88
|
}
|
|
146
89
|
async updateMessage(channel, ts, text) {
|
|
147
|
-
return
|
|
90
|
+
return slackRetry(async () => {
|
|
148
91
|
await this.webClient.chat.update({ channel, ts, text });
|
|
149
92
|
});
|
|
150
93
|
}
|
|
151
94
|
async deleteMessage(channel, ts) {
|
|
152
|
-
return
|
|
95
|
+
return slackRetry(async () => {
|
|
153
96
|
await this.webClient.chat.delete({ channel, ts });
|
|
154
97
|
});
|
|
155
98
|
}
|
|
@@ -158,7 +101,7 @@ export class SlackBot {
|
|
|
158
101
|
// ==========================================================================
|
|
159
102
|
/** Set the status for an assistant thread (shows "thinking" state) */
|
|
160
103
|
async setAssistantStatus(channel, threadTs, status) {
|
|
161
|
-
return
|
|
104
|
+
return slackRetry(async () => {
|
|
162
105
|
await this.webClient.assistant.threads.setStatus({
|
|
163
106
|
channel_id: channel,
|
|
164
107
|
thread_ts: threadTs,
|
|
@@ -167,7 +110,7 @@ export class SlackBot {
|
|
|
167
110
|
});
|
|
168
111
|
}
|
|
169
112
|
async postInThread(channel, threadTs, text) {
|
|
170
|
-
return
|
|
113
|
+
return slackRetry(async () => {
|
|
171
114
|
// Use Block Kit section for long messages to trigger Slack's "Show more" collapsing (~700 chars)
|
|
172
115
|
const SECTION_TEXT_LIMIT = 3000;
|
|
173
116
|
if (text.length > 500) {
|
|
@@ -187,7 +130,7 @@ export class SlackBot {
|
|
|
187
130
|
});
|
|
188
131
|
}
|
|
189
132
|
async postInThreadBlocks(channel, threadTs, text, blocks) {
|
|
190
|
-
return
|
|
133
|
+
return slackRetry(async () => {
|
|
191
134
|
const result = await this.webClient.chat.postMessage({
|
|
192
135
|
channel,
|
|
193
136
|
thread_ts: threadTs,
|
|
@@ -198,7 +141,7 @@ export class SlackBot {
|
|
|
198
141
|
});
|
|
199
142
|
}
|
|
200
143
|
async uploadFile(channel, filePath, title, threadTs) {
|
|
201
|
-
return
|
|
144
|
+
return slackRetry(async () => {
|
|
202
145
|
const fileName = title || basename(filePath);
|
|
203
146
|
const fileContent = readFileSync(filePath);
|
|
204
147
|
await this.webClient.files.uploadV2({
|
|
@@ -210,29 +153,11 @@ export class SlackBot {
|
|
|
210
153
|
});
|
|
211
154
|
});
|
|
212
155
|
}
|
|
213
|
-
/**
|
|
214
|
-
* Log a message to log.jsonl (SYNC)
|
|
215
|
-
* This is the ONLY place messages are written to log.jsonl
|
|
216
|
-
*/
|
|
217
156
|
logToFile(channel, entry) {
|
|
218
|
-
|
|
219
|
-
if (!existsSync(dir))
|
|
220
|
-
mkdirSync(dir, { recursive: true });
|
|
221
|
-
appendFileSync(join(dir, "log.jsonl"), `${JSON.stringify(entry)}\n`);
|
|
157
|
+
appendChannelLog(this.workingDir, channel, entry);
|
|
222
158
|
}
|
|
223
|
-
/**
|
|
224
|
-
* Log a bot response to log.jsonl
|
|
225
|
-
*/
|
|
226
159
|
logBotResponse(channel, text, ts, threadTs) {
|
|
227
|
-
this.
|
|
228
|
-
date: new Date().toISOString(),
|
|
229
|
-
ts,
|
|
230
|
-
threadTs,
|
|
231
|
-
user: "bot",
|
|
232
|
-
text,
|
|
233
|
-
attachments: [],
|
|
234
|
-
isBot: true,
|
|
235
|
-
});
|
|
160
|
+
appendBotResponseLog(this.workingDir, channel, text, ts, threadTs);
|
|
236
161
|
}
|
|
237
162
|
getPlatformInfo() {
|
|
238
163
|
return {
|
|
@@ -291,7 +216,7 @@ export class SlackBot {
|
|
|
291
216
|
getQueue(channelId) {
|
|
292
217
|
let queue = this.queues.get(channelId);
|
|
293
218
|
if (!queue) {
|
|
294
|
-
queue = new ChannelQueue();
|
|
219
|
+
queue = new ChannelQueue("Slack");
|
|
295
220
|
this.queues.set(channelId, queue);
|
|
296
221
|
}
|
|
297
222
|
return queue;
|
|
@@ -303,6 +228,14 @@ export class SlackBot {
|
|
|
303
228
|
? sessionKey
|
|
304
229
|
: conversationId;
|
|
305
230
|
}
|
|
231
|
+
shouldTriggerSharedThreadReply(channelId, threadTs) {
|
|
232
|
+
if (!threadTs)
|
|
233
|
+
return false;
|
|
234
|
+
const sessionKey = resolveSlackSessionKey(channelId, threadTs);
|
|
235
|
+
if (this.handler.isRunning(sessionKey))
|
|
236
|
+
return true;
|
|
237
|
+
return hasMaterializedSlackBranchSession(join(this.workingDir, channelId), sessionKey);
|
|
238
|
+
}
|
|
306
239
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
307
240
|
buildHomeView() {
|
|
308
241
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -439,76 +372,19 @@ export class SlackBot {
|
|
|
439
372
|
});
|
|
440
373
|
return { type: "home", blocks };
|
|
441
374
|
}
|
|
442
|
-
/**
|
|
443
|
-
* Resolve which session key to stop.
|
|
444
|
-
* When stop is called from a thread, the thread session (channelId:thread_ts) might
|
|
445
|
-
* not be running — but the channel session (channelId) might be, because the bot's
|
|
446
|
-
* reply to a top-level mention creates a thread. Check both, prefer thread first.
|
|
447
|
-
*
|
|
448
|
-
* For top-level stop requests, fall back to the only running scoped thread session in
|
|
449
|
-
* this conversation when there is exactly one candidate. This keeps DM/channel stop
|
|
450
|
-
* useful after thread sessions were introduced without guessing between multiple threads.
|
|
451
|
-
*/
|
|
452
375
|
resolveStopTarget(channelId, threadTs) {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
376
|
+
const directTarget = resolveStopTarget({
|
|
377
|
+
handler: this.handler,
|
|
378
|
+
conversationId: channelId,
|
|
379
|
+
sessionKey: threadTs ? resolveSlackSessionKey(channelId, threadTs) : undefined,
|
|
380
|
+
});
|
|
381
|
+
if (directTarget)
|
|
382
|
+
return directTarget;
|
|
383
|
+
if (threadTs)
|
|
459
384
|
return null;
|
|
460
|
-
|
|
461
|
-
if (this.handler.isRunning(channelId))
|
|
462
|
-
return channelId;
|
|
463
|
-
const runningInConversation = this.handler
|
|
464
|
-
.getRunningSessions()
|
|
465
|
-
.map((session) => session.sessionKey)
|
|
466
|
-
.filter((sessionKey) => sessionKey.startsWith(`${channelId}:`));
|
|
467
|
-
return runningInConversation.length === 1 ? runningInConversation[0] : null;
|
|
468
|
-
}
|
|
469
|
-
createDirectCommandAdapters(conversationId, userId, userName, text, ts) {
|
|
470
|
-
const message = {
|
|
471
|
-
id: ts,
|
|
472
|
-
sessionKey: conversationId,
|
|
473
|
-
conversationKind: "direct",
|
|
474
|
-
userId,
|
|
475
|
-
userName,
|
|
476
|
-
text,
|
|
477
|
-
attachments: [],
|
|
478
|
-
};
|
|
479
|
-
const responseCtx = {
|
|
480
|
-
respond: async (responseText) => {
|
|
481
|
-
const messageTs = await this.postMessage(conversationId, responseText);
|
|
482
|
-
this.logBotResponse(conversationId, responseText, messageTs);
|
|
483
|
-
},
|
|
484
|
-
replaceResponse: async (responseText) => {
|
|
485
|
-
const messageTs = await this.postMessage(conversationId, responseText);
|
|
486
|
-
this.logBotResponse(conversationId, responseText, messageTs);
|
|
487
|
-
},
|
|
488
|
-
respondDiagnostic: async (responseText) => {
|
|
489
|
-
const messageTs = await this.postMessage(conversationId, responseText);
|
|
490
|
-
this.logBotResponse(conversationId, responseText, messageTs);
|
|
491
|
-
},
|
|
492
|
-
respondToolResult: async (result) => {
|
|
493
|
-
const duration = (result.durationMs / 1000).toFixed(1);
|
|
494
|
-
const text = `${result.isError ? "Error" : "Done"} ${result.toolName} (${duration}s)\n${result.result}`;
|
|
495
|
-
const messageTs = await this.postMessage(conversationId, text);
|
|
496
|
-
this.logBotResponse(conversationId, text, messageTs);
|
|
497
|
-
},
|
|
498
|
-
setTyping: async () => { },
|
|
499
|
-
setWorking: async () => { },
|
|
500
|
-
uploadFile: async (filePath, title) => {
|
|
501
|
-
await this.uploadFile(conversationId, filePath, title);
|
|
502
|
-
},
|
|
503
|
-
deleteResponse: async () => { },
|
|
504
|
-
};
|
|
505
|
-
return {
|
|
506
|
-
message,
|
|
507
|
-
responseCtx,
|
|
508
|
-
platform: this.getPlatformInfo(),
|
|
509
|
-
};
|
|
385
|
+
return resolveOnlyScopedStopTarget(this.handler, channelId);
|
|
510
386
|
}
|
|
511
|
-
|
|
387
|
+
createCommandAdapters(conversationId, userId, userName, text, ts, options = {}) {
|
|
512
388
|
const message = {
|
|
513
389
|
id: ts,
|
|
514
390
|
sessionKey: conversationId,
|
|
@@ -518,8 +394,7 @@ export class SlackBot {
|
|
|
518
394
|
text,
|
|
519
395
|
attachments: [],
|
|
520
396
|
};
|
|
521
|
-
|
|
522
|
-
const respondPrivately = async (responseText) => {
|
|
397
|
+
const respond = async (responseText) => {
|
|
523
398
|
if (options.ephemeralChannelId) {
|
|
524
399
|
await this.postEphemeral(options.ephemeralChannelId, userId, responseText);
|
|
525
400
|
return;
|
|
@@ -528,23 +403,12 @@ export class SlackBot {
|
|
|
528
403
|
this.logBotResponse(conversationId, responseText, messageTs);
|
|
529
404
|
};
|
|
530
405
|
const responseCtx = {
|
|
531
|
-
respond
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
},
|
|
535
|
-
replaceResponse: async (responseText) => {
|
|
536
|
-
if (!hasResponded) {
|
|
537
|
-
hasResponded = true;
|
|
538
|
-
}
|
|
539
|
-
await respondPrivately(responseText);
|
|
540
|
-
},
|
|
541
|
-
respondDiagnostic: async (responseText) => {
|
|
542
|
-
await respondPrivately(responseText);
|
|
543
|
-
},
|
|
406
|
+
respond,
|
|
407
|
+
replaceResponse: respond,
|
|
408
|
+
respondDiagnostic: respond,
|
|
544
409
|
respondToolResult: async (result) => {
|
|
545
410
|
const duration = (result.durationMs / 1000).toFixed(1);
|
|
546
|
-
|
|
547
|
-
await respondPrivately(formatted);
|
|
411
|
+
await respond(`${result.isError ? "Error" : "Done"} ${result.toolName} (${duration}s)\n${result.result}`);
|
|
548
412
|
},
|
|
549
413
|
setTyping: async () => { },
|
|
550
414
|
setWorking: async () => { },
|
|
@@ -608,7 +472,7 @@ export class SlackBot {
|
|
|
608
472
|
attachments: [],
|
|
609
473
|
sessionKey: targetChannelId,
|
|
610
474
|
};
|
|
611
|
-
const adapters = this.
|
|
475
|
+
const adapters = this.createCommandAdapters(targetChannelId, payload.user_id, userName, commandText, eventTs);
|
|
612
476
|
await this.handler.handleEvent(event, this, adapters, false);
|
|
613
477
|
}
|
|
614
478
|
async routeSlashNewCommand(payload) {
|
|
@@ -659,7 +523,7 @@ export class SlackBot {
|
|
|
659
523
|
attachments: [],
|
|
660
524
|
sessionKey,
|
|
661
525
|
};
|
|
662
|
-
const adapters = this.
|
|
526
|
+
const adapters = this.createCommandAdapters(conversationId, payload.user_id, userName, commandText, eventTs, isDirectMessage ? {} : { ephemeralChannelId: conversationId });
|
|
663
527
|
await this.handler.handleEvent(event, this, adapters, false);
|
|
664
528
|
}
|
|
665
529
|
setupEventHandlers() {
|
|
@@ -742,7 +606,7 @@ export class SlackBot {
|
|
|
742
606
|
ack();
|
|
743
607
|
return;
|
|
744
608
|
}
|
|
745
|
-
const isSharedThreadReply = !isDM &&
|
|
609
|
+
const isSharedThreadReply = !isDM && this.shouldTriggerSharedThreadReply(e.channel, e.thread_ts);
|
|
746
610
|
const sessionKey = isDM || isSharedThreadReply ? resolveSlackSessionKey(e.channel, e.thread_ts) : undefined;
|
|
747
611
|
const slackEvent = {
|
|
748
612
|
type: isDM ? "dm" : "mention",
|