@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 CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Swarm API",
5
- "version": "1.67.3",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.67.3",
3
+ "version": "1.67.4",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
@@ -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
 
@@ -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
- _botUserId: string,
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
+ });