@geminixiang/mama 0.2.0-beta.2 → 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 +69 -41
- package/dist/adapter.d.ts +14 -4
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +8 -5
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +252 -98
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +83 -21
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/shared.d.ts +71 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/adapters/shared.js +168 -0
- package/dist/adapters/shared.js.map +1 -0
- package/dist/adapters/slack/bot.d.ts +5 -21
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +148 -150
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/branch-manager.d.ts +21 -0
- package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
- package/dist/adapters/slack/branch-manager.js +96 -0
- package/dist/adapters/slack/branch-manager.js.map +1 -0
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +92 -56
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/slack/session.d.ts +3 -0
- package/dist/adapters/slack/session.d.ts.map +1 -0
- package/dist/adapters/slack/session.js +16 -0
- package/dist/adapters/slack/session.js.map +1 -0
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +89 -103
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +40 -14
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/agent.d.ts +2 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +71 -142
- 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 +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +16 -3
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +11 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +100 -16
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +7 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +61 -30
- package/dist/events.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/{login.d.ts → login/index.d.ts} +1 -1
- package/dist/login/index.d.ts.map +1 -0
- package/dist/{login.js → login/index.js} +1 -1
- package/dist/login/index.js.map +1 -0
- package/dist/{link-server.d.ts → login/portal.d.ts} +5 -4
- package/dist/login/portal.d.ts.map +1 -0
- package/dist/login/portal.js +1453 -0
- package/dist/login/portal.js.map +1 -0
- package/dist/{link-token.d.ts → login/session.d.ts} +1 -1
- package/dist/login/session.d.ts.map +1 -0
- package/dist/{link-token.js → login/session.js} +1 -1
- package/dist/login/session.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +89 -19
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +17 -2
- package/dist/provisioner.d.ts.map +1 -1
- package/dist/provisioner.js +84 -5
- package/dist/provisioner.js.map +1 -1
- package/dist/session-policy.d.ts +13 -0
- package/dist/session-policy.d.ts.map +1 -0
- package/dist/session-policy.js +23 -0
- package/dist/session-policy.js.map +1 -0
- package/dist/session-store.d.ts +31 -1
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +168 -6
- package/dist/session-store.js.map +1 -1
- package/dist/session-view/command.d.ts +5 -0
- package/dist/session-view/command.d.ts.map +1 -0
- package/dist/session-view/command.js +11 -0
- package/dist/session-view/command.js.map +1 -0
- package/dist/session-view/portal.d.ts +11 -0
- package/dist/session-view/portal.d.ts.map +1 -0
- package/dist/session-view/portal.js +795 -0
- package/dist/session-view/portal.js.map +1 -0
- package/dist/session-view/service.d.ts +34 -0
- package/dist/session-view/service.d.ts.map +1 -0
- package/dist/session-view/service.js +416 -0
- package/dist/session-view/service.js.map +1 -0
- package/dist/session-view/store.d.ts +16 -0
- package/dist/session-view/store.d.ts.map +1 -0
- package/dist/session-view/store.js +38 -0
- package/dist/session-view/store.js.map +1 -0
- package/dist/store.d.ts +3 -6
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +15 -35
- package/dist/store.js.map +1 -1
- package/dist/tools/event.d.ts +2 -0
- package/dist/tools/event.d.ts.map +1 -1
- package/dist/tools/event.js +21 -3
- package/dist/tools/event.js.map +1 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.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
- package/dist/link-server.d.ts.map +0 -1
- package/dist/link-server.js +0 -899
- package/dist/link-server.js.map +0 -1
- package/dist/link-token.d.ts.map +0 -1
- package/dist/link-token.js.map +0 -1
- package/dist/login.d.ts.map +0 -1
- package/dist/login.js.map +0 -1
|
@@ -1,78 +1,23 @@
|
|
|
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 {
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
let lastError;
|
|
17
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
18
|
-
try {
|
|
19
|
-
return await fn();
|
|
20
|
-
}
|
|
21
|
-
catch (err) {
|
|
22
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
23
|
-
// Check for rate limit errors
|
|
24
|
-
let isRateLimited = false;
|
|
25
|
-
// Check for rate_limited error code (Slack SDK)
|
|
26
|
-
if ("code" in lastError && lastError.code === "rate_limited") {
|
|
27
|
-
isRateLimited = true;
|
|
28
|
-
}
|
|
29
|
-
// Check for rate_limited in error response
|
|
30
|
-
if ("data" in lastError) {
|
|
31
|
-
const data = lastError
|
|
32
|
-
.data;
|
|
33
|
-
if (data?.error === "rate_limited" || data?.response?.status === 429) {
|
|
34
|
-
isRateLimited = true;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
if (isRateLimited) {
|
|
38
|
-
const delay = baseDelayMs * Math.pow(2, attempt);
|
|
39
|
-
log.logWarning(`Rate limited, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
|
|
40
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
// Non-retryable error
|
|
44
|
-
throw lastError;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
throw lastError;
|
|
48
|
-
}
|
|
49
|
-
class ChannelQueue {
|
|
50
|
-
constructor() {
|
|
51
|
-
this.queue = [];
|
|
52
|
-
this.processing = false;
|
|
53
|
-
}
|
|
54
|
-
enqueue(work) {
|
|
55
|
-
this.queue.push(work);
|
|
56
|
-
this.processNext();
|
|
57
|
-
}
|
|
58
|
-
size() {
|
|
59
|
-
return this.queue.length;
|
|
60
|
-
}
|
|
61
|
-
async processNext() {
|
|
62
|
-
if (this.processing || this.queue.length === 0)
|
|
63
|
-
return;
|
|
64
|
-
this.processing = true;
|
|
65
|
-
const work = this.queue.shift();
|
|
66
|
-
try {
|
|
67
|
-
await work();
|
|
68
|
-
}
|
|
69
|
-
catch (err) {
|
|
70
|
-
log.logWarning("Queue error", err instanceof Error ? err.message : String(err));
|
|
71
|
-
}
|
|
72
|
-
this.processing = false;
|
|
73
|
-
this.processNext();
|
|
74
|
-
}
|
|
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;
|
|
75
16
|
}
|
|
17
|
+
const slackRetry = (fn) => withRetry(fn, { isRateLimited: slackIsRateLimited });
|
|
18
|
+
import { createSlackAdapters } from "./context.js";
|
|
19
|
+
import { hasMaterializedSlackBranchSession } from "./branch-manager.js";
|
|
20
|
+
import { resolveSlackSessionKey } from "./session.js";
|
|
76
21
|
// ============================================================================
|
|
77
22
|
// SlackBot
|
|
78
23
|
// ============================================================================
|
|
@@ -121,18 +66,18 @@ export class SlackBot {
|
|
|
121
66
|
return Array.from(this.channels.values());
|
|
122
67
|
}
|
|
123
68
|
async postMessage(channel, text) {
|
|
124
|
-
return
|
|
69
|
+
return slackRetry(async () => {
|
|
125
70
|
const result = await this.webClient.chat.postMessage({ channel, text });
|
|
126
71
|
return result.ts;
|
|
127
72
|
});
|
|
128
73
|
}
|
|
129
74
|
async postEphemeral(channel, user, text) {
|
|
130
|
-
return
|
|
75
|
+
return slackRetry(async () => {
|
|
131
76
|
await this.webClient.chat.postEphemeral({ channel, user, text });
|
|
132
77
|
});
|
|
133
78
|
}
|
|
134
79
|
async openDirectMessage(userId) {
|
|
135
|
-
return
|
|
80
|
+
return slackRetry(async () => {
|
|
136
81
|
const result = await this.webClient.conversations.open({ users: userId });
|
|
137
82
|
const channelId = result.channel?.id;
|
|
138
83
|
if (!channelId) {
|
|
@@ -142,12 +87,12 @@ export class SlackBot {
|
|
|
142
87
|
});
|
|
143
88
|
}
|
|
144
89
|
async updateMessage(channel, ts, text) {
|
|
145
|
-
return
|
|
90
|
+
return slackRetry(async () => {
|
|
146
91
|
await this.webClient.chat.update({ channel, ts, text });
|
|
147
92
|
});
|
|
148
93
|
}
|
|
149
94
|
async deleteMessage(channel, ts) {
|
|
150
|
-
return
|
|
95
|
+
return slackRetry(async () => {
|
|
151
96
|
await this.webClient.chat.delete({ channel, ts });
|
|
152
97
|
});
|
|
153
98
|
}
|
|
@@ -156,7 +101,7 @@ export class SlackBot {
|
|
|
156
101
|
// ==========================================================================
|
|
157
102
|
/** Set the status for an assistant thread (shows "thinking" state) */
|
|
158
103
|
async setAssistantStatus(channel, threadTs, status) {
|
|
159
|
-
return
|
|
104
|
+
return slackRetry(async () => {
|
|
160
105
|
await this.webClient.assistant.threads.setStatus({
|
|
161
106
|
channel_id: channel,
|
|
162
107
|
thread_ts: threadTs,
|
|
@@ -165,7 +110,7 @@ export class SlackBot {
|
|
|
165
110
|
});
|
|
166
111
|
}
|
|
167
112
|
async postInThread(channel, threadTs, text) {
|
|
168
|
-
return
|
|
113
|
+
return slackRetry(async () => {
|
|
169
114
|
// Use Block Kit section for long messages to trigger Slack's "Show more" collapsing (~700 chars)
|
|
170
115
|
const SECTION_TEXT_LIMIT = 3000;
|
|
171
116
|
if (text.length > 500) {
|
|
@@ -185,7 +130,7 @@ export class SlackBot {
|
|
|
185
130
|
});
|
|
186
131
|
}
|
|
187
132
|
async postInThreadBlocks(channel, threadTs, text, blocks) {
|
|
188
|
-
return
|
|
133
|
+
return slackRetry(async () => {
|
|
189
134
|
const result = await this.webClient.chat.postMessage({
|
|
190
135
|
channel,
|
|
191
136
|
thread_ts: threadTs,
|
|
@@ -196,7 +141,7 @@ export class SlackBot {
|
|
|
196
141
|
});
|
|
197
142
|
}
|
|
198
143
|
async uploadFile(channel, filePath, title, threadTs) {
|
|
199
|
-
return
|
|
144
|
+
return slackRetry(async () => {
|
|
200
145
|
const fileName = title || basename(filePath);
|
|
201
146
|
const fileContent = readFileSync(filePath);
|
|
202
147
|
await this.webClient.files.uploadV2({
|
|
@@ -208,29 +153,11 @@ export class SlackBot {
|
|
|
208
153
|
});
|
|
209
154
|
});
|
|
210
155
|
}
|
|
211
|
-
/**
|
|
212
|
-
* Log a message to log.jsonl (SYNC)
|
|
213
|
-
* This is the ONLY place messages are written to log.jsonl
|
|
214
|
-
*/
|
|
215
156
|
logToFile(channel, entry) {
|
|
216
|
-
|
|
217
|
-
if (!existsSync(dir))
|
|
218
|
-
mkdirSync(dir, { recursive: true });
|
|
219
|
-
appendFileSync(join(dir, "log.jsonl"), `${JSON.stringify(entry)}\n`);
|
|
157
|
+
appendChannelLog(this.workingDir, channel, entry);
|
|
220
158
|
}
|
|
221
|
-
/**
|
|
222
|
-
* Log a bot response to log.jsonl
|
|
223
|
-
*/
|
|
224
159
|
logBotResponse(channel, text, ts, threadTs) {
|
|
225
|
-
this.
|
|
226
|
-
date: new Date().toISOString(),
|
|
227
|
-
ts,
|
|
228
|
-
threadTs,
|
|
229
|
-
user: "bot",
|
|
230
|
-
text,
|
|
231
|
-
attachments: [],
|
|
232
|
-
isBot: true,
|
|
233
|
-
});
|
|
160
|
+
appendBotResponseLog(this.workingDir, channel, text, ts, threadTs);
|
|
234
161
|
}
|
|
235
162
|
getPlatformInfo() {
|
|
236
163
|
return {
|
|
@@ -242,6 +169,9 @@ export class SlackBot {
|
|
|
242
169
|
userName: u.userName,
|
|
243
170
|
displayName: u.displayName,
|
|
244
171
|
})),
|
|
172
|
+
diagnostics: {
|
|
173
|
+
showUsageSummary: true,
|
|
174
|
+
},
|
|
245
175
|
};
|
|
246
176
|
}
|
|
247
177
|
// ==========================================================================
|
|
@@ -286,11 +216,26 @@ export class SlackBot {
|
|
|
286
216
|
getQueue(channelId) {
|
|
287
217
|
let queue = this.queues.get(channelId);
|
|
288
218
|
if (!queue) {
|
|
289
|
-
queue = new ChannelQueue();
|
|
219
|
+
queue = new ChannelQueue("Slack");
|
|
290
220
|
this.queues.set(channelId, queue);
|
|
291
221
|
}
|
|
292
222
|
return queue;
|
|
293
223
|
}
|
|
224
|
+
resolveQueueKey(conversationId, sessionKey) {
|
|
225
|
+
if (!sessionKey.includes(":"))
|
|
226
|
+
return sessionKey;
|
|
227
|
+
return hasMaterializedSlackBranchSession(join(this.workingDir, conversationId), sessionKey)
|
|
228
|
+
? sessionKey
|
|
229
|
+
: conversationId;
|
|
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
|
+
}
|
|
294
239
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
295
240
|
buildHomeView() {
|
|
296
241
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -427,46 +372,43 @@ export class SlackBot {
|
|
|
427
372
|
});
|
|
428
373
|
return { type: "home", blocks };
|
|
429
374
|
}
|
|
430
|
-
/**
|
|
431
|
-
* Resolve which session key to stop.
|
|
432
|
-
* When stop is called from a thread, the thread session (channelId:thread_ts) might
|
|
433
|
-
* not be running — but the channel session (channelId) might be, because the bot's
|
|
434
|
-
* reply to a top-level mention creates a thread. Check both, prefer thread first.
|
|
435
|
-
*/
|
|
436
375
|
resolveStopTarget(channelId, threadTs) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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)
|
|
444
384
|
return null;
|
|
445
|
-
|
|
446
|
-
return this.handler.isRunning(channelId) ? channelId : null;
|
|
385
|
+
return resolveOnlyScopedStopTarget(this.handler, channelId);
|
|
447
386
|
}
|
|
448
|
-
|
|
387
|
+
createCommandAdapters(conversationId, userId, userName, text, ts, options = {}) {
|
|
449
388
|
const message = {
|
|
450
389
|
id: ts,
|
|
451
390
|
sessionKey: conversationId,
|
|
452
|
-
conversationKind: "direct",
|
|
391
|
+
conversationKind: options.ephemeralChannelId ? "shared" : "direct",
|
|
453
392
|
userId,
|
|
454
393
|
userName,
|
|
455
394
|
text,
|
|
456
395
|
attachments: [],
|
|
457
396
|
};
|
|
397
|
+
const respond = async (responseText) => {
|
|
398
|
+
if (options.ephemeralChannelId) {
|
|
399
|
+
await this.postEphemeral(options.ephemeralChannelId, userId, responseText);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const messageTs = await this.postMessage(conversationId, responseText);
|
|
403
|
+
this.logBotResponse(conversationId, responseText, messageTs);
|
|
404
|
+
};
|
|
458
405
|
const responseCtx = {
|
|
459
|
-
respond
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
this.logBotResponse(conversationId, responseText, messageTs);
|
|
466
|
-
},
|
|
467
|
-
respondInThread: async (responseText) => {
|
|
468
|
-
const messageTs = await this.postMessage(conversationId, responseText);
|
|
469
|
-
this.logBotResponse(conversationId, responseText, messageTs);
|
|
406
|
+
respond,
|
|
407
|
+
replaceResponse: respond,
|
|
408
|
+
respondDiagnostic: respond,
|
|
409
|
+
respondToolResult: async (result) => {
|
|
410
|
+
const duration = (result.durationMs / 1000).toFixed(1);
|
|
411
|
+
await respond(`${result.isError ? "Error" : "Done"} ${result.toolName} (${duration}s)\n${result.result}`);
|
|
470
412
|
},
|
|
471
413
|
setTyping: async () => { },
|
|
472
414
|
setWorking: async () => { },
|
|
@@ -530,7 +472,7 @@ export class SlackBot {
|
|
|
530
472
|
attachments: [],
|
|
531
473
|
sessionKey: targetChannelId,
|
|
532
474
|
};
|
|
533
|
-
const adapters = this.
|
|
475
|
+
const adapters = this.createCommandAdapters(targetChannelId, payload.user_id, userName, commandText, eventTs);
|
|
534
476
|
await this.handler.handleEvent(event, this, adapters, false);
|
|
535
477
|
}
|
|
536
478
|
async routeSlashNewCommand(payload) {
|
|
@@ -554,6 +496,36 @@ export class SlackBot {
|
|
|
554
496
|
const commandBot = this.createSlashCommandBot(conversationId);
|
|
555
497
|
await this.handler.handleNew(conversationId, conversationId, commandBot);
|
|
556
498
|
}
|
|
499
|
+
async routeSlashSessionCommand(payload) {
|
|
500
|
+
const conversationId = payload.channel_id;
|
|
501
|
+
const isDirectMessage = conversationId.startsWith("D");
|
|
502
|
+
const createdAt = new Date();
|
|
503
|
+
const eventTs = (createdAt.getTime() / 1000).toFixed(6);
|
|
504
|
+
const userName = payload.user_name ?? this.getUser(payload.user_id)?.userName;
|
|
505
|
+
const commandText = payload.command;
|
|
506
|
+
this.logToFile(conversationId, {
|
|
507
|
+
date: createdAt.toISOString(),
|
|
508
|
+
ts: eventTs,
|
|
509
|
+
user: payload.user_id,
|
|
510
|
+
userName,
|
|
511
|
+
text: commandText,
|
|
512
|
+
attachments: [],
|
|
513
|
+
isBot: false,
|
|
514
|
+
});
|
|
515
|
+
const sessionKey = conversationId;
|
|
516
|
+
const event = {
|
|
517
|
+
type: "dm",
|
|
518
|
+
conversationId,
|
|
519
|
+
conversationKind: isDirectMessage ? "direct" : "shared",
|
|
520
|
+
ts: eventTs,
|
|
521
|
+
user: payload.user_id,
|
|
522
|
+
text: commandText,
|
|
523
|
+
attachments: [],
|
|
524
|
+
sessionKey,
|
|
525
|
+
};
|
|
526
|
+
const adapters = this.createCommandAdapters(conversationId, payload.user_id, userName, commandText, eventTs, isDirectMessage ? {} : { ephemeralChannelId: conversationId });
|
|
527
|
+
await this.handler.handleEvent(event, this, adapters, false);
|
|
528
|
+
}
|
|
557
529
|
setupEventHandlers() {
|
|
558
530
|
// Channel @mentions
|
|
559
531
|
this.socketClient.on("app_mention", ({ event, ack }) => {
|
|
@@ -565,7 +537,7 @@ export class SlackBot {
|
|
|
565
537
|
}
|
|
566
538
|
// Top-level mentions use a persistent channel session.
|
|
567
539
|
// Thread replies get their own isolated session (channelId:thread_ts).
|
|
568
|
-
const sessionKey = e.
|
|
540
|
+
const sessionKey = resolveSlackSessionKey(e.channel, e.thread_ts);
|
|
569
541
|
const slackEvent = {
|
|
570
542
|
type: "mention",
|
|
571
543
|
conversationId: e.channel,
|
|
@@ -578,12 +550,13 @@ export class SlackBot {
|
|
|
578
550
|
files: e.files,
|
|
579
551
|
sessionKey,
|
|
580
552
|
};
|
|
581
|
-
|
|
582
|
-
// Also downloads attachments in background and stores local paths
|
|
583
|
-
slackEvent.attachments = this.logUserMessage(slackEvent);
|
|
553
|
+
const attachmentsPromise = this.logUserMessage(slackEvent);
|
|
584
554
|
// Only trigger processing for messages AFTER startup (not replayed old messages)
|
|
585
555
|
if (this.startupTs && e.ts < this.startupTs) {
|
|
586
556
|
log.logInfo(`[${e.channel}] Logged old message (pre-startup), not triggering: ${slackEvent.text.substring(0, 30)}`);
|
|
557
|
+
void attachmentsPromise.catch((err) => {
|
|
558
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
559
|
+
});
|
|
587
560
|
ack();
|
|
588
561
|
return;
|
|
589
562
|
}
|
|
@@ -596,10 +569,14 @@ export class SlackBot {
|
|
|
596
569
|
else {
|
|
597
570
|
this.postMessage(e.channel, formatNothingRunning("slack"));
|
|
598
571
|
}
|
|
572
|
+
void attachmentsPromise.catch((err) => {
|
|
573
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
574
|
+
});
|
|
599
575
|
ack();
|
|
600
576
|
return;
|
|
601
577
|
}
|
|
602
|
-
this.getQueue(sessionKey).enqueue(() => {
|
|
578
|
+
this.getQueue(this.resolveQueueKey(e.channel, sessionKey)).enqueue(async () => {
|
|
579
|
+
slackEvent.attachments = await attachmentsPromise;
|
|
603
580
|
const adapters = createSlackAdapters(slackEvent, this, false);
|
|
604
581
|
return this.handler.handleEvent(slackEvent, this, adapters, false);
|
|
605
582
|
});
|
|
@@ -629,6 +606,8 @@ export class SlackBot {
|
|
|
629
606
|
ack();
|
|
630
607
|
return;
|
|
631
608
|
}
|
|
609
|
+
const isSharedThreadReply = !isDM && this.shouldTriggerSharedThreadReply(e.channel, e.thread_ts);
|
|
610
|
+
const sessionKey = isDM || isSharedThreadReply ? resolveSlackSessionKey(e.channel, e.thread_ts) : undefined;
|
|
632
611
|
const slackEvent = {
|
|
633
612
|
type: isDM ? "dm" : "mention",
|
|
634
613
|
conversationId: e.channel,
|
|
@@ -639,14 +618,15 @@ export class SlackBot {
|
|
|
639
618
|
user: e.user,
|
|
640
619
|
text: (e.text || "").replace(/<@[A-Z0-9]+>/gi, "").trim(),
|
|
641
620
|
files: e.files,
|
|
642
|
-
sessionKey
|
|
621
|
+
sessionKey,
|
|
643
622
|
};
|
|
644
|
-
|
|
645
|
-
// Also downloads attachments in background and stores local paths
|
|
646
|
-
slackEvent.attachments = this.logUserMessage(slackEvent);
|
|
623
|
+
const attachmentsPromise = this.logUserMessage(slackEvent);
|
|
647
624
|
// Only trigger processing for messages AFTER startup (not replayed old messages)
|
|
648
625
|
if (this.startupTs && e.ts < this.startupTs) {
|
|
649
626
|
log.logInfo(`[${e.channel}] Skipping old message (pre-startup): ${slackEvent.text.substring(0, 30)}`);
|
|
627
|
+
void attachmentsPromise.catch((err) => {
|
|
628
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
629
|
+
});
|
|
650
630
|
ack();
|
|
651
631
|
return;
|
|
652
632
|
}
|
|
@@ -660,28 +640,41 @@ export class SlackBot {
|
|
|
660
640
|
else {
|
|
661
641
|
this.postMessage(e.channel, formatNothingRunning("slack"));
|
|
662
642
|
}
|
|
643
|
+
void attachmentsPromise.catch((err) => {
|
|
644
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
645
|
+
});
|
|
663
646
|
ack();
|
|
664
647
|
return;
|
|
665
648
|
}
|
|
666
|
-
//
|
|
667
|
-
if (isDM) {
|
|
668
|
-
const
|
|
649
|
+
// Trigger handler for DMs and bare replies inside shared-channel threads.
|
|
650
|
+
if (isDM || isSharedThreadReply) {
|
|
651
|
+
const activeSessionKey = slackEvent.sessionKey ?? resolveSlackSessionKey(e.channel, e.thread_ts);
|
|
669
652
|
// Check for stop command - execute immediately, don't queue!
|
|
670
653
|
if (slackEvent.text.toLowerCase().trim() === "stop") {
|
|
671
|
-
|
|
672
|
-
|
|
654
|
+
const stopTarget = this.resolveStopTarget(e.channel, e.thread_ts);
|
|
655
|
+
if (stopTarget) {
|
|
656
|
+
this.handler.handleStop(stopTarget, e.channel, this); // Don't await, don't queue
|
|
673
657
|
}
|
|
674
658
|
else {
|
|
675
659
|
this.postMessage(e.channel, formatNothingRunning("slack"));
|
|
676
660
|
}
|
|
661
|
+
void attachmentsPromise.catch((err) => {
|
|
662
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
663
|
+
});
|
|
677
664
|
ack();
|
|
678
665
|
return;
|
|
679
666
|
}
|
|
680
|
-
this.getQueue(
|
|
667
|
+
this.getQueue(this.resolveQueueKey(e.channel, activeSessionKey)).enqueue(async () => {
|
|
668
|
+
slackEvent.attachments = await attachmentsPromise;
|
|
681
669
|
const adapters = createSlackAdapters(slackEvent, this, false);
|
|
682
670
|
return this.handler.handleEvent(slackEvent, this, adapters, false);
|
|
683
671
|
});
|
|
684
672
|
}
|
|
673
|
+
else {
|
|
674
|
+
void attachmentsPromise.catch((err) => {
|
|
675
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
676
|
+
});
|
|
677
|
+
}
|
|
685
678
|
ack();
|
|
686
679
|
});
|
|
687
680
|
this.socketClient.on("slash_commands", async ({ body, ack }) => {
|
|
@@ -705,7 +698,14 @@ export class SlackBot {
|
|
|
705
698
|
user_id: payload.user_id,
|
|
706
699
|
user_name: payload.user_name,
|
|
707
700
|
})
|
|
708
|
-
:
|
|
701
|
+
: payload.command === "/pi-session"
|
|
702
|
+
? this.routeSlashSessionCommand({
|
|
703
|
+
command: payload.command,
|
|
704
|
+
channel_id: payload.channel_id,
|
|
705
|
+
user_id: payload.user_id,
|
|
706
|
+
user_name: payload.user_name,
|
|
707
|
+
})
|
|
708
|
+
: null;
|
|
709
709
|
if (!handlerPromise) {
|
|
710
710
|
return;
|
|
711
711
|
}
|
|
@@ -758,14 +758,12 @@ export class SlackBot {
|
|
|
758
758
|
});
|
|
759
759
|
}
|
|
760
760
|
/**
|
|
761
|
-
* Log a user message to log.jsonl
|
|
762
|
-
* Downloads attachments in background via store
|
|
761
|
+
* Log a user message to log.jsonl after attachments are ready.
|
|
763
762
|
*/
|
|
764
|
-
logUserMessage(event) {
|
|
763
|
+
async logUserMessage(event) {
|
|
765
764
|
const user = this.users.get(event.user);
|
|
766
|
-
// Process attachments - queues downloads in background
|
|
767
765
|
const attachments = event.files
|
|
768
|
-
? this.store.processAttachments(event.channel, event.files, event.ts)
|
|
766
|
+
? await this.store.processAttachments(event.channel, event.files, event.ts)
|
|
769
767
|
: [];
|
|
770
768
|
this.logToFile(event.channel, {
|
|
771
769
|
date: new Date(parseFloat(event.ts) * 1000).toISOString(),
|
|
@@ -850,13 +848,13 @@ export class SlackBot {
|
|
|
850
848
|
const user = this.users.get(msg.user);
|
|
851
849
|
// Strip @mentions from text (same as live messages)
|
|
852
850
|
const text = (msg.text || "").replace(/<@[A-Z0-9]+>/gi, "").trim();
|
|
853
|
-
// Process attachments - queues downloads in background
|
|
854
851
|
const attachments = msg.files
|
|
855
|
-
? this.store.processAttachments(channelId, msg.files, msg.ts)
|
|
852
|
+
? await this.store.processAttachments(channelId, msg.files, msg.ts)
|
|
856
853
|
: [];
|
|
857
854
|
this.logToFile(channelId, {
|
|
858
855
|
date: new Date(parseFloat(msg.ts) * 1000).toISOString(),
|
|
859
856
|
ts: msg.ts,
|
|
857
|
+
threadTs: msg.thread_ts,
|
|
860
858
|
user: isMamaMessage ? "bot" : msg.user,
|
|
861
859
|
userName: isMamaMessage ? undefined : user?.userName,
|
|
862
860
|
displayName: isMamaMessage ? undefined : user?.displayName,
|