@desplega.ai/agent-swarm 1.67.3 → 1.67.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/openapi.json +1 -1
- package/package.json +1 -1
- package/src/slack/handlers.ts +17 -2
- package/src/slack/router.ts +20 -2
- package/src/tests/slack-bot-filter.test.ts +156 -0
package/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Swarm API",
|
|
5
|
-
"version": "1.67.
|
|
5
|
+
"version": "1.67.4",
|
|
6
6
|
"description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
|
|
7
7
|
},
|
|
8
8
|
"servers": [
|
package/package.json
CHANGED
package/src/slack/handlers.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { resolveTemplate } from "../prompts/resolver";
|
|
|
13
13
|
import { workflowEventBus } from "../workflows/event-bus";
|
|
14
14
|
import { buildTreeBlocks, type TreeNode } from "./blocks";
|
|
15
15
|
import type { SlackFile } from "./files";
|
|
16
|
-
import { extractTaskFromMessage, routeMessage } from "./router";
|
|
16
|
+
import { extractTaskFromMessage, hasOtherUserMention, routeMessage } from "./router";
|
|
17
17
|
// Side-effect import: registers all Slack event templates in the in-memory registry
|
|
18
18
|
import "./templates";
|
|
19
19
|
import { bufferThreadMessage, getBufferMessageCount, instantFlush } from "./thread-buffer";
|
|
@@ -162,6 +162,12 @@ interface MessageEvent {
|
|
|
162
162
|
* - `bot_id` present (newer Slack API, may lack subtype)
|
|
163
163
|
* - `user` matches the bot's own user ID (catches edge cases where
|
|
164
164
|
* messages posted with `username` override lack `bot_id`)
|
|
165
|
+
*
|
|
166
|
+
* Note: intentionally does NOT filter on `app_id`/`bot_profile`/`username` —
|
|
167
|
+
* those signals also appear on human messages sent via Slack apps that proxy
|
|
168
|
+
* a user (e.g. Claude.ai's Slack integration sends with `app_id` + `bot_profile`
|
|
169
|
+
* set, but the poster is still a real human). Filtering those drops legitimate
|
|
170
|
+
* human @mentions of the swarm.
|
|
165
171
|
*/
|
|
166
172
|
export function isBotMessage(
|
|
167
173
|
event: { subtype?: string; bot_id?: string; user?: string },
|
|
@@ -439,8 +445,17 @@ export function registerMessageHandler(app: App): void {
|
|
|
439
445
|
}
|
|
440
446
|
}
|
|
441
447
|
|
|
442
|
-
// ADDITIVE_SLACK: Buffer non-mention thread messages
|
|
448
|
+
// ADDITIVE_SLACK: Buffer non-mention thread messages.
|
|
449
|
+
// Skip if the message @-mentions someone other than our bot (e.g. "@Devin wdyt?"):
|
|
450
|
+
// that message is directed at a different bot/user and must not be fed to
|
|
451
|
+
// the swarm as an implicit follow-up.
|
|
443
452
|
if (additiveSlack && !botMentioned && msg.thread_ts && !requireMentionForThreadFollowup) {
|
|
453
|
+
if (hasOtherUserMention(effectiveText, botUserId)) {
|
|
454
|
+
console.log(
|
|
455
|
+
`[Slack] Skipping ADDITIVE buffer in ${msg.channel}/${msg.thread_ts}: message mentions another user`,
|
|
456
|
+
);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
444
459
|
// Check if this thread has any swarm activity (existing tasks)
|
|
445
460
|
const hasSwarmActivity = getAgentWorkingOnThread(msg.channel, msg.thread_ts) !== null;
|
|
446
461
|
|
package/src/slack/router.ts
CHANGED
|
@@ -6,6 +6,15 @@ export interface ThreadContext {
|
|
|
6
6
|
threadTs: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Returns true if the text contains a `<@U...>` mention of anyone other than our bot.
|
|
11
|
+
* Exported for testing.
|
|
12
|
+
*/
|
|
13
|
+
export function hasOtherUserMention(text: string, botUserId: string): boolean {
|
|
14
|
+
const mentions = text.match(/<@([A-Z0-9]+)>/g) ?? [];
|
|
15
|
+
return mentions.some((m) => m !== `<@${botUserId}>`);
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
/**
|
|
10
19
|
* Routes a Slack message to the appropriate agent(s) based on mentions.
|
|
11
20
|
*
|
|
@@ -16,7 +25,7 @@ export interface ThreadContext {
|
|
|
16
25
|
*/
|
|
17
26
|
export function routeMessage(
|
|
18
27
|
text: string,
|
|
19
|
-
|
|
28
|
+
botUserId: string,
|
|
20
29
|
botMentioned: boolean,
|
|
21
30
|
threadContext?: ThreadContext,
|
|
22
31
|
): AgentMatch[] {
|
|
@@ -46,8 +55,17 @@ export function routeMessage(
|
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
// Thread follow-up — route to agent already working in this thread
|
|
58
|
+
// Thread follow-up — route to agent already working in this thread.
|
|
59
|
+
// Skip if the message @-mentions someone other than our bot (e.g. "@Devin wdyt?")
|
|
60
|
+
// and does not mention our bot: that message is directed at a different bot/user,
|
|
61
|
+
// not a follow-up intended for the swarm.
|
|
50
62
|
if (matches.length === 0 && threadContext && (!requireMentionForThreadFollowup || botMentioned)) {
|
|
63
|
+
if (!botMentioned && hasOtherUserMention(text, botUserId)) {
|
|
64
|
+
console.log(
|
|
65
|
+
`[Slack] Skipping thread follow-up in ${threadContext.channelId}/${threadContext.threadTs}: message mentions another user`,
|
|
66
|
+
);
|
|
67
|
+
return matches;
|
|
68
|
+
}
|
|
51
69
|
const workingAgent = getAgentWorkingOnThread(threadContext.channelId, threadContext.threadTs);
|
|
52
70
|
if (workingAgent && workingAgent.status !== "offline") {
|
|
53
71
|
console.log(
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlinkSync } from "node:fs";
|
|
3
|
+
import { closeDb, createAgent, createTaskExtended, initDb } from "../be/db";
|
|
4
|
+
import { isBotMessage } from "../slack/handlers";
|
|
5
|
+
import { hasOtherUserMention, routeMessage } from "../slack/router";
|
|
6
|
+
import type { Agent } from "../types";
|
|
7
|
+
|
|
8
|
+
const TEST_DB_PATH = "./test-slack-bot-filter.sqlite";
|
|
9
|
+
|
|
10
|
+
let leadAgent: Agent;
|
|
11
|
+
let workerAgent: Agent;
|
|
12
|
+
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
initDb(TEST_DB_PATH);
|
|
15
|
+
leadAgent = createAgent({
|
|
16
|
+
name: "filter-lead",
|
|
17
|
+
isLead: true,
|
|
18
|
+
status: "idle",
|
|
19
|
+
capabilities: [],
|
|
20
|
+
});
|
|
21
|
+
workerAgent = createAgent({
|
|
22
|
+
name: "filter-worker",
|
|
23
|
+
isLead: false,
|
|
24
|
+
status: "idle",
|
|
25
|
+
capabilities: [],
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterAll(() => {
|
|
30
|
+
closeDb();
|
|
31
|
+
try {
|
|
32
|
+
unlinkSync(TEST_DB_PATH);
|
|
33
|
+
unlinkSync(`${TEST_DB_PATH}-wal`);
|
|
34
|
+
unlinkSync(`${TEST_DB_PATH}-shm`);
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("isBotMessage", () => {
|
|
41
|
+
const BOT_ID = "UBOT123";
|
|
42
|
+
|
|
43
|
+
test("plain human message → false", () => {
|
|
44
|
+
expect(isBotMessage({ user: "UHUMAN" }, BOT_ID)).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("subtype bot_message → true", () => {
|
|
48
|
+
expect(isBotMessage({ subtype: "bot_message", user: "UHUMAN" }, BOT_ID)).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("bot_id present → true", () => {
|
|
52
|
+
expect(isBotMessage({ bot_id: "B001", user: "UHUMAN" }, BOT_ID)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("own bot user ID (self-posted) → true", () => {
|
|
56
|
+
expect(isBotMessage({ user: BOT_ID }, BOT_ID)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("empty event with no bot signals → false", () => {
|
|
60
|
+
expect(isBotMessage({ user: "UHUMAN" }, BOT_ID)).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("hasOtherUserMention", () => {
|
|
65
|
+
const BOT_ID = "UBOT123";
|
|
66
|
+
|
|
67
|
+
test("no mentions → false", () => {
|
|
68
|
+
expect(hasOtherUserMention("hello everyone", BOT_ID)).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("only our bot mentioned → false", () => {
|
|
72
|
+
expect(hasOtherUserMention(`hey <@${BOT_ID}> pls`, BOT_ID)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("different user mentioned → true", () => {
|
|
76
|
+
expect(hasOtherUserMention("hey <@UDEVIN01> wdyt", BOT_ID)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("our bot AND another user mentioned → true", () => {
|
|
80
|
+
expect(hasOtherUserMention(`<@${BOT_ID}> and <@UDEVIN01> hi`, BOT_ID)).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("routeMessage — thread follow-up skips messages aimed at other users", () => {
|
|
85
|
+
const BOT_ID = "UBOT123";
|
|
86
|
+
|
|
87
|
+
test("plain follow-up (no mentions) still routes to active worker", () => {
|
|
88
|
+
const channelId = "C_BF_100";
|
|
89
|
+
const threadTs = "1100.0001";
|
|
90
|
+
createTaskExtended("original", {
|
|
91
|
+
agentId: workerAgent.id,
|
|
92
|
+
source: "slack",
|
|
93
|
+
slackChannelId: channelId,
|
|
94
|
+
slackThreadTs: threadTs,
|
|
95
|
+
slackUserId: "U_HUMAN",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const matches = routeMessage("and also the weather", BOT_ID, false, {
|
|
99
|
+
channelId,
|
|
100
|
+
threadTs,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(matches).toHaveLength(1);
|
|
104
|
+
expect(matches[0].agent.id).toBe(workerAgent.id);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("follow-up mentioning another bot (Devin) does NOT route", () => {
|
|
108
|
+
const channelId = "C_BF_200";
|
|
109
|
+
const threadTs = "1200.0001";
|
|
110
|
+
createTaskExtended("original", {
|
|
111
|
+
agentId: workerAgent.id,
|
|
112
|
+
source: "slack",
|
|
113
|
+
slackChannelId: channelId,
|
|
114
|
+
slackThreadTs: threadTs,
|
|
115
|
+
slackUserId: "U_HUMAN",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const matches = routeMessage("<@UDEVIN01> wdyt?", BOT_ID, false, {
|
|
119
|
+
channelId,
|
|
120
|
+
threadTs,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(matches).toHaveLength(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("follow-up mentioning BOTH our bot and another bot routes to swarm", () => {
|
|
127
|
+
const channelId = "C_BF_300";
|
|
128
|
+
const threadTs = "1300.0001";
|
|
129
|
+
createTaskExtended("original", {
|
|
130
|
+
agentId: workerAgent.id,
|
|
131
|
+
source: "slack",
|
|
132
|
+
slackChannelId: channelId,
|
|
133
|
+
slackThreadTs: threadTs,
|
|
134
|
+
slackUserId: "U_HUMAN",
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const matches = routeMessage(`<@${BOT_ID}> and <@UDEVIN01> please coordinate`, BOT_ID, true, {
|
|
138
|
+
channelId,
|
|
139
|
+
threadTs,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(matches).toHaveLength(1);
|
|
143
|
+
expect(matches[0].agent.id).toBe(workerAgent.id);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("no thread activity + only another bot mentioned → no match", () => {
|
|
147
|
+
const matches = routeMessage("<@UDEVIN01> hi", BOT_ID, false, {
|
|
148
|
+
channelId: "C_BF_400",
|
|
149
|
+
threadTs: "1400.0001",
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(matches).toHaveLength(0);
|
|
153
|
+
// Silence unused lead warning — lead exists but should not be routed to
|
|
154
|
+
expect(leadAgent.id).toBeDefined();
|
|
155
|
+
});
|
|
156
|
+
});
|