@desplega.ai/agent-swarm 1.68.0 โ†’ 1.69.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.
Files changed (41) hide show
  1. package/README.md +3 -3
  2. package/openapi.json +349 -1
  3. package/package.json +1 -1
  4. package/src/agentmail/handlers.ts +87 -6
  5. package/src/be/db-queries/mcp-oauth.ts +456 -0
  6. package/src/be/db.ts +44 -3
  7. package/src/be/migrations/041_mcp_oauth_tokens.sql +84 -0
  8. package/src/be/migrations/042_task_context_key.sql +13 -0
  9. package/src/github/handlers.ts +42 -10
  10. package/src/gitlab/handlers.ts +29 -5
  11. package/src/http/index.ts +8 -0
  12. package/src/http/mcp-oauth.ts +648 -0
  13. package/src/http/mcp-servers.ts +78 -48
  14. package/src/http/schedules.ts +4 -2
  15. package/src/http/tasks.ts +4 -2
  16. package/src/linear/sync.ts +22 -10
  17. package/src/oauth/ensure-mcp-token.ts +87 -0
  18. package/src/oauth/mcp-wrapper.ts +411 -0
  19. package/src/providers/claude-adapter.ts +1 -0
  20. package/src/scheduler/scheduler.ts +9 -10
  21. package/src/slack/actions.ts +10 -9
  22. package/src/slack/assistant.ts +8 -4
  23. package/src/slack/handlers.ts +8 -3
  24. package/src/slack/thread-buffer.ts +61 -72
  25. package/src/tasks/additive-buffer.ts +152 -0
  26. package/src/tasks/additive-ingress.ts +125 -0
  27. package/src/tasks/context-key.ts +245 -0
  28. package/src/tasks/sibling-awareness.ts +144 -0
  29. package/src/tasks/sibling-block.ts +164 -0
  30. package/src/tests/additive-buffer.test.ts +186 -0
  31. package/src/tests/additive-ingress.test.ts +111 -0
  32. package/src/tests/context-key-db.test.ts +87 -0
  33. package/src/tests/context-key.test.ts +173 -0
  34. package/src/tests/mcp-oauth-ensure-token.test.ts +190 -0
  35. package/src/tests/mcp-oauth-queries.test.ts +241 -0
  36. package/src/tests/mcp-oauth-resolve-secrets.test.ts +224 -0
  37. package/src/tests/mcp-oauth-wrapper.test.ts +477 -0
  38. package/src/tests/sibling-awareness-db.test.ts +172 -0
  39. package/src/tests/sibling-block.test.ts +232 -0
  40. package/src/types.ts +9 -0
  41. package/src/workflows/executors/agent-task.ts +21 -14
package/README.md CHANGED
@@ -31,8 +31,8 @@
31
31
  <a href="https://discord.gg/KZgfyyDVZa">
32
32
  <img src="https://img.shields.io/badge/Discord-Join%20Community-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Join Discord">
33
33
  </a>
34
- <a href="https://x.com/swarm_lead">
35
- <img src="https://img.shields.io/badge/๐•-@swarm__lead-000?style=for-the-badge&logo=x&logoColor=white" alt="Follow on X">
34
+ <a href="https://x.com/desplegalabs">
35
+ <img src="https://img.shields.io/badge/๐•-@desplegalabs-000?style=for-the-badge&logo=x&logoColor=white" alt="Follow on X">
36
36
  </a>
37
37
  </p>
38
38
 
@@ -89,7 +89,7 @@ flowchart LR
89
89
  - **Multi-channel inputs** โ€” Slack, GitHub, GitLab, email, Linear, and the HTTP API all create tasks. [Integrations](#integrations)
90
90
  - **Workflow engine with Human-in-the-Loop** โ€” DAG-based automation with approval gates, retries, and structured I/O. [Workflows โ†’](https://docs.agent-swarm.dev/docs/concepts/workflows)
91
91
  - **Scheduled & recurring tasks** โ€” cron-based automation for standing work. [Scheduling โ†’](https://docs.agent-swarm.dev/docs/concepts/scheduling)
92
- - **Multi-provider** โ€” run with Claude Code, OpenAI Codex, or pi-mono. [Harness config โ†’](https://docs.agent-swarm.dev/docs/guides/harness-configuration)
92
+ - **Multi-provider** โ€” run with Claude Code, OpenAI Codex, or pi-mono. [Harness config โ†’](https://docs.agent-swarm.dev/docs/guides/harness-configuration) ยท [Add a new provider โ†’](https://docs.agent-swarm.dev/docs/guides/harness-providers)
93
93
  - **Skills & MCP servers** โ€” reusable procedural knowledge and per-agent MCP servers with scope cascade. [MCP tools โ†’](https://docs.agent-swarm.dev/docs/reference/mcp-tools)
94
94
  - **Real-time dashboard** โ€” monitor agents, tasks, and inter-agent chat. [app.agent-swarm.dev โ†’](https://app.agent-swarm.dev)
95
95
 
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.68.0",
5
+ "version": "1.69.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": [
@@ -4724,6 +4724,351 @@
4724
4724
  }
4725
4725
  }
4726
4726
  },
4727
+ "/api/mcp-oauth/{mcpServerId}/metadata": {
4728
+ "get": {
4729
+ "summary": "Probe OAuth metadata (PRMD + AS) for an MCP server",
4730
+ "tags": [
4731
+ "MCP OAuth"
4732
+ ],
4733
+ "security": [
4734
+ {
4735
+ "bearerAuth": []
4736
+ }
4737
+ ],
4738
+ "parameters": [
4739
+ {
4740
+ "schema": {
4741
+ "type": "string"
4742
+ },
4743
+ "required": true,
4744
+ "name": "mcpServerId",
4745
+ "in": "path"
4746
+ }
4747
+ ],
4748
+ "responses": {
4749
+ "200": {
4750
+ "description": "OAuth metadata or { requiresOAuth: false }"
4751
+ },
4752
+ "400": {
4753
+ "description": "MCP has no URL / invalid transport"
4754
+ },
4755
+ "404": {
4756
+ "description": "MCP server not found"
4757
+ }
4758
+ }
4759
+ }
4760
+ },
4761
+ "/api/mcp-oauth/{mcpServerId}/status": {
4762
+ "get": {
4763
+ "summary": "Get the current OAuth connection status for an MCP server",
4764
+ "tags": [
4765
+ "MCP OAuth"
4766
+ ],
4767
+ "security": [
4768
+ {
4769
+ "bearerAuth": []
4770
+ }
4771
+ ],
4772
+ "parameters": [
4773
+ {
4774
+ "schema": {
4775
+ "type": "string"
4776
+ },
4777
+ "required": true,
4778
+ "name": "mcpServerId",
4779
+ "in": "path"
4780
+ },
4781
+ {
4782
+ "schema": {
4783
+ "type": "string"
4784
+ },
4785
+ "required": false,
4786
+ "name": "userId",
4787
+ "in": "query"
4788
+ }
4789
+ ],
4790
+ "responses": {
4791
+ "200": {
4792
+ "description": "Token status (never includes the token value itself)"
4793
+ },
4794
+ "404": {
4795
+ "description": "MCP server not found"
4796
+ }
4797
+ }
4798
+ }
4799
+ },
4800
+ "/api/mcp-oauth/{mcpServerId}/authorize": {
4801
+ "get": {
4802
+ "summary": "Start an OAuth flow. Redirects to the provider.",
4803
+ "tags": [
4804
+ "MCP OAuth"
4805
+ ],
4806
+ "security": [
4807
+ {
4808
+ "bearerAuth": []
4809
+ }
4810
+ ],
4811
+ "parameters": [
4812
+ {
4813
+ "schema": {
4814
+ "type": "string"
4815
+ },
4816
+ "required": true,
4817
+ "name": "mcpServerId",
4818
+ "in": "path"
4819
+ },
4820
+ {
4821
+ "schema": {
4822
+ "type": "string"
4823
+ },
4824
+ "required": false,
4825
+ "name": "redirect",
4826
+ "in": "query"
4827
+ },
4828
+ {
4829
+ "schema": {
4830
+ "type": "string"
4831
+ },
4832
+ "required": false,
4833
+ "name": "userId",
4834
+ "in": "query"
4835
+ },
4836
+ {
4837
+ "schema": {
4838
+ "type": "string"
4839
+ },
4840
+ "required": false,
4841
+ "name": "scopes",
4842
+ "in": "query"
4843
+ }
4844
+ ],
4845
+ "responses": {
4846
+ "302": {
4847
+ "description": "Redirect to authorization server"
4848
+ },
4849
+ "400": {
4850
+ "description": "MCP has no URL / does not require OAuth"
4851
+ },
4852
+ "404": {
4853
+ "description": "MCP server not found"
4854
+ }
4855
+ }
4856
+ }
4857
+ },
4858
+ "/api/mcp-oauth/callback": {
4859
+ "get": {
4860
+ "summary": "OAuth redirect target. Exchanges code -> tokens and redirects back to dashboard.",
4861
+ "tags": [
4862
+ "MCP OAuth"
4863
+ ],
4864
+ "parameters": [
4865
+ {
4866
+ "schema": {
4867
+ "type": "string"
4868
+ },
4869
+ "required": false,
4870
+ "name": "code",
4871
+ "in": "query"
4872
+ },
4873
+ {
4874
+ "schema": {
4875
+ "type": "string"
4876
+ },
4877
+ "required": false,
4878
+ "name": "state",
4879
+ "in": "query"
4880
+ },
4881
+ {
4882
+ "schema": {
4883
+ "type": "string"
4884
+ },
4885
+ "required": false,
4886
+ "name": "error",
4887
+ "in": "query"
4888
+ },
4889
+ {
4890
+ "schema": {
4891
+ "type": "string"
4892
+ },
4893
+ "required": false,
4894
+ "name": "error_description",
4895
+ "in": "query"
4896
+ }
4897
+ ],
4898
+ "responses": {
4899
+ "302": {
4900
+ "description": "Redirect back to dashboard with oauth=success or oauth=error"
4901
+ },
4902
+ "400": {
4903
+ "description": "Bad state / missing code"
4904
+ }
4905
+ }
4906
+ }
4907
+ },
4908
+ "/api/mcp-oauth/{mcpServerId}/refresh": {
4909
+ "post": {
4910
+ "summary": "Force-refresh the access token for an MCP server",
4911
+ "tags": [
4912
+ "MCP OAuth"
4913
+ ],
4914
+ "security": [
4915
+ {
4916
+ "bearerAuth": []
4917
+ }
4918
+ ],
4919
+ "parameters": [
4920
+ {
4921
+ "schema": {
4922
+ "type": "string"
4923
+ },
4924
+ "required": true,
4925
+ "name": "mcpServerId",
4926
+ "in": "path"
4927
+ }
4928
+ ],
4929
+ "requestBody": {
4930
+ "content": {
4931
+ "application/json": {
4932
+ "schema": {
4933
+ "type": "object",
4934
+ "properties": {
4935
+ "userId": {
4936
+ "type": "string"
4937
+ }
4938
+ }
4939
+ }
4940
+ }
4941
+ }
4942
+ },
4943
+ "responses": {
4944
+ "200": {
4945
+ "description": "Refreshed token"
4946
+ },
4947
+ "404": {
4948
+ "description": "No token for this MCP server"
4949
+ },
4950
+ "500": {
4951
+ "description": "Refresh failed"
4952
+ }
4953
+ }
4954
+ }
4955
+ },
4956
+ "/api/mcp-oauth/{mcpServerId}": {
4957
+ "delete": {
4958
+ "summary": "Revoke and delete the OAuth token for an MCP server",
4959
+ "tags": [
4960
+ "MCP OAuth"
4961
+ ],
4962
+ "security": [
4963
+ {
4964
+ "bearerAuth": []
4965
+ }
4966
+ ],
4967
+ "parameters": [
4968
+ {
4969
+ "schema": {
4970
+ "type": "string"
4971
+ },
4972
+ "required": true,
4973
+ "name": "mcpServerId",
4974
+ "in": "path"
4975
+ },
4976
+ {
4977
+ "schema": {
4978
+ "type": "string"
4979
+ },
4980
+ "required": false,
4981
+ "name": "userId",
4982
+ "in": "query"
4983
+ }
4984
+ ],
4985
+ "responses": {
4986
+ "200": {
4987
+ "description": "Token revoked/deleted"
4988
+ },
4989
+ "404": {
4990
+ "description": "No token for this MCP server"
4991
+ }
4992
+ }
4993
+ }
4994
+ },
4995
+ "/api/mcp-oauth/{mcpServerId}/manual-client": {
4996
+ "post": {
4997
+ "summary": "Register a pre-existing OAuth client (DCR fallback)",
4998
+ "tags": [
4999
+ "MCP OAuth"
5000
+ ],
5001
+ "security": [
5002
+ {
5003
+ "bearerAuth": []
5004
+ }
5005
+ ],
5006
+ "parameters": [
5007
+ {
5008
+ "schema": {
5009
+ "type": "string"
5010
+ },
5011
+ "required": true,
5012
+ "name": "mcpServerId",
5013
+ "in": "path"
5014
+ }
5015
+ ],
5016
+ "requestBody": {
5017
+ "content": {
5018
+ "application/json": {
5019
+ "schema": {
5020
+ "type": "object",
5021
+ "properties": {
5022
+ "clientId": {
5023
+ "type": "string",
5024
+ "minLength": 1
5025
+ },
5026
+ "clientSecret": {
5027
+ "type": "string"
5028
+ },
5029
+ "authorizationServerIssuer": {
5030
+ "type": "string",
5031
+ "format": "uri"
5032
+ },
5033
+ "authorizeUrl": {
5034
+ "type": "string",
5035
+ "format": "uri"
5036
+ },
5037
+ "tokenUrl": {
5038
+ "type": "string",
5039
+ "format": "uri"
5040
+ },
5041
+ "revocationUrl": {
5042
+ "type": "string",
5043
+ "format": "uri"
5044
+ },
5045
+ "scopes": {
5046
+ "type": "array",
5047
+ "items": {
5048
+ "type": "string"
5049
+ }
5050
+ }
5051
+ },
5052
+ "required": [
5053
+ "clientId"
5054
+ ]
5055
+ }
5056
+ }
5057
+ }
5058
+ },
5059
+ "responses": {
5060
+ "200": {
5061
+ "description": "Pending client stored. Call /authorize to start the flow."
5062
+ },
5063
+ "400": {
5064
+ "description": "Bad input"
5065
+ },
5066
+ "404": {
5067
+ "description": "MCP server not found"
5068
+ }
5069
+ }
5070
+ }
5071
+ },
4727
5072
  "/api/mcp-servers": {
4728
5073
  "get": {
4729
5074
  "summary": "List MCP servers with optional filters",
@@ -5399,6 +5744,9 @@
5399
5744
  "outputSchema": {
5400
5745
  "type": "object",
5401
5746
  "additionalProperties": {}
5747
+ },
5748
+ "contextKey": {
5749
+ "type": "string"
5402
5750
  }
5403
5751
  },
5404
5752
  "required": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.68.0",
3
+ "version": "1.69.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>",
@@ -1,5 +1,4 @@
1
1
  import {
2
- createTaskExtended,
3
2
  findTaskByAgentMailThread,
4
3
  getAgentById,
5
4
  getAgentMailInboxMapping,
@@ -7,11 +6,63 @@ import {
7
6
  resolveUser,
8
7
  } from "../be/db";
9
8
  import { resolveTemplate } from "../prompts/resolver";
9
+ import { createIngressBuffer } from "../tasks/additive-ingress";
10
+ import { agentmailContextKey } from "../tasks/context-key";
11
+ import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
10
12
  import { workflowEventBus } from "../workflows/event-bus";
11
13
  // Side-effect import: registers all AgentMail event templates in the in-memory registry
12
14
  import "./templates";
13
15
  import type { AgentMailMessage, AgentMailWebhookPayload } from "./types";
14
16
 
17
+ const ACTIVE_TASK_STATUSES = new Set(["pending", "in_progress", "offered", "paused"]);
18
+
19
+ interface BufferedAgentMailMessage {
20
+ from: string;
21
+ subject: string;
22
+ inboxId: string;
23
+ threadId: string;
24
+ messageId: string;
25
+ preview: string;
26
+ agentId: string | null;
27
+ parentTaskId: string;
28
+ requestedByUserId: string | undefined;
29
+ }
30
+
31
+ const AGENTMAIL_BUFFER_TIMEOUT_MS = Number(process.env.ADDITIVE_AGENTMAIL_BUFFER_MS) || 10_000;
32
+
33
+ const agentmailBuffer = createIngressBuffer<BufferedAgentMailMessage>({
34
+ source: "agentmail",
35
+ envFlag: "ADDITIVE_AGENTMAIL",
36
+ timeoutMs: AGENTMAIL_BUFFER_TIMEOUT_MS,
37
+ onFlush: (items, contextKey) => {
38
+ if (items.length === 0) return;
39
+ const first = items[0]!;
40
+ const combinedPreview = items.map((m) => m.preview).join("\n---\n");
41
+ const followupResult = resolveTemplate("agentmail.email.followup", {
42
+ from: first.from,
43
+ subject: first.subject,
44
+ inbox_id: first.inboxId,
45
+ thread_id: first.threadId,
46
+ preview: `[${items.length} buffered message(s)]\n\n${combinedPreview}`,
47
+ });
48
+ if (followupResult.skipped) return;
49
+ const task = createTaskWithSiblingAwareness(followupResult.text, {
50
+ agentId: first.agentId,
51
+ source: "agentmail",
52
+ taskType: "agentmail-reply",
53
+ agentmailInboxId: first.inboxId,
54
+ agentmailMessageId: first.messageId,
55
+ agentmailThreadId: first.threadId,
56
+ parentTaskId: first.parentTaskId,
57
+ requestedByUserId: first.requestedByUserId,
58
+ contextKey,
59
+ });
60
+ console.log(
61
+ `[AgentMail] Buffered flush โ†’ task ${task.id} (${items.length} messages, thread ${first.threadId})`,
62
+ );
63
+ },
64
+ });
65
+
15
66
  /**
16
67
  * Extract bare email address from a from_ field like "Taras Yarema <t@desplega.ai>" or "t@desplega.ai".
17
68
  */
@@ -126,6 +177,31 @@ export async function handleMessageReceived(
126
177
  // Check for thread continuity - find existing task for this thread
127
178
  const existingTask = findTaskByAgentMailThread(thread_id);
128
179
  if (existingTask) {
180
+ const contextKey = agentmailContextKey({ threadId: thread_id });
181
+ const siblingInFlight = ACTIVE_TASK_STATUSES.has(existingTask.status);
182
+
183
+ // Opt-in: when ADDITIVE_AGENTMAIL is true, buffer rapid follow-ups while
184
+ // the prior task is still running โ€” coalesce into ONE follow-up task.
185
+ if (
186
+ agentmailBuffer.enabled &&
187
+ agentmailBuffer.maybeBuffer(contextKey, siblingInFlight, {
188
+ from,
189
+ subject,
190
+ inboxId: inbox_id,
191
+ threadId: thread_id,
192
+ messageId: message_id,
193
+ preview,
194
+ agentId: existingTask.agentId,
195
+ parentTaskId: existingTask.id,
196
+ requestedByUserId,
197
+ })
198
+ ) {
199
+ console.log(
200
+ `[AgentMail] Buffered follow-up for thread ${thread_id} (parent ${existingTask.id}, status ${existingTask.status})`,
201
+ );
202
+ return { created: false };
203
+ }
204
+
129
205
  // Create a follow-up task with parentTaskId to continue the session
130
206
  const followupResult = resolveTemplate("agentmail.email.followup", {
131
207
  from,
@@ -139,7 +215,7 @@ export async function handleMessageReceived(
139
215
  return { created: false };
140
216
  }
141
217
 
142
- const task = createTaskExtended(followupResult.text, {
218
+ const task = createTaskWithSiblingAwareness(followupResult.text, {
143
219
  agentId: existingTask.agentId,
144
220
  source: "agentmail",
145
221
  taskType: "agentmail-reply",
@@ -148,6 +224,7 @@ export async function handleMessageReceived(
148
224
  agentmailThreadId: thread_id,
149
225
  parentTaskId: existingTask.id,
150
226
  requestedByUserId,
227
+ contextKey,
151
228
  });
152
229
 
153
230
  console.log(
@@ -177,7 +254,7 @@ export async function handleMessageReceived(
177
254
  return { created: false };
178
255
  }
179
256
 
180
- const task = createTaskExtended(leadResult.text, {
257
+ const task = createTaskWithSiblingAwareness(leadResult.text, {
181
258
  agentId: agent.id,
182
259
  source: "agentmail",
183
260
  taskType: "agentmail-message",
@@ -185,6 +262,7 @@ export async function handleMessageReceived(
185
262
  agentmailMessageId: message_id,
186
263
  agentmailThreadId: thread_id,
187
264
  requestedByUserId,
265
+ contextKey: agentmailContextKey({ threadId: thread_id }),
188
266
  });
189
267
 
190
268
  console.log(
@@ -206,7 +284,7 @@ export async function handleMessageReceived(
206
284
  return { created: false };
207
285
  }
208
286
 
209
- const task = createTaskExtended(workerResult.text, {
287
+ const task = createTaskWithSiblingAwareness(workerResult.text, {
210
288
  agentId: agent.id,
211
289
  source: "agentmail",
212
290
  taskType: "agentmail-message",
@@ -214,6 +292,7 @@ export async function handleMessageReceived(
214
292
  agentmailMessageId: message_id,
215
293
  agentmailThreadId: thread_id,
216
294
  requestedByUserId,
295
+ contextKey: agentmailContextKey({ threadId: thread_id }),
217
296
  });
218
297
 
219
298
  console.log(
@@ -239,7 +318,7 @@ export async function handleMessageReceived(
239
318
  return { created: false };
240
319
  }
241
320
 
242
- const task = createTaskExtended(unmappedResult.text, {
321
+ const task = createTaskWithSiblingAwareness(unmappedResult.text, {
243
322
  agentId: lead.id,
244
323
  source: "agentmail",
245
324
  taskType: "agentmail-message",
@@ -247,6 +326,7 @@ export async function handleMessageReceived(
247
326
  agentmailMessageId: message_id,
248
327
  agentmailThreadId: thread_id,
249
328
  requestedByUserId,
329
+ contextKey: agentmailContextKey({ threadId: thread_id }),
250
330
  });
251
331
 
252
332
  console.log(
@@ -268,13 +348,14 @@ export async function handleMessageReceived(
268
348
  return { created: false };
269
349
  }
270
350
 
271
- const task = createTaskExtended(noAgentResult.text, {
351
+ const task = createTaskWithSiblingAwareness(noAgentResult.text, {
272
352
  source: "agentmail",
273
353
  taskType: "agentmail-message",
274
354
  agentmailInboxId: inbox_id,
275
355
  agentmailMessageId: message_id,
276
356
  agentmailThreadId: thread_id,
277
357
  requestedByUserId,
358
+ contextKey: agentmailContextKey({ threadId: thread_id }),
278
359
  });
279
360
 
280
361
  console.log(`[AgentMail] Created unassigned task ${task.id} (no lead or mapping available)`);