@desplega.ai/agent-swarm 1.81.0 → 1.81.1

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.81.0",
5
+ "version": "1.81.1",
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": [
@@ -7358,6 +7358,15 @@
7358
7358
  "required": false,
7359
7359
  "name": "q",
7360
7360
  "in": "query"
7361
+ },
7362
+ {
7363
+ "schema": {
7364
+ "type": "string",
7365
+ "minLength": 1
7366
+ },
7367
+ "required": false,
7368
+ "name": "requestedByUserId",
7369
+ "in": "query"
7361
7370
  }
7362
7371
  ],
7363
7372
  "responses": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.81.0",
3
+ "version": "1.81.1",
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>",
package/src/be/db.ts CHANGED
@@ -9110,11 +9110,14 @@ export function listRecentSessions(opts?: {
9110
9110
  source?: string[];
9111
9111
  /** Case-insensitive substring match against `r.task`. */
9112
9112
  q?: string;
9113
+ /** When set, restrict to root tasks where `requestedByUserId` equals this value. NULL rows are excluded. */
9114
+ requestedByUserId?: string;
9113
9115
  }): SessionListItem[] {
9114
9116
  const limit = opts?.limit ?? 25;
9115
9117
  const offset = opts?.offset ?? 0;
9116
9118
  const sources = opts?.source?.filter((s) => s.length > 0) ?? [];
9117
9119
  const q = opts?.q?.trim();
9120
+ const requestedByUserId = opts?.requestedByUserId?.trim() || undefined;
9118
9121
 
9119
9122
  const conditions: string[] = ["r.parentTaskId IS NULL"];
9120
9123
  const params: (string | number)[] = [];
@@ -9127,6 +9130,10 @@ export function listRecentSessions(opts?: {
9127
9130
  conditions.push("lower(r.task) LIKE ?");
9128
9131
  params.push(`%${q.toLowerCase()}%`);
9129
9132
  }
9133
+ if (requestedByUserId) {
9134
+ conditions.push("r.requestedByUserId = ?");
9135
+ params.push(requestedByUserId);
9136
+ }
9130
9137
  params.push(limit, offset);
9131
9138
 
9132
9139
  const rootRows = getDb()
@@ -798,11 +798,10 @@ export async function handleComment(
798
798
  ): Promise<{ created: boolean; taskId?: string }> {
799
799
  const { action, comment, repository, sender, issue, pull_request, installation } = event;
800
800
 
801
- // Resolve canonical user from GitHub sender (currently unused, but the
802
- // unmapped-tracker side effect is still useful for operator triage).
803
- const _requestedByUserId = resolveGitHubSender(
801
+ // Resolve canonical user from GitHub sender
802
+ const requestedByUserId = resolveGitHubSender(
804
803
  sender.login,
805
- "issue_comment",
804
+ eventType,
806
805
  comment.body.slice(0, 100),
807
806
  );
808
807
 
@@ -873,6 +872,7 @@ export async function handleComment(
873
872
  vcsNumber: targetNumber,
874
873
  vcsCommentId: comment.id,
875
874
  vcsAuthor: sender.login,
875
+ requestedByUserId,
876
876
  vcsUrl: targetUrl,
877
877
  vcsInstallationId: installation?.id,
878
878
  vcsNodeId: comment.node_id,
@@ -911,9 +911,8 @@ export async function handlePullRequestReview(
911
911
  ): Promise<{ created: boolean; taskId?: string }> {
912
912
  const { action, review, pull_request: pr, repository, sender, installation } = event;
913
913
 
914
- // Resolve canonical user from GitHub sender (currently unused, but the
915
- // unmapped-tracker side effect is still useful for operator triage).
916
- const _requestedByUserId = resolveGitHubSender(
914
+ // Resolve canonical user from GitHub sender
915
+ const requestedByUserId = resolveGitHubSender(
917
916
  sender.login,
918
917
  "pull_request_review",
919
918
  `Review on PR #${pr.number}: ${review.state}`,
@@ -995,6 +994,7 @@ export async function handlePullRequestReview(
995
994
  vcsEventType: "pull_request_review",
996
995
  vcsNumber: pr.number,
997
996
  vcsAuthor: sender.login,
997
+ requestedByUserId,
998
998
  vcsUrl: review.html_url,
999
999
  vcsInstallationId: installation?.id,
1000
1000
  vcsNodeId: review.node_id,
@@ -19,6 +19,12 @@ const listSessions = route({
19
19
  source: z.string().optional(),
20
20
  /** Case-insensitive substring match against the root task's text. */
21
21
  q: z.string().optional(),
22
+ /**
23
+ * When present, restrict results to root tasks where
24
+ * `agent_tasks.requestedByUserId` equals this value. NULL rows are
25
+ * excluded. Omit to return every session (legacy / non-UI callers).
26
+ */
27
+ requestedByUserId: z.string().min(1).optional(),
22
28
  }),
23
29
  responses: {
24
30
  200: { description: "Recent sessions ordered by chain-wide last activity" },
@@ -64,6 +70,7 @@ export async function handleSessions(
64
70
  offset: parsed.query.offset,
65
71
  source: sources,
66
72
  q: parsed.query.q,
73
+ requestedByUserId: parsed.query.requestedByUserId,
67
74
  });
68
75
  json(res, { sessions });
69
76
  return true;
@@ -187,6 +187,35 @@ describe("known github sender", () => {
187
187
  expect(getKv(UNMAPPED_NAMESPACE, "assigner:meta")).toBeNull();
188
188
  expect(getKv(UNMAPPED_NAMESPACE, "assigner:count")).toBeNull();
189
189
  });
190
+
191
+ test("comment event with bot mention from mapped user puts user id on the task", async () => {
192
+ const user = createUser({ name: "Mapped Commenter", email: "commenter@example.com" });
193
+ linkIdentity(user.id, "github", "commenter", SYSTEM_ACTOR);
194
+
195
+ const result = await handleComment(
196
+ makeCommentEvent("commenter", `Hey @${GITHUB_BOT_NAME} please take a look`),
197
+ "issue_comment",
198
+ );
199
+ expect(result.created).toBe(true);
200
+ expect(getMappedUserTaskCount(user.id)).toBe(1);
201
+
202
+ // Mapped sender → no unmapped kv writes.
203
+ expect(getKv(UNMAPPED_NAMESPACE, "commenter:meta")).toBeNull();
204
+ expect(getKv(UNMAPPED_NAMESPACE, "commenter:count")).toBeNull();
205
+ });
206
+
207
+ test("review event from mapped user puts user id on the task", async () => {
208
+ const user = createUser({ name: "Mapped Reviewer", email: "reviewer@example.com" });
209
+ linkIdentity(user.id, "github", "reviewer", SYSTEM_ACTOR);
210
+
211
+ const result = await handlePullRequestReview(makeReviewEvent("reviewer"));
212
+ expect(result.created).toBe(true);
213
+ expect(getMappedUserTaskCount(user.id)).toBe(1);
214
+
215
+ // Mapped sender → no unmapped kv writes.
216
+ expect(getKv(UNMAPPED_NAMESPACE, "reviewer:meta")).toBeNull();
217
+ expect(getKv(UNMAPPED_NAMESPACE, "reviewer:count")).toBeNull();
218
+ });
190
219
  });
191
220
 
192
221
  // ── Unknown sender → unmapped kv tracker ──
@@ -4,6 +4,7 @@ import {
4
4
  closeDb,
5
5
  createAgent,
6
6
  createTaskExtended,
7
+ createUser,
7
8
  getRootTaskChain,
8
9
  initDb,
9
10
  listRecentSessions,
@@ -138,4 +139,56 @@ describe("sessions — getRootTaskChain + listRecentSessions", () => {
138
139
  expect(sessions[i - 1].lastActivityAt >= sessions[i].lastActivityAt).toBe(true);
139
140
  }
140
141
  });
142
+
143
+ test("listRecentSessions — requestedByUserId filter: positive / negative / NULL exclusion / compat", () => {
144
+ const userA = createUser({ name: "Test User A" });
145
+ const userB = createUser({ name: "Test User B" });
146
+
147
+ const agent = createAgent({
148
+ id: "sessions-test-agent-user-filter",
149
+ name: "Sessions Test Agent UserFilter",
150
+ isLead: false,
151
+ status: "idle",
152
+ });
153
+ createTaskExtended("user-a session 1", {
154
+ agentId: agent.id,
155
+ requestedByUserId: userA.id,
156
+ });
157
+ createTaskExtended("user-a session 2", {
158
+ agentId: agent.id,
159
+ requestedByUserId: userA.id,
160
+ });
161
+ createTaskExtended("user-b session 1", {
162
+ agentId: agent.id,
163
+ requestedByUserId: userB.id,
164
+ });
165
+
166
+ // Positive: user A sees only their own sessions
167
+ const aOnly = listRecentSessions({ limit: 50, requestedByUserId: userA.id });
168
+ const aTasks = aOnly.map((s) => s.root.task);
169
+ expect(aTasks).toContain("user-a session 1");
170
+ expect(aTasks).toContain("user-a session 2");
171
+ expect(aTasks).not.toContain("user-b session 1");
172
+ for (const s of aOnly) {
173
+ expect(s.root.requestedByUserId).toBe(userA.id);
174
+ }
175
+
176
+ // Negative: user A cannot see user B's sessions
177
+ const hasUserBInA = aOnly.some((s) => s.root.requestedByUserId === userB.id);
178
+ expect(hasUserBInA).toBe(false);
179
+
180
+ // NULL exclusion: NULL requestedByUserId rows excluded when filter is active
181
+ const hasNullInA = aOnly.some((s) => s.root.requestedByUserId == null);
182
+ expect(hasNullInA).toBe(false);
183
+
184
+ // Empty: unknown user ID returns empty list
185
+ const nobody = listRecentSessions({ limit: 50, requestedByUserId: "non-existent-user-id" });
186
+ expect(nobody).toHaveLength(0);
187
+
188
+ // Compat: no filter returns all sessions including NULL rows and both users
189
+ const all = listRecentSessions({ limit: 100 });
190
+ expect(all.some((s) => s.root.requestedByUserId == null)).toBe(true);
191
+ expect(all.some((s) => s.root.requestedByUserId === userA.id)).toBe(true);
192
+ expect(all.some((s) => s.root.requestedByUserId === userB.id)).toBe(true);
193
+ });
141
194
  });