@desplega.ai/agent-swarm 1.57.5 → 1.58.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.
- package/openapi.json +1 -1
- package/package.json +1 -1
- package/src/agentmail/handlers.ts +20 -0
- package/src/be/db.ts +265 -2
- package/src/be/migrations/031_user_registry.sql +35 -0
- package/src/commands/runner.ts +8 -1
- package/src/github/handlers.ts +18 -1
- package/src/gitlab/handlers.ts +12 -1
- package/src/http/poll.ts +9 -0
- package/src/linear/sync.ts +25 -1
- package/src/server.ts +6 -0
- package/src/slack/actions.ts +10 -1
- package/src/slack/assistant.ts +7 -0
- package/src/slack/handlers.ts +9 -2
- package/src/tests/events-http.test.ts +2 -2
- package/src/tests/http-api-integration.test.ts +3 -3
- package/src/tests/linear-outbound-sync.test.ts +7 -7
- package/src/tests/preload.ts +14 -0
- package/src/tests/rest-api.test.ts +1 -1
- package/src/tests/user-identity.test.ts +306 -0
- package/src/tests/workflow-async-v2.test.ts +7 -7
- package/src/tests/workflow-engine-v2.test.ts +3 -3
- package/src/tests/workflow-hitl-routing.test.ts +7 -7
- package/src/tests/workflow-http-v2.test.ts +1 -1
- package/src/tests/workflow-retry-v2.test.ts +4 -4
- package/src/tests/workflow-retry-validation.test.ts +3 -3
- package/src/tests/workflow-validation-port-routing.test.ts +221 -0
- package/src/tools/get-task-details.ts +14 -1
- package/src/tools/manage-user.ts +172 -0
- package/src/tools/resolve-user.ts +55 -0
- package/src/tools/tool-config.ts +4 -0
- package/src/types.ts +26 -0
- package/src/workflows/engine.ts +18 -4
- package/src/workflows/validation.ts +6 -3
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.
|
|
5
|
+
"version": "1.58.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": [
|
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
getAgentById,
|
|
5
5
|
getAgentMailInboxMapping,
|
|
6
6
|
getAllAgents,
|
|
7
|
+
resolveUser,
|
|
7
8
|
} from "../be/db";
|
|
8
9
|
import { resolveTemplate } from "../prompts/resolver";
|
|
9
10
|
import { workflowEventBus } from "../workflows/event-bus";
|
|
@@ -11,6 +12,16 @@ import { workflowEventBus } from "../workflows/event-bus";
|
|
|
11
12
|
import "./templates";
|
|
12
13
|
import type { AgentMailMessage, AgentMailWebhookPayload } from "./types";
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Extract bare email address from a from_ field like "Taras Yarema <t@desplega.ai>" or "t@desplega.ai".
|
|
17
|
+
*/
|
|
18
|
+
function extractEmailFromField(from: string): string | undefined {
|
|
19
|
+
const angleMatch = from.match(/<([^>]+@[^>]+)>/);
|
|
20
|
+
if (angleMatch?.[1]) return angleMatch[1].toLowerCase();
|
|
21
|
+
const bareMatch = from.match(/[\w.+-]+@[\w.-]+\.\w+/);
|
|
22
|
+
return bareMatch?.[0]?.toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
/**
|
|
15
26
|
* Check if an inbox domain is allowed by the filter.
|
|
16
27
|
* Returns true if no filter is set or the inbox domain matches.
|
|
@@ -96,6 +107,10 @@ export async function handleMessageReceived(
|
|
|
96
107
|
(Array.isArray(message.from_) ? message.from_.join(", ") : message.from_) || "unknown";
|
|
97
108
|
const subject = message.subject || "(no subject)";
|
|
98
109
|
const body = message.text || message.html || "";
|
|
110
|
+
|
|
111
|
+
// Resolve canonical user from sender email
|
|
112
|
+
const senderEmail = extractEmailFromField(from);
|
|
113
|
+
const requestedByUserId = senderEmail ? resolveUser({ email: senderEmail })?.id : undefined;
|
|
99
114
|
const preview = body.length > 500 ? `${body.substring(0, 500)}...` : body;
|
|
100
115
|
|
|
101
116
|
// Emit workflow trigger event
|
|
@@ -132,6 +147,7 @@ export async function handleMessageReceived(
|
|
|
132
147
|
agentmailMessageId: message_id,
|
|
133
148
|
agentmailThreadId: thread_id,
|
|
134
149
|
parentTaskId: existingTask.id,
|
|
150
|
+
requestedByUserId,
|
|
135
151
|
});
|
|
136
152
|
|
|
137
153
|
console.log(
|
|
@@ -168,6 +184,7 @@ export async function handleMessageReceived(
|
|
|
168
184
|
agentmailInboxId: inbox_id,
|
|
169
185
|
agentmailMessageId: message_id,
|
|
170
186
|
agentmailThreadId: thread_id,
|
|
187
|
+
requestedByUserId,
|
|
171
188
|
});
|
|
172
189
|
|
|
173
190
|
console.log(
|
|
@@ -196,6 +213,7 @@ export async function handleMessageReceived(
|
|
|
196
213
|
agentmailInboxId: inbox_id,
|
|
197
214
|
agentmailMessageId: message_id,
|
|
198
215
|
agentmailThreadId: thread_id,
|
|
216
|
+
requestedByUserId,
|
|
199
217
|
});
|
|
200
218
|
|
|
201
219
|
console.log(
|
|
@@ -228,6 +246,7 @@ export async function handleMessageReceived(
|
|
|
228
246
|
agentmailInboxId: inbox_id,
|
|
229
247
|
agentmailMessageId: message_id,
|
|
230
248
|
agentmailThreadId: thread_id,
|
|
249
|
+
requestedByUserId,
|
|
231
250
|
});
|
|
232
251
|
|
|
233
252
|
console.log(
|
|
@@ -255,6 +274,7 @@ export async function handleMessageReceived(
|
|
|
255
274
|
agentmailInboxId: inbox_id,
|
|
256
275
|
agentmailMessageId: message_id,
|
|
257
276
|
agentmailThreadId: thread_id,
|
|
277
|
+
requestedByUserId,
|
|
258
278
|
});
|
|
259
279
|
|
|
260
280
|
console.log(`[AgentMail] Created unassigned task ${task.id} (no lead or mapping available)`);
|
package/src/be/db.ts
CHANGED
|
@@ -44,6 +44,7 @@ import type {
|
|
|
44
44
|
SwarmConfig,
|
|
45
45
|
SwarmRepo,
|
|
46
46
|
TriggerConfig,
|
|
47
|
+
User,
|
|
47
48
|
VersionableField,
|
|
48
49
|
VersionMeta,
|
|
49
50
|
Workflow,
|
|
@@ -66,6 +67,17 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
|
|
|
66
67
|
return db;
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
// Fast path for tests: restore from pre-built template that already has
|
|
71
|
+
// migrations, seeds, and all post-init work baked in. Only the per-connection
|
|
72
|
+
// PRAGMA and the in-memory resolver function need to be set.
|
|
73
|
+
const templateBytes = (globalThis as any).__testMigrationTemplate as Uint8Array | undefined;
|
|
74
|
+
if (templateBytes) {
|
|
75
|
+
db = Database.deserialize(templateBytes);
|
|
76
|
+
db.run("PRAGMA foreign_keys = ON;");
|
|
77
|
+
configureDbResolver(resolvePromptTemplate);
|
|
78
|
+
return db;
|
|
79
|
+
}
|
|
80
|
+
|
|
69
81
|
db = new Database(dbPath, { create: true });
|
|
70
82
|
console.log(`Database initialized at ${dbPath}`);
|
|
71
83
|
|
|
@@ -730,6 +742,7 @@ type AgentTaskRow = {
|
|
|
730
742
|
was_paused: number;
|
|
731
743
|
credentialKeySuffix: string | null;
|
|
732
744
|
credentialKeyType: string | null;
|
|
745
|
+
requestedByUserId: string | null;
|
|
733
746
|
};
|
|
734
747
|
|
|
735
748
|
function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
@@ -785,6 +798,7 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
|
785
798
|
wasPaused: !!row.was_paused,
|
|
786
799
|
credentialKeySuffix: row.credentialKeySuffix ?? undefined,
|
|
787
800
|
credentialKeyType: row.credentialKeyType ?? undefined,
|
|
801
|
+
requestedByUserId: row.requestedByUserId ?? undefined,
|
|
788
802
|
};
|
|
789
803
|
}
|
|
790
804
|
|
|
@@ -1798,6 +1812,7 @@ export interface CreateTaskOptions {
|
|
|
1798
1812
|
workflowRunStepId?: string;
|
|
1799
1813
|
sourceTaskId?: string;
|
|
1800
1814
|
outputSchema?: Record<string, unknown>;
|
|
1815
|
+
requestedByUserId?: string;
|
|
1801
1816
|
}
|
|
1802
1817
|
|
|
1803
1818
|
/**
|
|
@@ -1865,6 +1880,9 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
1865
1880
|
if (parent.agentmailThreadId && !options.agentmailThreadId) {
|
|
1866
1881
|
options.agentmailThreadId = parent.agentmailThreadId;
|
|
1867
1882
|
}
|
|
1883
|
+
if (parent.requestedByUserId && !options.requestedByUserId) {
|
|
1884
|
+
options.requestedByUserId = parent.requestedByUserId;
|
|
1885
|
+
}
|
|
1868
1886
|
}
|
|
1869
1887
|
}
|
|
1870
1888
|
|
|
@@ -1889,8 +1907,8 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
1889
1907
|
vcsProvider, vcsRepo, vcsEventType, vcsNumber, vcsCommentId, vcsAuthor, vcsUrl,
|
|
1890
1908
|
agentmailInboxId, agentmailMessageId, agentmailThreadId,
|
|
1891
1909
|
mentionMessageId, mentionChannelId, dir, parentTaskId, model, scheduleId,
|
|
1892
|
-
workflowRunId, workflowRunStepId, outputSchema, createdAt, lastUpdatedAt
|
|
1893
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
1910
|
+
workflowRunId, workflowRunStepId, outputSchema, requestedByUserId, createdAt, lastUpdatedAt
|
|
1911
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
1894
1912
|
)
|
|
1895
1913
|
.get(
|
|
1896
1914
|
id,
|
|
@@ -1927,6 +1945,7 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
1927
1945
|
options?.workflowRunId ?? null,
|
|
1928
1946
|
options?.workflowRunStepId ?? null,
|
|
1929
1947
|
options?.outputSchema ? JSON.stringify(options.outputSchema) : null,
|
|
1948
|
+
options?.requestedByUserId ?? null,
|
|
1930
1949
|
now,
|
|
1931
1950
|
now,
|
|
1932
1951
|
);
|
|
@@ -7685,3 +7704,247 @@ export function getKeyCostSummary(keyType?: string): KeyCostSummary[] {
|
|
|
7685
7704
|
)
|
|
7686
7705
|
.all(...params);
|
|
7687
7706
|
}
|
|
7707
|
+
|
|
7708
|
+
// ============================================================================
|
|
7709
|
+
// User Identity Operations
|
|
7710
|
+
// ============================================================================
|
|
7711
|
+
|
|
7712
|
+
type UserRow = {
|
|
7713
|
+
id: string;
|
|
7714
|
+
name: string;
|
|
7715
|
+
email: string | null;
|
|
7716
|
+
role: string | null;
|
|
7717
|
+
notes: string | null;
|
|
7718
|
+
slackUserId: string | null;
|
|
7719
|
+
linearUserId: string | null;
|
|
7720
|
+
githubUsername: string | null;
|
|
7721
|
+
gitlabUsername: string | null;
|
|
7722
|
+
emailAliases: string | null;
|
|
7723
|
+
preferredChannel: string | null;
|
|
7724
|
+
timezone: string | null;
|
|
7725
|
+
createdAt: string;
|
|
7726
|
+
lastUpdatedAt: string;
|
|
7727
|
+
};
|
|
7728
|
+
|
|
7729
|
+
function rowToUser(row: UserRow): User {
|
|
7730
|
+
return {
|
|
7731
|
+
id: row.id,
|
|
7732
|
+
name: row.name,
|
|
7733
|
+
email: row.email ?? undefined,
|
|
7734
|
+
role: row.role ?? undefined,
|
|
7735
|
+
notes: row.notes ?? undefined,
|
|
7736
|
+
slackUserId: row.slackUserId ?? undefined,
|
|
7737
|
+
linearUserId: row.linearUserId ?? undefined,
|
|
7738
|
+
githubUsername: row.githubUsername ?? undefined,
|
|
7739
|
+
gitlabUsername: row.gitlabUsername ?? undefined,
|
|
7740
|
+
emailAliases: row.emailAliases ? JSON.parse(row.emailAliases) : [],
|
|
7741
|
+
preferredChannel: row.preferredChannel ?? "slack",
|
|
7742
|
+
timezone: row.timezone ?? undefined,
|
|
7743
|
+
createdAt: row.createdAt,
|
|
7744
|
+
lastUpdatedAt: row.lastUpdatedAt,
|
|
7745
|
+
};
|
|
7746
|
+
}
|
|
7747
|
+
|
|
7748
|
+
/**
|
|
7749
|
+
* Resolve a user by any platform-specific identifier.
|
|
7750
|
+
* Priority: exact match on platform ID, then email (including aliases), then name substring.
|
|
7751
|
+
*/
|
|
7752
|
+
export function resolveUser(opts: {
|
|
7753
|
+
slackUserId?: string;
|
|
7754
|
+
linearUserId?: string;
|
|
7755
|
+
githubUsername?: string;
|
|
7756
|
+
gitlabUsername?: string;
|
|
7757
|
+
email?: string;
|
|
7758
|
+
name?: string;
|
|
7759
|
+
}): User | null {
|
|
7760
|
+
const db = getDb();
|
|
7761
|
+
|
|
7762
|
+
// Try exact platform ID matches first
|
|
7763
|
+
if (opts.slackUserId) {
|
|
7764
|
+
const row = db
|
|
7765
|
+
.prepare<UserRow, string>("SELECT * FROM users WHERE slackUserId = ?")
|
|
7766
|
+
.get(opts.slackUserId);
|
|
7767
|
+
if (row) return rowToUser(row);
|
|
7768
|
+
}
|
|
7769
|
+
if (opts.linearUserId) {
|
|
7770
|
+
const row = db
|
|
7771
|
+
.prepare<UserRow, string>("SELECT * FROM users WHERE linearUserId = ?")
|
|
7772
|
+
.get(opts.linearUserId);
|
|
7773
|
+
if (row) return rowToUser(row);
|
|
7774
|
+
}
|
|
7775
|
+
if (opts.githubUsername) {
|
|
7776
|
+
const row = db
|
|
7777
|
+
.prepare<UserRow, string>("SELECT * FROM users WHERE githubUsername = ?")
|
|
7778
|
+
.get(opts.githubUsername);
|
|
7779
|
+
if (row) return rowToUser(row);
|
|
7780
|
+
}
|
|
7781
|
+
if (opts.gitlabUsername) {
|
|
7782
|
+
const row = db
|
|
7783
|
+
.prepare<UserRow, string>("SELECT * FROM users WHERE gitlabUsername = ?")
|
|
7784
|
+
.get(opts.gitlabUsername);
|
|
7785
|
+
if (row) return rowToUser(row);
|
|
7786
|
+
}
|
|
7787
|
+
|
|
7788
|
+
// Try email match (primary email)
|
|
7789
|
+
if (opts.email) {
|
|
7790
|
+
const row = db.prepare<UserRow, string>("SELECT * FROM users WHERE email = ?").get(opts.email);
|
|
7791
|
+
if (row) return rowToUser(row);
|
|
7792
|
+
|
|
7793
|
+
// Check emailAliases (JSON array search)
|
|
7794
|
+
const aliasRows = db
|
|
7795
|
+
.prepare<UserRow, []>("SELECT * FROM users WHERE emailAliases != '[]'")
|
|
7796
|
+
.all();
|
|
7797
|
+
for (const r of aliasRows) {
|
|
7798
|
+
const aliases: string[] = r.emailAliases ? JSON.parse(r.emailAliases) : [];
|
|
7799
|
+
if (aliases.some((a) => a.toLowerCase() === opts.email!.toLowerCase())) {
|
|
7800
|
+
return rowToUser(r);
|
|
7801
|
+
}
|
|
7802
|
+
}
|
|
7803
|
+
}
|
|
7804
|
+
|
|
7805
|
+
// Try name substring match (case-insensitive)
|
|
7806
|
+
if (opts.name) {
|
|
7807
|
+
const row = db
|
|
7808
|
+
.prepare<UserRow, string>("SELECT * FROM users WHERE LOWER(name) LIKE '%' || LOWER(?) || '%'")
|
|
7809
|
+
.get(opts.name);
|
|
7810
|
+
if (row) return rowToUser(row);
|
|
7811
|
+
}
|
|
7812
|
+
|
|
7813
|
+
return null;
|
|
7814
|
+
}
|
|
7815
|
+
|
|
7816
|
+
export function getUserById(id: string): User | null {
|
|
7817
|
+
const row = getDb().prepare<UserRow, string>("SELECT * FROM users WHERE id = ?").get(id);
|
|
7818
|
+
return row ? rowToUser(row) : null;
|
|
7819
|
+
}
|
|
7820
|
+
|
|
7821
|
+
export function getAllUsers(): User[] {
|
|
7822
|
+
return getDb().prepare<UserRow, []>("SELECT * FROM users ORDER BY name").all().map(rowToUser);
|
|
7823
|
+
}
|
|
7824
|
+
|
|
7825
|
+
export function createUser(data: {
|
|
7826
|
+
name: string;
|
|
7827
|
+
email?: string;
|
|
7828
|
+
role?: string;
|
|
7829
|
+
notes?: string;
|
|
7830
|
+
slackUserId?: string;
|
|
7831
|
+
linearUserId?: string;
|
|
7832
|
+
githubUsername?: string;
|
|
7833
|
+
gitlabUsername?: string;
|
|
7834
|
+
emailAliases?: string[];
|
|
7835
|
+
preferredChannel?: string;
|
|
7836
|
+
timezone?: string;
|
|
7837
|
+
}): User {
|
|
7838
|
+
const id = crypto.randomUUID().replace(/-/g, "");
|
|
7839
|
+
const now = new Date().toISOString();
|
|
7840
|
+
const row = getDb()
|
|
7841
|
+
.prepare<UserRow, (string | null)[]>(
|
|
7842
|
+
`INSERT INTO users (id, name, email, role, notes, slackUserId, linearUserId, githubUsername, gitlabUsername, emailAliases, preferredChannel, timezone, createdAt, lastUpdatedAt)
|
|
7843
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
7844
|
+
)
|
|
7845
|
+
.get(
|
|
7846
|
+
id,
|
|
7847
|
+
data.name,
|
|
7848
|
+
data.email ?? null,
|
|
7849
|
+
data.role ?? null,
|
|
7850
|
+
data.notes ?? null,
|
|
7851
|
+
data.slackUserId ?? null,
|
|
7852
|
+
data.linearUserId ?? null,
|
|
7853
|
+
data.githubUsername ?? null,
|
|
7854
|
+
data.gitlabUsername ?? null,
|
|
7855
|
+
JSON.stringify(data.emailAliases ?? []),
|
|
7856
|
+
data.preferredChannel ?? "slack",
|
|
7857
|
+
data.timezone ?? null,
|
|
7858
|
+
now,
|
|
7859
|
+
now,
|
|
7860
|
+
);
|
|
7861
|
+
if (!row) throw new Error("Failed to create user");
|
|
7862
|
+
return rowToUser(row);
|
|
7863
|
+
}
|
|
7864
|
+
|
|
7865
|
+
export function updateUser(
|
|
7866
|
+
id: string,
|
|
7867
|
+
data: Partial<{
|
|
7868
|
+
name: string;
|
|
7869
|
+
email: string;
|
|
7870
|
+
role: string;
|
|
7871
|
+
notes: string;
|
|
7872
|
+
slackUserId: string;
|
|
7873
|
+
linearUserId: string;
|
|
7874
|
+
githubUsername: string;
|
|
7875
|
+
gitlabUsername: string;
|
|
7876
|
+
emailAliases: string[];
|
|
7877
|
+
preferredChannel: string;
|
|
7878
|
+
timezone: string;
|
|
7879
|
+
}>,
|
|
7880
|
+
): User | null {
|
|
7881
|
+
const sets: string[] = [];
|
|
7882
|
+
const params: (string | null)[] = [];
|
|
7883
|
+
|
|
7884
|
+
if (data.name !== undefined) {
|
|
7885
|
+
sets.push("name = ?");
|
|
7886
|
+
params.push(data.name);
|
|
7887
|
+
}
|
|
7888
|
+
if (data.email !== undefined) {
|
|
7889
|
+
sets.push("email = ?");
|
|
7890
|
+
params.push(data.email);
|
|
7891
|
+
}
|
|
7892
|
+
if (data.role !== undefined) {
|
|
7893
|
+
sets.push("role = ?");
|
|
7894
|
+
params.push(data.role);
|
|
7895
|
+
}
|
|
7896
|
+
if (data.notes !== undefined) {
|
|
7897
|
+
sets.push("notes = ?");
|
|
7898
|
+
params.push(data.notes);
|
|
7899
|
+
}
|
|
7900
|
+
if (data.slackUserId !== undefined) {
|
|
7901
|
+
sets.push("slackUserId = ?");
|
|
7902
|
+
params.push(data.slackUserId);
|
|
7903
|
+
}
|
|
7904
|
+
if (data.linearUserId !== undefined) {
|
|
7905
|
+
sets.push("linearUserId = ?");
|
|
7906
|
+
params.push(data.linearUserId);
|
|
7907
|
+
}
|
|
7908
|
+
if (data.githubUsername !== undefined) {
|
|
7909
|
+
sets.push("githubUsername = ?");
|
|
7910
|
+
params.push(data.githubUsername);
|
|
7911
|
+
}
|
|
7912
|
+
if (data.gitlabUsername !== undefined) {
|
|
7913
|
+
sets.push("gitlabUsername = ?");
|
|
7914
|
+
params.push(data.gitlabUsername);
|
|
7915
|
+
}
|
|
7916
|
+
if (data.emailAliases !== undefined) {
|
|
7917
|
+
sets.push("emailAliases = ?");
|
|
7918
|
+
params.push(JSON.stringify(data.emailAliases));
|
|
7919
|
+
}
|
|
7920
|
+
if (data.preferredChannel !== undefined) {
|
|
7921
|
+
sets.push("preferredChannel = ?");
|
|
7922
|
+
params.push(data.preferredChannel);
|
|
7923
|
+
}
|
|
7924
|
+
if (data.timezone !== undefined) {
|
|
7925
|
+
sets.push("timezone = ?");
|
|
7926
|
+
params.push(data.timezone);
|
|
7927
|
+
}
|
|
7928
|
+
|
|
7929
|
+
if (sets.length === 0) return getUserById(id);
|
|
7930
|
+
|
|
7931
|
+
sets.push("lastUpdatedAt = ?");
|
|
7932
|
+
params.push(new Date().toISOString());
|
|
7933
|
+
params.push(id);
|
|
7934
|
+
|
|
7935
|
+
const row = getDb()
|
|
7936
|
+
.prepare<UserRow, (string | null)[]>(
|
|
7937
|
+
`UPDATE users SET ${sets.join(", ")} WHERE id = ? RETURNING *`,
|
|
7938
|
+
)
|
|
7939
|
+
.get(...params);
|
|
7940
|
+
return row ? rowToUser(row) : null;
|
|
7941
|
+
}
|
|
7942
|
+
|
|
7943
|
+
export function deleteUser(id: string): boolean {
|
|
7944
|
+
// Clear any task references before deleting
|
|
7945
|
+
getDb()
|
|
7946
|
+
.prepare("UPDATE agent_tasks SET requestedByUserId = NULL WHERE requestedByUserId = ?")
|
|
7947
|
+
.run(id);
|
|
7948
|
+
const result = getDb().prepare("DELETE FROM users WHERE id = ?").run(id);
|
|
7949
|
+
return result.changes > 0;
|
|
7950
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
-- User registry: canonical user profiles for cross-channel identity resolution
|
|
2
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
3
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
4
|
+
name TEXT NOT NULL,
|
|
5
|
+
email TEXT,
|
|
6
|
+
role TEXT,
|
|
7
|
+
notes TEXT,
|
|
8
|
+
slackUserId TEXT UNIQUE,
|
|
9
|
+
linearUserId TEXT UNIQUE,
|
|
10
|
+
githubUsername TEXT UNIQUE,
|
|
11
|
+
gitlabUsername TEXT UNIQUE,
|
|
12
|
+
emailAliases TEXT DEFAULT '[]',
|
|
13
|
+
preferredChannel TEXT DEFAULT 'slack',
|
|
14
|
+
timezone TEXT,
|
|
15
|
+
createdAt TEXT NOT NULL DEFAULT (datetime('now')),
|
|
16
|
+
lastUpdatedAt TEXT NOT NULL DEFAULT (datetime('now'))
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
-- Reverse lookup indexes (partial — only index non-null values)
|
|
20
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_slack ON users(slackUserId) WHERE slackUserId IS NOT NULL;
|
|
21
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_linear ON users(linearUserId) WHERE linearUserId IS NOT NULL;
|
|
22
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_github ON users(githubUsername) WHERE githubUsername IS NOT NULL;
|
|
23
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_gitlab ON users(gitlabUsername) WHERE gitlabUsername IS NOT NULL;
|
|
24
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email) WHERE email IS NOT NULL;
|
|
25
|
+
|
|
26
|
+
-- Link tasks to canonical users
|
|
27
|
+
ALTER TABLE agent_tasks ADD COLUMN requestedByUserId TEXT REFERENCES users(id);
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_requested_by ON agent_tasks(requestedByUserId) WHERE requestedByUserId IS NOT NULL;
|
|
29
|
+
|
|
30
|
+
-- Seed initial users
|
|
31
|
+
INSERT OR IGNORE INTO users (id, name, email, role, slackUserId, githubUsername)
|
|
32
|
+
VALUES (lower(hex(randomblob(16))), 'Taras', 't@desplega.ai', 'founder', 'U08NR6QD6CS', 'tarasyarema');
|
|
33
|
+
|
|
34
|
+
INSERT OR IGNORE INTO users (id, name, email, role, slackUserId)
|
|
35
|
+
VALUES (lower(hex(randomblob(16))), 'Eze', 'e@desplega.ai', 'founder', 'U08NY4B5R2M');
|
package/src/commands/runner.ts
CHANGED
|
@@ -1222,6 +1222,7 @@ interface Trigger {
|
|
|
1222
1222
|
text?: string;
|
|
1223
1223
|
}>;
|
|
1224
1224
|
cursorUpdates?: Array<{ channelId: string; ts: string }>; // Deferred cursor commits for channel_activity
|
|
1225
|
+
requestedBy?: { name: string; email?: string };
|
|
1225
1226
|
}
|
|
1226
1227
|
|
|
1227
1228
|
/** Options for polling */
|
|
@@ -1339,10 +1340,16 @@ async function buildPromptForTrigger(
|
|
|
1339
1340
|
'\n\nWhen done, use `store-progress` with status: "completed" and include your output.';
|
|
1340
1341
|
}
|
|
1341
1342
|
|
|
1343
|
+
// Include requesting user info if available from the poll trigger
|
|
1344
|
+
const requestedBy = trigger.requestedBy;
|
|
1345
|
+
const requestedBySection = requestedBy
|
|
1346
|
+
? `\n\nRequested by: ${requestedBy.name}${requestedBy.email ? ` (${requestedBy.email})` : ""}`
|
|
1347
|
+
: "";
|
|
1348
|
+
|
|
1342
1349
|
const result = await resolveTemplateAsync("task.trigger.assigned", {
|
|
1343
1350
|
work_on_task_cmd: fmt("work-on-task"),
|
|
1344
1351
|
task_id: trigger.taskId,
|
|
1345
|
-
task_desc_section: taskDescSection,
|
|
1352
|
+
task_desc_section: taskDescSection + requestedBySection,
|
|
1346
1353
|
output_instructions: outputInstructions,
|
|
1347
1354
|
});
|
|
1348
1355
|
return result.text;
|
package/src/github/handlers.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createTaskExtended, failTask, findTaskByVcs, getAllAgents } from "../be/db";
|
|
1
|
+
import { createTaskExtended, failTask, findTaskByVcs, getAllAgents, resolveUser } from "../be/db";
|
|
2
2
|
import { resolveTemplate } from "../prompts/resolver";
|
|
3
3
|
import {
|
|
4
4
|
detectMention,
|
|
@@ -134,6 +134,9 @@ export async function handlePullRequest(
|
|
|
134
134
|
requested_reviewer,
|
|
135
135
|
} = event;
|
|
136
136
|
|
|
137
|
+
// Resolve canonical user from GitHub sender
|
|
138
|
+
const requestedByUserId = resolveUser({ githubUsername: sender.login })?.id;
|
|
139
|
+
|
|
137
140
|
// Handle assigned action - bot was assigned to PR
|
|
138
141
|
if (action === "assigned") {
|
|
139
142
|
// Check if bot was assigned
|
|
@@ -178,6 +181,7 @@ export async function handlePullRequest(
|
|
|
178
181
|
vcsEventType: "pull_request",
|
|
179
182
|
vcsNumber: pr.number,
|
|
180
183
|
vcsAuthor: sender.login,
|
|
184
|
+
requestedByUserId,
|
|
181
185
|
vcsUrl: pr.html_url,
|
|
182
186
|
});
|
|
183
187
|
|
|
@@ -275,6 +279,7 @@ export async function handlePullRequest(
|
|
|
275
279
|
vcsEventType: "pull_request",
|
|
276
280
|
vcsNumber: pr.number,
|
|
277
281
|
vcsAuthor: sender.login,
|
|
282
|
+
requestedByUserId,
|
|
278
283
|
vcsUrl: pr.html_url,
|
|
279
284
|
});
|
|
280
285
|
|
|
@@ -364,6 +369,7 @@ export async function handlePullRequest(
|
|
|
364
369
|
vcsEventType: "pull_request",
|
|
365
370
|
vcsNumber: pr.number,
|
|
366
371
|
vcsAuthor: sender.login,
|
|
372
|
+
requestedByUserId,
|
|
367
373
|
vcsUrl: pr.html_url,
|
|
368
374
|
});
|
|
369
375
|
|
|
@@ -478,6 +484,9 @@ export async function handleIssue(
|
|
|
478
484
|
): Promise<{ created: boolean; taskId?: string }> {
|
|
479
485
|
const { action, issue, repository, sender, installation, assignee } = event;
|
|
480
486
|
|
|
487
|
+
// Resolve canonical user from GitHub sender
|
|
488
|
+
const requestedByUserId = resolveUser({ githubUsername: sender.login })?.id;
|
|
489
|
+
|
|
481
490
|
// Handle assigned action - bot was assigned to issue
|
|
482
491
|
if (action === "assigned") {
|
|
483
492
|
// Check if bot was assigned
|
|
@@ -520,6 +529,7 @@ export async function handleIssue(
|
|
|
520
529
|
vcsEventType: "issues",
|
|
521
530
|
vcsNumber: issue.number,
|
|
522
531
|
vcsAuthor: sender.login,
|
|
532
|
+
requestedByUserId,
|
|
523
533
|
vcsUrl: issue.html_url,
|
|
524
534
|
});
|
|
525
535
|
|
|
@@ -605,6 +615,7 @@ export async function handleIssue(
|
|
|
605
615
|
vcsEventType: "issues",
|
|
606
616
|
vcsNumber: issue.number,
|
|
607
617
|
vcsAuthor: sender.login,
|
|
618
|
+
requestedByUserId,
|
|
608
619
|
vcsUrl: issue.html_url,
|
|
609
620
|
});
|
|
610
621
|
|
|
@@ -702,6 +713,9 @@ export async function handleComment(
|
|
|
702
713
|
): Promise<{ created: boolean; taskId?: string }> {
|
|
703
714
|
const { action, comment, repository, sender, issue, pull_request, installation } = event;
|
|
704
715
|
|
|
716
|
+
// Resolve canonical user from GitHub sender
|
|
717
|
+
const requestedByUserId = resolveUser({ githubUsername: sender.login })?.id;
|
|
718
|
+
|
|
705
719
|
// Only handle created action
|
|
706
720
|
if (action !== "created") {
|
|
707
721
|
return { created: false };
|
|
@@ -802,6 +816,9 @@ export async function handlePullRequestReview(
|
|
|
802
816
|
): Promise<{ created: boolean; taskId?: string }> {
|
|
803
817
|
const { action, review, pull_request: pr, repository, sender, installation } = event;
|
|
804
818
|
|
|
819
|
+
// Resolve canonical user from GitHub sender
|
|
820
|
+
const requestedByUserId = resolveUser({ githubUsername: sender.login })?.id;
|
|
821
|
+
|
|
805
822
|
// Only handle submitted reviews (the most important action)
|
|
806
823
|
// Edited reviews are less common and dismissed is handled by the state
|
|
807
824
|
if (action !== "submitted") {
|
package/src/gitlab/handlers.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Detects bot mentions
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { createTaskExtended, failTask, findTaskByVcs, getAllAgents } from "../be/db";
|
|
11
|
+
import { createTaskExtended, failTask, findTaskByVcs, getAllAgents, resolveUser } from "../be/db";
|
|
12
12
|
import { resolveTemplate } from "../prompts/resolver";
|
|
13
13
|
import { GITLAB_BOT_NAME } from "./auth";
|
|
14
14
|
import { addGitLabNoteReaction, addGitLabReaction } from "./reactions";
|
|
@@ -60,6 +60,9 @@ export async function handleMergeRequest(
|
|
|
60
60
|
const action = mr.action;
|
|
61
61
|
const repo = project.path_with_namespace;
|
|
62
62
|
|
|
63
|
+
// Resolve canonical user from GitLab sender
|
|
64
|
+
const requestedByUserId = resolveUser({ gitlabUsername: user.username })?.id;
|
|
65
|
+
|
|
63
66
|
console.log(`[GitLab] MR #${mr.iid} ${action} by ${user.username} in ${repo}`);
|
|
64
67
|
|
|
65
68
|
const dedupKey = `gitlab-mr-${repo}-${mr.iid}-${action}-${user.username}`;
|
|
@@ -105,6 +108,7 @@ export async function handleMergeRequest(
|
|
|
105
108
|
vcsEventType: "merge_request",
|
|
106
109
|
vcsNumber: mr.iid,
|
|
107
110
|
vcsAuthor: user.username,
|
|
111
|
+
requestedByUserId,
|
|
108
112
|
vcsUrl: mr.url,
|
|
109
113
|
});
|
|
110
114
|
|
|
@@ -151,6 +155,9 @@ export async function handleIssue(
|
|
|
151
155
|
const action = issue.action;
|
|
152
156
|
const repo = project.path_with_namespace;
|
|
153
157
|
|
|
158
|
+
// Resolve canonical user from GitLab sender
|
|
159
|
+
const requestedByUserId = resolveUser({ gitlabUsername: user.username })?.id;
|
|
160
|
+
|
|
154
161
|
console.log(`[GitLab] Issue #${issue.iid} ${action} by ${user.username} in ${repo}`);
|
|
155
162
|
|
|
156
163
|
const dedupKey = `gitlab-issue-${repo}-${issue.iid}-${action}-${user.username}`;
|
|
@@ -198,6 +205,7 @@ export async function handleIssue(
|
|
|
198
205
|
vcsEventType: "issue",
|
|
199
206
|
vcsNumber: issue.iid,
|
|
200
207
|
vcsAuthor: user.username,
|
|
208
|
+
requestedByUserId,
|
|
201
209
|
vcsUrl: issue.url,
|
|
202
210
|
});
|
|
203
211
|
|
|
@@ -226,6 +234,9 @@ export async function handleNote(event: NoteEvent): Promise<{ created: boolean;
|
|
|
226
234
|
const { user, project, object_attributes: note } = event;
|
|
227
235
|
const repo = project.path_with_namespace;
|
|
228
236
|
|
|
237
|
+
// Resolve canonical user from GitLab sender
|
|
238
|
+
const requestedByUserId = resolveUser({ gitlabUsername: user.username })?.id;
|
|
239
|
+
|
|
229
240
|
// Only handle comments with bot mentions
|
|
230
241
|
if (!detectMention(note.note)) {
|
|
231
242
|
return { created: false };
|
package/src/http/poll.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getOfferedTasksForAgent,
|
|
13
13
|
getPendingTaskForAgent,
|
|
14
14
|
getUnassignedTaskIds,
|
|
15
|
+
getUserById,
|
|
15
16
|
hasCapacity,
|
|
16
17
|
startTask,
|
|
17
18
|
upsertChannelActivityCursor,
|
|
@@ -144,11 +145,19 @@ export async function handlePoll(
|
|
|
144
145
|
conditions: [{ timeout_ms: 300_000 }], // 5 min: polling interval + queue wait
|
|
145
146
|
});
|
|
146
147
|
|
|
148
|
+
// Resolve requesting user if available
|
|
149
|
+
const requestedByUser = pendingTask.requestedByUserId
|
|
150
|
+
? getUserById(pendingTask.requestedByUserId)
|
|
151
|
+
: undefined;
|
|
152
|
+
|
|
147
153
|
return {
|
|
148
154
|
trigger: {
|
|
149
155
|
type: "task_assigned",
|
|
150
156
|
taskId: pendingTask.id,
|
|
151
157
|
task: { ...pendingTask, status: "in_progress" },
|
|
158
|
+
...(requestedByUser && {
|
|
159
|
+
requestedBy: { name: requestedByUser.name, email: requestedByUser.email },
|
|
160
|
+
}),
|
|
152
161
|
},
|
|
153
162
|
};
|
|
154
163
|
}
|