@desplega.ai/agent-swarm 1.80.3 → 1.81.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.
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "How to manage the user registry — creating users for new Slack/GitHub/GitLab identities, managing aliases, resolving users across platforms. Use when a new human interacts with the swarm or when user identity needs updating."
2
+ description: "How to manage the user registry — creating users for new Slack/GitHub/GitLab/Linear identities, managing aliases, resolving users across platforms. Use when a new human interacts with the swarm or when user identity needs updating."
3
3
  argument-hint: [action]
4
4
  ---
5
5
 
@@ -7,16 +7,18 @@ argument-hint: [action]
7
7
 
8
8
  Manage the swarm's user registry — creating, updating, resolving, and listing users. Users link human identities across platforms (Slack, GitHub, GitLab, Linear, email) so the swarm can track who requested work.
9
9
 
10
+ > **Migration note (2026-05)**: the old top-level identity fields (`slackUserId`, `linearUserId`, `githubUsername`, `gitlabUsername`) and the fuzzy `name` lookup were removed in lockstep with the user-identity refactor. Use the new `{kind, externalId}` shape instead. Old payloads now fail Zod validation at runtime — there is no compatibility shim.
11
+
10
12
  ## When to Create Users
11
13
 
12
14
  Create a new user when:
13
- - An **unknown Slack user** sends a message to the swarm (no `resolveUser` match for their `slackUserId`)
15
+ - An **unknown Slack user** sends a message to the swarm (`resolve-user` with `{kind: "slack", externalId: "<U_X>"}` returns no match)
14
16
  - An **unknown GitHub user** opens an issue or PR that triggers a task
15
17
  - An **unknown GitLab user** creates an issue or MR
16
18
  - An **unknown Linear user** is assigned to or creates a synced issue
17
19
  - A human explicitly asks to be registered
18
20
 
19
- **Do NOT** create duplicate users. Always call `resolve-user` first to check if the person already exists under a different platform identity.
21
+ **Do NOT** create duplicate users. Always call `resolve-user` first — by `{kind, externalId}` AND, when you have it, by `email` — to check if the person already exists under a different platform identity.
20
22
 
21
23
  ## Tools
22
24
 
@@ -24,41 +26,62 @@ Two MCP tools handle user management:
24
26
 
25
27
  ### `resolve-user` — Find an existing user
26
28
 
27
- Looks up a user by any platform identity. Use this BEFORE creating a new user.
29
+ Looks up a user by an `(kind, externalId)` pair OR by email (primary or alias). Use this BEFORE creating a new user. Caller MUST supply either `(kind + externalId)` OR `email` — empty input is rejected.
28
30
 
29
31
  ```
32
+ # Lookup by platform identity
33
+ resolve-user with:
34
+ kind: "slack"
35
+ externalId: "U12345"
36
+
37
+ # OR
38
+ resolve-user with:
39
+ kind: "github"
40
+ externalId: "octocat"
41
+
42
+ # OR
43
+ resolve-user with:
44
+ kind: "gitlab"
45
+ externalId: "octocat"
46
+
47
+ # OR
48
+ resolve-user with:
49
+ kind: "linear"
50
+ externalId: "uuid-from-linear"
51
+
52
+ # OR lookup by email (primary or alias)
30
53
  resolve-user with:
31
- slackUserId: "U12345" # Slack member ID
32
- # OR
33
- githubUsername: "octocat" # GitHub username
34
- # OR
35
- gitlabUsername: "octocat" # GitLab username
36
- # OR
37
- linearUserId: "uuid" # Linear user UUID
38
- # OR
39
- email: "user@example.com" # Primary email or alias
40
- # OR
41
- name: "Jane Doe" # Fuzzy name search (least specific)
54
+ email: "user@example.com"
42
55
  ```
43
56
 
44
- Priority order: platform IDs > email > name. Platform IDs are exact matches; email checks aliases (case-insensitive); name is substring match.
57
+ Email lookup is case-insensitive and checks the primary `email` column AND every entry in `emailAliases`.
45
58
 
46
59
  ### `manage-user` — CRUD operations
47
60
 
61
+ Identities are managed via a declarative `identities: [{kind, externalId}, ...]` array:
62
+
63
+ - On **create**: every entry in `identities` is linked (each emits `identity_added`).
64
+ - On **update**: `identities` is treated as the full desired set. Helper computes a diff against the user's current identities — adds emit `identity_added`, removes emit `identity_removed`. Omit the field entirely to leave identities untouched.
65
+
48
66
  ```
49
67
  # Create a new user
50
68
  manage-user with:
51
69
  action: "create"
52
- name: "Jane Doe" # Required
53
- email: "jane@company.com" # Optional
54
- role: "engineering lead" # Optional, free-form
55
- slackUserId: "U12345" # Optional
56
- githubUsername: "janedoe" # Optional
57
- gitlabUsername: "janedoe" # Optional
58
- linearUserId: "uuid-from-linear" # Optional
59
- emailAliases: ["jane.doe@company.com"] # Optional
60
- timezone: "America/New_York" # Optional
61
- notes: "Prefers async communication" # Optional
70
+ name: "Jane Doe" # Required
71
+ email: "jane@company.com" # Optional
72
+ role: "engineering lead" # Optional, free-form
73
+ identities: # Optional
74
+ - kind: "slack"
75
+ externalId: "U12345"
76
+ - kind: "github"
77
+ externalId: "janedoe"
78
+ - kind: "linear"
79
+ externalId: "uuid-from-linear"
80
+ emailAliases: ["jane.doe@company.com"] # Optional
81
+ timezone: "America/New_York" # Optional
82
+ notes: "Prefers async communication" # Optional
83
+ dailyBudgetUsd: 25.0 # Optional — null/omitted = unlimited
84
+ status: "active" # Optional — "invited" | "active" | "suspended"
62
85
 
63
86
  # List all users
64
87
  manage-user with:
@@ -69,11 +92,16 @@ manage-user with:
69
92
  action: "get"
70
93
  userId: "<uuid>"
71
94
 
72
- # Update a user (only send fields to change)
95
+ # Update a user (declarative pass the FULL desired set for `identities`)
73
96
  manage-user with:
74
97
  action: "update"
75
98
  userId: "<uuid>"
76
- githubUsername: "new-username"
99
+ identities: # FULL desired set; diff is applied
100
+ - kind: "slack"
101
+ externalId: "U12345"
102
+ - kind: "github"
103
+ externalId: "janedoe-new" # renamed → identity_added + identity_removed
104
+ emailAliases: ["jane.doe@company.com", "jd@example.com"] # emits email_added/email_removed per delta
77
105
 
78
106
  # Delete a user
79
107
  manage-user with:
@@ -83,37 +111,48 @@ manage-user with:
83
111
 
84
112
  ## Workflow: New Slack User
85
113
 
86
- 1. Receive a message from an unknown Slack user (e.g., `slackUserId: "U_NEW123"`)
87
- 2. Call `resolve-user` with `slackUserId: "U_NEW123"` — returns null
88
- 3. Get the user's Slack profile (name, email) via `slack-read` or from the message metadata
89
- 4. Call `resolve-user` with `email: "<their-email>"` — check if they exist under a different platform
90
- 5. If found: call `manage-user` with `action: "update"` to add their `slackUserId`
91
- 6. If not found: call `manage-user` with `action: "create"` including name, email, and slackUserId
114
+ 1. Receive a message from an unknown Slack user (e.g., external ID `U_NEW123`).
115
+ 2. Call `resolve-user` with `{kind: "slack", externalId: "U_NEW123"}` — returns null.
116
+ 3. Get the user's Slack profile (name, email) via `slack-read` or from the message metadata.
117
+ 4. Call `resolve-user` with `{email: "<their-email>"}` — check if they exist under a different platform.
118
+ 5. If found: call `manage-user` with `action: "update"`, passing the user's FULL identity set including the new Slack entry.
119
+ 6. If not found: call `manage-user` with `action: "create"`, including `name`, `email`, and `identities: [{kind: "slack", externalId: "U_NEW123"}]`.
92
120
 
93
121
  ## Workflow: New GitHub User
94
122
 
95
- 1. Receive a webhook from an unknown GitHub user (e.g., `githubUsername: "octocat"`)
96
- 2. Call `resolve-user` with `githubUsername: "octocat"` — returns null
97
- 3. Call `manage-user` with `action: "create"` including at minimum `name` and `githubUsername`
98
- 4. If you know their email (from the webhook payload), include it
123
+ 1. Receive a webhook from an unknown GitHub user (e.g., login `octocat`).
124
+ 2. Call `resolve-user` with `{kind: "github", externalId: "octocat"}` — returns null.
125
+ 3. Call `manage-user` with `action: "create"`, including at minimum `name` and `identities: [{kind: "github", externalId: "octocat"}]`.
126
+ 4. If you know their email (from the webhook payload), include it.
99
127
 
100
128
  ## Workflow: Linking Identities
101
129
 
102
130
  When you discover a known user is also active on another platform:
103
131
 
104
- 1. Call `resolve-user` to find them by their known identity
105
- 2. Call `manage-user` with `action: "update"` to add the new platform identity
132
+ 1. Call `resolve-user` to find them by their known identity.
133
+ 2. Call `manage-user` with `action: "update"`, passing the FULL desired `identities` set (existing + the new one).
134
+
135
+ Example: You know "Jane" by Slack ID, and discover her GitHub login:
106
136
 
107
- Example: You know "Jane" by Slack ID, and discover her GitHub username:
108
137
  ```
109
- resolve-user slackUserId: "U_JANE" returns user with id "abc-123"
110
- manage-user action: "update" userId: "abc-123" githubUsername: "janedoe"
138
+ resolve-user kind: "slack" externalId: "U_JANE"
139
+ → returns user with id "abc-123" (identities currently: [{kind: "slack", externalId: "U_JANE"}])
140
+
141
+ manage-user action: "update" userId: "abc-123"
142
+ identities:
143
+ - kind: "slack"
144
+ externalId: "U_JANE"
145
+ - kind: "github"
146
+ externalId: "janedoe"
147
+ → adds GitHub identity (emit identity_added). Slack identity unchanged.
111
148
  ```
112
149
 
113
150
  ## Important Notes
114
151
 
115
152
  - `manage-user` is **lead-only** — workers cannot use it for any action (the lead check happens before action dispatch). Workers must use `resolve-user` for lookups.
116
- - `slackUserId`, `githubUsername`, `gitlabUsername`, and `linearUserId` have **unique constraints**duplicates will error.
153
+ - The `(kind, externalId)` PK on `user_external_ids` means the same identifier cannot be linked to two different users a re-link to a different user surfaces as a PK collision (the operator can investigate via the People page merge flow).
117
154
  - Deleting a user clears `requestedByUserId` on all their associated tasks (sets to null).
118
- - Email aliases are case-insensitive for resolution.
119
- - The `preferredChannel` field defaults to `"slack"` and can be `"slack"`, `"email"`, `"github"`, or `"gitlab"`.
155
+ - Email aliases are case-insensitive for resolution. Editing them via `manage-user update` emits `email_added` / `email_removed` events per delta.
156
+ - The `preferredChannel` field defaults to `"slack"` and can be `"slack"`, `"email"`, `"github"`, `"gitlab"`, or any custom string.
157
+ - `dailyBudgetUsd` is `null` = unlimited.
158
+ - `status` lifecycle: `invited` → `active` → `suspended`. The CHECK constraint rejects other values.
@@ -1,22 +1,24 @@
1
1
  ---
2
2
  name: user-management
3
- description: "How to manage the user registry — creating users for new Slack/GitHub/GitLab identities, managing aliases, resolving users across platforms. Use when a new human interacts with the swarm or when user identity needs updating."
3
+ description: "How to manage the user registry — creating users for new Slack/GitHub/GitLab/Linear identities, managing aliases, resolving users across platforms. Use when a new human interacts with the swarm or when user identity needs updating."
4
4
  ---
5
5
 
6
6
  # User Management
7
7
 
8
8
  Manage the swarm's user registry — creating, updating, resolving, and listing users. Users link human identities across platforms (Slack, GitHub, GitLab, Linear, email) so the swarm can track who requested work.
9
9
 
10
+ > **Migration note (2026-05)**: the old top-level identity fields (`slackUserId`, `linearUserId`, `githubUsername`, `gitlabUsername`) and the fuzzy `name` lookup were removed in lockstep with the user-identity refactor. Use the new `{kind, externalId}` shape instead. Old payloads now fail Zod validation at runtime — there is no compatibility shim.
11
+
10
12
  ## When to Create Users
11
13
 
12
14
  Create a new user when:
13
- - An **unknown Slack user** sends a message to the swarm (no `resolveUser` match for their `slackUserId`)
15
+ - An **unknown Slack user** sends a message to the swarm (`resolve-user` with `{kind: "slack", externalId: "<U_X>"}` returns no match)
14
16
  - An **unknown GitHub user** opens an issue or PR that triggers a task
15
17
  - An **unknown GitLab user** creates an issue or MR
16
18
  - An **unknown Linear user** is assigned to or creates a synced issue
17
19
  - A human explicitly asks to be registered
18
20
 
19
- **Do NOT** create duplicate users. Always call `resolve-user` first to check if the person already exists under a different platform identity.
21
+ **Do NOT** create duplicate users. Always call `resolve-user` first — by `{kind, externalId}` AND, when you have it, by `email` — to check if the person already exists under a different platform identity.
20
22
 
21
23
  ## Tools
22
24
 
@@ -24,41 +26,62 @@ Two MCP tools handle user management:
24
26
 
25
27
  ### `resolve-user` — Find an existing user
26
28
 
27
- Looks up a user by any platform identity. Use this BEFORE creating a new user.
29
+ Looks up a user by an `(kind, externalId)` pair OR by email (primary or alias). Use this BEFORE creating a new user. Caller MUST supply either `(kind + externalId)` OR `email` — empty input is rejected.
28
30
 
29
31
  ```
32
+ # Lookup by platform identity
33
+ resolve-user with:
34
+ kind: "slack"
35
+ externalId: "U12345"
36
+
37
+ # OR
38
+ resolve-user with:
39
+ kind: "github"
40
+ externalId: "octocat"
41
+
42
+ # OR
43
+ resolve-user with:
44
+ kind: "gitlab"
45
+ externalId: "octocat"
46
+
47
+ # OR
48
+ resolve-user with:
49
+ kind: "linear"
50
+ externalId: "uuid-from-linear"
51
+
52
+ # OR lookup by email (primary or alias)
30
53
  resolve-user with:
31
- slackUserId: "U12345" # Slack member ID
32
- # OR
33
- githubUsername: "octocat" # GitHub username
34
- # OR
35
- gitlabUsername: "octocat" # GitLab username
36
- # OR
37
- linearUserId: "uuid" # Linear user UUID
38
- # OR
39
- email: "user@example.com" # Primary email or alias
40
- # OR
41
- name: "Jane Doe" # Fuzzy name search (least specific)
54
+ email: "user@example.com"
42
55
  ```
43
56
 
44
- Priority order: platform IDs > email > name. Platform IDs are exact matches; email checks aliases (case-insensitive); name is substring match.
57
+ Email lookup is case-insensitive and checks the primary `email` column AND every entry in `emailAliases`.
45
58
 
46
59
  ### `manage-user` — CRUD operations
47
60
 
61
+ Identities are managed via a declarative `identities: [{kind, externalId}, ...]` array:
62
+
63
+ - On **create**: every entry in `identities` is linked (each emits `identity_added`).
64
+ - On **update**: `identities` is treated as the full desired set. Helper computes a diff against the user's current identities — adds emit `identity_added`, removes emit `identity_removed`. Omit the field entirely to leave identities untouched.
65
+
48
66
  ```
49
67
  # Create a new user
50
68
  manage-user with:
51
69
  action: "create"
52
- name: "Jane Doe" # Required
53
- email: "jane@company.com" # Optional
54
- role: "engineering lead" # Optional, free-form
55
- slackUserId: "U12345" # Optional
56
- githubUsername: "janedoe" # Optional
57
- gitlabUsername: "janedoe" # Optional
58
- linearUserId: "uuid-from-linear" # Optional
59
- emailAliases: ["jane.doe@company.com"] # Optional
60
- timezone: "America/New_York" # Optional
61
- notes: "Prefers async communication" # Optional
70
+ name: "Jane Doe" # Required
71
+ email: "jane@company.com" # Optional
72
+ role: "engineering lead" # Optional, free-form
73
+ identities: # Optional
74
+ - kind: "slack"
75
+ externalId: "U12345"
76
+ - kind: "github"
77
+ externalId: "janedoe"
78
+ - kind: "linear"
79
+ externalId: "uuid-from-linear"
80
+ emailAliases: ["jane.doe@company.com"] # Optional
81
+ timezone: "America/New_York" # Optional
82
+ notes: "Prefers async communication" # Optional
83
+ dailyBudgetUsd: 25.0 # Optional — null/omitted = unlimited
84
+ status: "active" # Optional — "invited" | "active" | "suspended"
62
85
 
63
86
  # List all users
64
87
  manage-user with:
@@ -69,11 +92,16 @@ manage-user with:
69
92
  action: "get"
70
93
  userId: "<uuid>"
71
94
 
72
- # Update a user (only send fields to change)
95
+ # Update a user (declarative pass the FULL desired set for `identities`)
73
96
  manage-user with:
74
97
  action: "update"
75
98
  userId: "<uuid>"
76
- githubUsername: "new-username"
99
+ identities: # FULL desired set; diff is applied
100
+ - kind: "slack"
101
+ externalId: "U12345"
102
+ - kind: "github"
103
+ externalId: "janedoe-new" # renamed → identity_added + identity_removed
104
+ emailAliases: ["jane.doe@company.com", "jd@example.com"] # emits email_added/email_removed per delta
77
105
 
78
106
  # Delete a user
79
107
  manage-user with:
@@ -83,37 +111,48 @@ manage-user with:
83
111
 
84
112
  ## Workflow: New Slack User
85
113
 
86
- 1. Receive a message from an unknown Slack user (e.g., `slackUserId: "U_NEW123"`)
87
- 2. Call `resolve-user` with `slackUserId: "U_NEW123"` — returns null
88
- 3. Get the user's Slack profile (name, email) via `slack-read` or from the message metadata
89
- 4. Call `resolve-user` with `email: "<their-email>"` — check if they exist under a different platform
90
- 5. If found: call `manage-user` with `action: "update"` to add their `slackUserId`
91
- 6. If not found: call `manage-user` with `action: "create"` including name, email, and slackUserId
114
+ 1. Receive a message from an unknown Slack user (e.g., external ID `U_NEW123`).
115
+ 2. Call `resolve-user` with `{kind: "slack", externalId: "U_NEW123"}` — returns null.
116
+ 3. Get the user's Slack profile (name, email) via `slack-read` or from the message metadata.
117
+ 4. Call `resolve-user` with `{email: "<their-email>"}` — check if they exist under a different platform.
118
+ 5. If found: call `manage-user` with `action: "update"`, passing the user's FULL identity set including the new Slack entry.
119
+ 6. If not found: call `manage-user` with `action: "create"`, including `name`, `email`, and `identities: [{kind: "slack", externalId: "U_NEW123"}]`.
92
120
 
93
121
  ## Workflow: New GitHub User
94
122
 
95
- 1. Receive a webhook from an unknown GitHub user (e.g., `githubUsername: "octocat"`)
96
- 2. Call `resolve-user` with `githubUsername: "octocat"` — returns null
97
- 3. Call `manage-user` with `action: "create"` including at minimum `name` and `githubUsername`
98
- 4. If you know their email (from the webhook payload), include it
123
+ 1. Receive a webhook from an unknown GitHub user (e.g., login `octocat`).
124
+ 2. Call `resolve-user` with `{kind: "github", externalId: "octocat"}` — returns null.
125
+ 3. Call `manage-user` with `action: "create"`, including at minimum `name` and `identities: [{kind: "github", externalId: "octocat"}]`.
126
+ 4. If you know their email (from the webhook payload), include it.
99
127
 
100
128
  ## Workflow: Linking Identities
101
129
 
102
130
  When you discover a known user is also active on another platform:
103
131
 
104
- 1. Call `resolve-user` to find them by their known identity
105
- 2. Call `manage-user` with `action: "update"` to add the new platform identity
132
+ 1. Call `resolve-user` to find them by their known identity.
133
+ 2. Call `manage-user` with `action: "update"`, passing the FULL desired `identities` set (existing + the new one).
134
+
135
+ Example: You know "Jane" by Slack ID, and discover her GitHub login:
106
136
 
107
- Example: You know "Jane" by Slack ID, and discover her GitHub username:
108
137
  ```
109
- resolve-user slackUserId: "U_JANE" returns user with id "abc-123"
110
- manage-user action: "update" userId: "abc-123" githubUsername: "janedoe"
138
+ resolve-user kind: "slack" externalId: "U_JANE"
139
+ → returns user with id "abc-123" (identities currently: [{kind: "slack", externalId: "U_JANE"}])
140
+
141
+ manage-user action: "update" userId: "abc-123"
142
+ identities:
143
+ - kind: "slack"
144
+ externalId: "U_JANE"
145
+ - kind: "github"
146
+ externalId: "janedoe"
147
+ → adds GitHub identity (emit identity_added). Slack identity unchanged.
111
148
  ```
112
149
 
113
150
  ## Important Notes
114
151
 
115
152
  - `manage-user` is **lead-only** — workers cannot use it for any action (the lead check happens before action dispatch). Workers must use `resolve-user` for lookups.
116
- - `slackUserId`, `githubUsername`, `gitlabUsername`, and `linearUserId` have **unique constraints**duplicates will error.
153
+ - The `(kind, externalId)` PK on `user_external_ids` means the same identifier cannot be linked to two different users a re-link to a different user surfaces as a PK collision (the operator can investigate via the People page merge flow).
117
154
  - Deleting a user clears `requestedByUserId` on all their associated tasks (sets to null).
118
- - Email aliases are case-insensitive for resolution.
119
- - The `preferredChannel` field defaults to `"slack"` and can be `"slack"`, `"email"`, `"github"`, or `"gitlab"`.
155
+ - Email aliases are case-insensitive for resolution. Editing them via `manage-user update` emits `email_added` / `email_removed` events per delta.
156
+ - The `preferredChannel` field defaults to `"slack"` and can be `"slack"`, `"email"`, `"github"`, `"gitlab"`, or any custom string.
157
+ - `dailyBudgetUsd` is `null` = unlimited.
158
+ - `status` lifecycle: `invited` → `active` → `suspended`. The CHECK constraint rejects other values.
@@ -3,8 +3,8 @@ import {
3
3
  getAgentById,
4
4
  getAgentMailInboxMapping,
5
5
  getAllAgents,
6
- resolveUser,
7
6
  } from "../be/db";
7
+ import { findOrCreateUserByEmail } from "../be/users";
8
8
  import { resolveTemplate } from "../prompts/resolver";
9
9
  import { createIngressBuffer } from "../tasks/additive-ingress";
10
10
  import { agentmailContextKey } from "../tasks/context-key";
@@ -73,6 +73,16 @@ function extractEmailFromField(from: string): string | undefined {
73
73
  return bareMatch?.[0]?.toLowerCase();
74
74
  }
75
75
 
76
+ /**
77
+ * Extract display name from a from_ field like "Taras Yarema <t@desplega.ai>".
78
+ * Returns undefined if no display-name portion is present (bare address).
79
+ */
80
+ function extractNameFromField(from: string): string | undefined {
81
+ const angleMatch = from.match(/^\s*"?([^"<]+?)"?\s*<[^>]+@[^>]+>\s*$/);
82
+ const name = angleMatch?.[1]?.trim();
83
+ return name && name.length > 0 ? name : undefined;
84
+ }
85
+
76
86
  /**
77
87
  * Check if an inbox domain is allowed by the filter.
78
88
  * Returns true if no filter is set or the inbox domain matches.
@@ -159,9 +169,21 @@ export async function handleMessageReceived(
159
169
  const subject = message.subject || "(no subject)";
160
170
  const body = message.text || message.html || "";
161
171
 
162
- // Resolve canonical user from sender email
172
+ // Resolve canonical user from sender email — per Q17.F, email is the
173
+ // primary identifier for inbound email events, so always auto-create when
174
+ // missing. findOrCreateUserByEmail emits identity_added (new row) or
175
+ // auto_merge (existing row matched via primary email or emailAliases).
163
176
  const senderEmail = extractEmailFromField(from);
164
- const requestedByUserId = senderEmail ? resolveUser({ email: senderEmail })?.id : undefined;
177
+ const senderName = extractNameFromField(from);
178
+ let requestedByUserId: string | undefined;
179
+ if (senderEmail) {
180
+ const { user } = findOrCreateUserByEmail(
181
+ senderEmail,
182
+ { name: senderName ?? undefined },
183
+ { kind: "system", id: "webhook:agentmail" },
184
+ );
185
+ requestedByUserId = user.id;
186
+ }
165
187
  const preview = body.length > 500 ? `${body.substring(0, 500)}...` : body;
166
188
 
167
189
  // Emit workflow trigger event