@desplega.ai/agent-swarm 1.69.0 โ†’ 1.70.0

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 (44) hide show
  1. package/README.md +3 -3
  2. package/openapi.json +62 -1
  3. package/package.json +1 -1
  4. package/src/agentmail/handlers.ts +87 -6
  5. package/src/be/db.ts +34 -2
  6. package/src/be/migrations/042_task_context_key.sql +13 -0
  7. package/src/commands/runner.ts +1 -0
  8. package/src/github/handlers.ts +42 -10
  9. package/src/gitlab/handlers.ts +29 -5
  10. package/src/hooks/hook.ts +4 -2
  11. package/src/http/core.ts +36 -26
  12. package/src/http/mcp-oauth.ts +132 -60
  13. package/src/http/mcp-servers.ts +5 -1
  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/providers/claude-adapter.ts +51 -29
  18. package/src/scheduler/scheduler.ts +9 -10
  19. package/src/server.ts +2 -0
  20. package/src/slack/actions.ts +10 -9
  21. package/src/slack/assistant.ts +8 -4
  22. package/src/slack/handlers.ts +8 -3
  23. package/src/slack/thread-buffer.ts +61 -72
  24. package/src/tasks/additive-buffer.ts +152 -0
  25. package/src/tasks/additive-ingress.ts +125 -0
  26. package/src/tasks/context-key.ts +245 -0
  27. package/src/tasks/sibling-awareness.ts +144 -0
  28. package/src/tasks/sibling-block.ts +164 -0
  29. package/src/tests/additive-buffer.test.ts +186 -0
  30. package/src/tests/additive-ingress.test.ts +111 -0
  31. package/src/tests/claude-adapter.test.ts +143 -1
  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/core-auth.test.ts +142 -0
  35. package/src/tests/mcp-oauth-resolve-secrets.test.ts +79 -0
  36. package/src/tests/sibling-awareness-db.test.ts +172 -0
  37. package/src/tests/sibling-block.test.ts +232 -0
  38. package/src/tests/tool-annotations.test.ts +1 -0
  39. package/src/tools/slack-post.ts +10 -3
  40. package/src/tools/slack-start-thread.ts +123 -0
  41. package/src/tools/tool-config.ts +2 -1
  42. package/src/tools/update-profile.ts +5 -2
  43. package/src/types.ts +5 -0
  44. 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.69.0",
5
+ "version": "1.70.0",
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": [
@@ -4855,6 +4855,64 @@
4855
4855
  }
4856
4856
  }
4857
4857
  },
4858
+ "/api/mcp-oauth/{mcpServerId}/authorize-url": {
4859
+ "get": {
4860
+ "summary": "Build an OAuth authorize URL. Returns JSON so the browser can navigate without losing the Bearer auth header.",
4861
+ "tags": [
4862
+ "MCP OAuth"
4863
+ ],
4864
+ "security": [
4865
+ {
4866
+ "bearerAuth": []
4867
+ }
4868
+ ],
4869
+ "parameters": [
4870
+ {
4871
+ "schema": {
4872
+ "type": "string"
4873
+ },
4874
+ "required": true,
4875
+ "name": "mcpServerId",
4876
+ "in": "path"
4877
+ },
4878
+ {
4879
+ "schema": {
4880
+ "type": "string"
4881
+ },
4882
+ "required": false,
4883
+ "name": "redirect",
4884
+ "in": "query"
4885
+ },
4886
+ {
4887
+ "schema": {
4888
+ "type": "string"
4889
+ },
4890
+ "required": false,
4891
+ "name": "userId",
4892
+ "in": "query"
4893
+ },
4894
+ {
4895
+ "schema": {
4896
+ "type": "string"
4897
+ },
4898
+ "required": false,
4899
+ "name": "scopes",
4900
+ "in": "query"
4901
+ }
4902
+ ],
4903
+ "responses": {
4904
+ "200": {
4905
+ "description": "{ providerUrl: string }"
4906
+ },
4907
+ "400": {
4908
+ "description": "MCP has no URL / does not require OAuth"
4909
+ },
4910
+ "404": {
4911
+ "description": "MCP server not found"
4912
+ }
4913
+ }
4914
+ }
4915
+ },
4858
4916
  "/api/mcp-oauth/callback": {
4859
4917
  "get": {
4860
4918
  "summary": "OAuth redirect target. Exchanges code -> tokens and redirects back to dashboard.",
@@ -5744,6 +5802,9 @@
5744
5802
  "outputSchema": {
5745
5803
  "type": "object",
5746
5804
  "additionalProperties": {}
5805
+ },
5806
+ "contextKey": {
5807
+ "type": "string"
5747
5808
  }
5748
5809
  },
5749
5810
  "required": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.69.0",
3
+ "version": "1.70.0",
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)`);
package/src/be/db.ts CHANGED
@@ -802,6 +802,7 @@ type AgentTaskRow = {
802
802
  workflowRunId: string | null;
803
803
  workflowRunStepId: string | null;
804
804
  outputSchema: string | null;
805
+ contextKey: string | null;
805
806
  createdAt: string;
806
807
  lastUpdatedAt: string;
807
808
  finishedAt: string | null;
@@ -862,6 +863,7 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
862
863
  workflowRunId: row.workflowRunId ?? undefined,
863
864
  workflowRunStepId: row.workflowRunStepId ?? undefined,
864
865
  outputSchema: row.outputSchema ? JSON.parse(row.outputSchema) : undefined,
866
+ contextKey: row.contextKey ?? undefined,
865
867
  compactionCount: row.compactionCount ?? undefined,
866
868
  peakContextPercent: row.peakContextPercent ?? undefined,
867
869
  totalContextTokensUsed: row.totalContextTokensUsed ?? undefined,
@@ -1430,6 +1432,31 @@ export function getInProgressSlackTasks(): AgentTask[] {
1430
1432
  .map(rowToAgentTask);
1431
1433
  }
1432
1434
 
1435
+ /**
1436
+ * Return sibling tasks for a given cross-ingress context key, optionally
1437
+ * filtered by status. The returned shape mirrors getInProgressSlackTasks for
1438
+ * consistency; callers can narrow further in TypeScript.
1439
+ *
1440
+ * See src/tasks/context-key.ts for the key schema.
1441
+ */
1442
+ export function getInProgressTasksByContextKey(
1443
+ contextKey: string,
1444
+ statuses: AgentTaskStatus[] = ["pending", "in_progress", "offered", "paused"],
1445
+ ): AgentTask[] {
1446
+ if (!contextKey || statuses.length === 0) return [];
1447
+ const placeholders = statuses.map(() => "?").join(",");
1448
+ return getDb()
1449
+ .prepare<AgentTaskRow, (string | AgentTaskStatus)[]>(
1450
+ `SELECT * FROM agent_tasks
1451
+ WHERE contextKey = ?
1452
+ AND status IN (${placeholders})
1453
+ ORDER BY lastUpdatedAt DESC
1454
+ LIMIT 200`,
1455
+ )
1456
+ .all(contextKey, ...statuses)
1457
+ .map(rowToAgentTask);
1458
+ }
1459
+
1433
1460
  /**
1434
1461
  * Find the most recent agent associated with a specific Slack thread.
1435
1462
  * No status filter โ€” returns the last agent that touched this thread regardless of task state.
@@ -1934,6 +1961,7 @@ export interface CreateTaskOptions {
1934
1961
  sourceTaskId?: string;
1935
1962
  outputSchema?: Record<string, unknown>;
1936
1963
  requestedByUserId?: string;
1964
+ contextKey?: string;
1937
1965
  }
1938
1966
 
1939
1967
  /**
@@ -2004,6 +2032,9 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
2004
2032
  if (parent.requestedByUserId && !options.requestedByUserId) {
2005
2033
  options.requestedByUserId = parent.requestedByUserId;
2006
2034
  }
2035
+ if (parent.contextKey && !options.contextKey) {
2036
+ options.contextKey = parent.contextKey;
2037
+ }
2007
2038
  }
2008
2039
  }
2009
2040
 
@@ -2029,8 +2060,8 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
2029
2060
  vcsInstallationId, vcsNodeId,
2030
2061
  agentmailInboxId, agentmailMessageId, agentmailThreadId,
2031
2062
  mentionMessageId, mentionChannelId, dir, parentTaskId, model, scheduleId,
2032
- workflowRunId, workflowRunStepId, outputSchema, requestedByUserId, swarmVersion, createdAt, lastUpdatedAt
2033
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
2063
+ workflowRunId, workflowRunStepId, outputSchema, requestedByUserId, contextKey, swarmVersion, createdAt, lastUpdatedAt
2064
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
2034
2065
  )
2035
2066
  .get(
2036
2067
  id,
@@ -2070,6 +2101,7 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
2070
2101
  options?.workflowRunStepId ?? null,
2071
2102
  options?.outputSchema ? JSON.stringify(options.outputSchema) : null,
2072
2103
  options?.requestedByUserId ?? null,
2104
+ options?.contextKey ?? null,
2073
2105
  pkg.version,
2074
2106
  now,
2075
2107
  now,
@@ -0,0 +1,13 @@
1
+ -- Add a uniform context_key column on tasks for cross-ingress sibling awareness.
2
+ -- See src/tasks/context-key.ts for the key schema.
3
+ -- Nullable: historical rows stay NULL, new ingress paths populate it going forward.
4
+ ALTER TABLE agent_tasks ADD COLUMN contextKey TEXT;
5
+
6
+ -- Plain btree index for generic lookups by context key.
7
+ CREATE INDEX IF NOT EXISTS idx_agent_tasks_context_key
8
+ ON agent_tasks(contextKey);
9
+
10
+ -- Composite index supporting the "in-progress sibling" lookup pattern:
11
+ -- WHERE contextKey = ? AND status IN (...).
12
+ CREATE INDEX IF NOT EXISTS idx_agent_tasks_context_key_status
13
+ ON agent_tasks(contextKey, status);
@@ -272,6 +272,7 @@ const SWARM_TOOL_LABELS: Record<string, string | null> = {
272
272
  "update-profile": "๐Ÿชช Updating profile",
273
273
  // Slack
274
274
  "slack-post": "๐Ÿ’ฌ Posting to Slack",
275
+ "slack-start-thread": "๐Ÿ’ฌ Starting Slack thread",
275
276
  "slack-reply": "๐Ÿ’ฌ Replying in Slack",
276
277
  "slack-read": "๐Ÿ’ฌ Reading Slack",
277
278
  "slack-list-channels": "๐Ÿ’ฌ Listing Slack channels",
@@ -1,5 +1,7 @@
1
- import { createTaskExtended, failTask, findTaskByVcs, getAllAgents, resolveUser } from "../be/db";
1
+ import { failTask, findTaskByVcs, getAllAgents, resolveUser } from "../be/db";
2
2
  import { resolveTemplate } from "../prompts/resolver";
3
+ import { githubContextKey } from "../tasks/context-key";
4
+ import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
3
5
  import {
4
6
  detectMention,
5
7
  extractMentionContext,
@@ -24,6 +26,25 @@ import type {
24
26
  const processedEvents = new Map<string, number>();
25
27
  const EVENT_TTL = 60_000;
26
28
 
29
+ /**
30
+ * Build a uniform cross-ingress context key for a GitHub issue or PR.
31
+ * `repository.full_name` is "owner/repo"; split it and fall back gracefully
32
+ * if the split unexpectedly fails so we never block task creation on a bad key.
33
+ */
34
+ function buildGithubContextKey(
35
+ fullName: string,
36
+ kind: "issue" | "pr",
37
+ number: number,
38
+ ): string | undefined {
39
+ const [owner, repo] = fullName.split("/");
40
+ if (!owner || !repo) return undefined;
41
+ try {
42
+ return githubContextKey({ owner, repo, kind, number });
43
+ } catch {
44
+ return undefined;
45
+ }
46
+ }
47
+
27
48
  /**
28
49
  * Get review state emoji and label
29
50
  */
@@ -172,7 +193,7 @@ export async function handlePullRequest(
172
193
  return { created: false };
173
194
  }
174
195
 
175
- const task = createTaskExtended(result.text, {
196
+ const task = createTaskWithSiblingAwareness(result.text, {
176
197
  agentId: lead?.id ?? "",
177
198
  source: "github",
178
199
  vcsProvider: "github",
@@ -184,6 +205,7 @@ export async function handlePullRequest(
184
205
  requestedByUserId,
185
206
  vcsUrl: pr.html_url,
186
207
  vcsInstallationId: installation?.id,
208
+ contextKey: buildGithubContextKey(repository.full_name, "pr", pr.number),
187
209
  });
188
210
 
189
211
  if (lead) {
@@ -271,7 +293,7 @@ export async function handlePullRequest(
271
293
  return { created: false };
272
294
  }
273
295
 
274
- const task = createTaskExtended(result.text, {
296
+ const task = createTaskWithSiblingAwareness(result.text, {
275
297
  agentId: lead?.id ?? "",
276
298
  source: "github",
277
299
  vcsProvider: "github",
@@ -283,6 +305,7 @@ export async function handlePullRequest(
283
305
  requestedByUserId,
284
306
  vcsUrl: pr.html_url,
285
307
  vcsInstallationId: installation?.id,
308
+ contextKey: buildGithubContextKey(repository.full_name, "pr", pr.number),
286
309
  });
287
310
 
288
311
  if (lead) {
@@ -362,7 +385,7 @@ export async function handlePullRequest(
362
385
  return { created: false };
363
386
  }
364
387
 
365
- const task = createTaskExtended(result.text, {
388
+ const task = createTaskWithSiblingAwareness(result.text, {
366
389
  agentId: lead?.id ?? "",
367
390
  source: "github",
368
391
  vcsProvider: "github",
@@ -374,6 +397,7 @@ export async function handlePullRequest(
374
397
  requestedByUserId,
375
398
  vcsUrl: pr.html_url,
376
399
  vcsInstallationId: installation?.id,
400
+ contextKey: buildGithubContextKey(repository.full_name, "pr", pr.number),
377
401
  });
378
402
 
379
403
  if (lead) {
@@ -451,7 +475,7 @@ export async function handlePullRequest(
451
475
  }
452
476
 
453
477
  // Create task (assigned to lead if available, otherwise unassigned)
454
- const task = createTaskExtended(result.text, {
478
+ const task = createTaskWithSiblingAwareness(result.text, {
455
479
  agentId: lead?.id ?? "",
456
480
  source: "github",
457
481
  vcsProvider: "github",
@@ -462,6 +486,7 @@ export async function handlePullRequest(
462
486
  vcsAuthor: sender.login,
463
487
  vcsUrl: pr.html_url,
464
488
  vcsInstallationId: installation?.id,
489
+ contextKey: buildGithubContextKey(repository.full_name, "pr", pr.number),
465
490
  });
466
491
 
467
492
  if (lead) {
@@ -524,7 +549,7 @@ export async function handleIssue(
524
549
  return { created: false };
525
550
  }
526
551
 
527
- const task = createTaskExtended(result.text, {
552
+ const task = createTaskWithSiblingAwareness(result.text, {
528
553
  agentId: lead?.id ?? "",
529
554
  source: "github",
530
555
  vcsProvider: "github",
@@ -536,6 +561,7 @@ export async function handleIssue(
536
561
  requestedByUserId,
537
562
  vcsUrl: issue.html_url,
538
563
  vcsInstallationId: installation?.id,
564
+ contextKey: buildGithubContextKey(repository.full_name, "issue", issue.number),
539
565
  });
540
566
 
541
567
  if (lead) {
@@ -611,7 +637,7 @@ export async function handleIssue(
611
637
  return { created: false };
612
638
  }
613
639
 
614
- const task = createTaskExtended(result.text, {
640
+ const task = createTaskWithSiblingAwareness(result.text, {
615
641
  agentId: lead?.id ?? "",
616
642
  source: "github",
617
643
  vcsProvider: "github",
@@ -623,6 +649,7 @@ export async function handleIssue(
623
649
  requestedByUserId,
624
650
  vcsUrl: issue.html_url,
625
651
  vcsInstallationId: installation?.id,
652
+ contextKey: buildGithubContextKey(repository.full_name, "issue", issue.number),
626
653
  });
627
654
 
628
655
  if (lead) {
@@ -682,7 +709,7 @@ export async function handleIssue(
682
709
  }
683
710
 
684
711
  // Create task (assigned to lead if available, otherwise unassigned)
685
- const task = createTaskExtended(result.text, {
712
+ const task = createTaskWithSiblingAwareness(result.text, {
686
713
  agentId: lead?.id ?? "",
687
714
  source: "github",
688
715
  vcsProvider: "github",
@@ -693,6 +720,7 @@ export async function handleIssue(
693
720
  vcsAuthor: sender.login,
694
721
  vcsUrl: issue.html_url,
695
722
  vcsInstallationId: installation?.id,
723
+ contextKey: buildGithubContextKey(repository.full_name, "issue", issue.number),
696
724
  });
697
725
 
698
726
  if (lead) {
@@ -780,7 +808,7 @@ export async function handleComment(
780
808
  }
781
809
 
782
810
  // Create task (assigned to lead if available, otherwise unassigned)
783
- const task = createTaskExtended(result.text, {
811
+ const task = createTaskWithSiblingAwareness(result.text, {
784
812
  agentId: lead?.id ?? "",
785
813
  source: "github",
786
814
  vcsProvider: "github",
@@ -793,6 +821,9 @@ export async function handleComment(
793
821
  vcsUrl: targetUrl,
794
822
  vcsInstallationId: installation?.id,
795
823
  vcsNodeId: comment.node_id,
824
+ contextKey: targetNumber
825
+ ? buildGithubContextKey(repository.full_name, pull_request ? "pr" : "issue", targetNumber)
826
+ : undefined,
796
827
  });
797
828
 
798
829
  if (lead) {
@@ -895,7 +926,7 @@ export async function handlePullRequestReview(
895
926
  }
896
927
 
897
928
  // Create task (assigned to lead if available, otherwise unassigned)
898
- const task = createTaskExtended(result.text, {
929
+ const task = createTaskWithSiblingAwareness(result.text, {
899
930
  agentId: lead?.id ?? "",
900
931
  source: "github",
901
932
  vcsProvider: "github",
@@ -907,6 +938,7 @@ export async function handlePullRequestReview(
907
938
  vcsUrl: review.html_url,
908
939
  vcsInstallationId: installation?.id,
909
940
  vcsNodeId: review.node_id,
941
+ contextKey: buildGithubContextKey(repository.full_name, "pr", pr.number),
910
942
  });
911
943
 
912
944
  if (lead) {
@@ -8,8 +8,10 @@
8
8
  * - Detects bot mentions
9
9
  */
10
10
 
11
- import { createTaskExtended, failTask, findTaskByVcs, getAllAgents, resolveUser } from "../be/db";
11
+ import { failTask, findTaskByVcs, getAllAgents, resolveUser } from "../be/db";
12
12
  import { resolveTemplate } from "../prompts/resolver";
13
+ import { gitlabContextKey } from "../tasks/context-key";
14
+ import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
13
15
  import { GITLAB_BOT_NAME } from "./auth";
14
16
  import { addGitLabNoteReaction, addGitLabReaction } from "./reactions";
15
17
  // Side-effect import: registers all GitLab event templates in the in-memory registry
@@ -99,7 +101,7 @@ export async function handleMergeRequest(
99
101
  return { created: false };
100
102
  }
101
103
 
102
- const task = createTaskExtended(result.text, {
104
+ const task = createTaskWithSiblingAwareness(result.text, {
103
105
  agentId: lead?.id ?? null,
104
106
  source: "gitlab",
105
107
  vcsProvider: "gitlab",
@@ -110,6 +112,11 @@ export async function handleMergeRequest(
110
112
  vcsAuthor: user.username,
111
113
  requestedByUserId,
112
114
  vcsUrl: mr.url,
115
+ contextKey: gitlabContextKey({
116
+ projectId: String(project.id),
117
+ kind: "mr",
118
+ iid: mr.iid,
119
+ }),
113
120
  });
114
121
 
115
122
  try {
@@ -196,7 +203,7 @@ export async function handleIssue(
196
203
  return { created: false };
197
204
  }
198
205
 
199
- const task = createTaskExtended(result.text, {
206
+ const task = createTaskWithSiblingAwareness(result.text, {
200
207
  agentId: lead?.id ?? null,
201
208
  source: "gitlab",
202
209
  vcsProvider: "gitlab",
@@ -207,6 +214,11 @@ export async function handleIssue(
207
214
  vcsAuthor: user.username,
208
215
  requestedByUserId,
209
216
  vcsUrl: issue.url,
217
+ contextKey: gitlabContextKey({
218
+ projectId: String(project.id),
219
+ kind: "issue",
220
+ iid: issue.iid,
221
+ }),
210
222
  });
211
223
 
212
224
  try {
@@ -293,7 +305,7 @@ export async function handleNote(event: NoteEvent): Promise<{ created: boolean;
293
305
  return { created: false };
294
306
  }
295
307
 
296
- const task = createTaskExtended(noteResult.text, {
308
+ const task = createTaskWithSiblingAwareness(noteResult.text, {
297
309
  agentId: lead?.id ?? null,
298
310
  source: "gitlab",
299
311
  vcsProvider: "gitlab",
@@ -305,6 +317,13 @@ export async function handleNote(event: NoteEvent): Promise<{ created: boolean;
305
317
  vcsAuthor: user.username,
306
318
  vcsUrl: targetUrl,
307
319
  parentTaskId: existingTask?.id,
320
+ contextKey: targetNumber
321
+ ? gitlabContextKey({
322
+ projectId: String(project.id),
323
+ kind: note.noteable_type === "MergeRequest" ? "mr" : "issue",
324
+ iid: targetNumber,
325
+ })
326
+ : undefined,
308
327
  });
309
328
 
310
329
  try {
@@ -362,7 +381,7 @@ export async function handlePipeline(
362
381
  return { created: false };
363
382
  }
364
383
 
365
- const task = createTaskExtended(pipelineResult.text, {
384
+ const task = createTaskWithSiblingAwareness(pipelineResult.text, {
366
385
  agentId: lead?.id ?? null,
367
386
  source: "gitlab",
368
387
  vcsProvider: "gitlab",
@@ -373,6 +392,11 @@ export async function handlePipeline(
373
392
  vcsAuthor: "",
374
393
  vcsUrl: event.merge_request.url,
375
394
  parentTaskId: existingTask.id,
395
+ contextKey: gitlabContextKey({
396
+ projectId: String(project.id),
397
+ kind: "mr",
398
+ iid: mrIid,
399
+ }),
376
400
  });
377
401
 
378
402
  return { created: true, taskId: task.id };