@desplega.ai/agent-swarm 1.69.1 → 1.71.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 +242 -1
- package/package.json +1 -1
- package/src/be/db-queries/oauth.ts +45 -15
- package/src/be/db-queries/tracker.ts +109 -0
- package/src/be/migrations/043_jira_source.sql +128 -0
- package/src/commands/runner.ts +8 -2
- package/src/hooks/hook.ts +4 -2
- package/src/http/core.ts +21 -26
- package/src/http/index.ts +9 -1
- package/src/http/mcp-oauth.ts +132 -60
- package/src/http/mcp-servers.ts +5 -1
- package/src/http/route-def.ts +19 -0
- package/src/http/trackers/index.ts +13 -0
- package/src/http/trackers/jira.ts +331 -0
- package/src/jira/adf.ts +132 -0
- package/src/jira/app.ts +65 -0
- package/src/jira/client.ts +82 -0
- package/src/jira/index.ts +24 -0
- package/src/jira/metadata.ts +104 -0
- package/src/jira/oauth.ts +98 -0
- package/src/jira/outbound.ts +155 -0
- package/src/jira/sync.ts +534 -0
- package/src/jira/templates.ts +84 -0
- package/src/jira/types.ts +35 -0
- package/src/jira/webhook-lifecycle.ts +363 -0
- package/src/jira/webhook.ts +159 -0
- package/src/oauth/wrapper.ts +11 -1
- package/src/providers/claude-adapter.ts +50 -29
- package/src/server.ts +2 -0
- package/src/tasks/context-key.ts +29 -1
- package/src/telemetry.ts +38 -3
- package/src/tests/claude-adapter.test.ts +143 -1
- package/src/tests/context-key.test.ts +19 -0
- package/src/tests/core-auth.test.ts +142 -0
- package/src/tests/jira-adf.test.ts +239 -0
- package/src/tests/jira-metadata.test.ts +147 -0
- package/src/tests/jira-oauth.test.ts +167 -0
- package/src/tests/jira-outbound-sync.test.ts +334 -0
- package/src/tests/jira-sync.test.ts +327 -0
- package/src/tests/jira-webhook-lifecycle.test.ts +234 -0
- package/src/tests/jira-webhook.test.ts +274 -0
- package/src/tests/mcp-oauth-resolve-secrets.test.ts +79 -0
- package/src/tests/telemetry-init.test.ts +108 -0
- package/src/tests/tool-annotations.test.ts +1 -0
- package/src/tools/slack-post.ts +10 -3
- package/src/tools/slack-start-thread.ts +123 -0
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/tracker/tracker-link-task.ts +1 -1
- package/src/tools/tracker/tracker-map-agent.ts +1 -1
- package/src/tools/tracker/tracker-status.ts +1 -1
- package/src/tools/tracker/tracker-sync-status.ts +1 -1
- package/src/tools/update-profile.ts +5 -2
- package/src/tracker/types.ts +1 -1
- package/src/types.ts +1 -0
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.71.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.",
|
|
@@ -6151,6 +6209,189 @@
|
|
|
6151
6209
|
}
|
|
6152
6210
|
}
|
|
6153
6211
|
},
|
|
6212
|
+
"/api/trackers/jira/authorize": {
|
|
6213
|
+
"get": {
|
|
6214
|
+
"summary": "Redirect to Atlassian OAuth consent screen",
|
|
6215
|
+
"tags": [
|
|
6216
|
+
"Trackers"
|
|
6217
|
+
],
|
|
6218
|
+
"responses": {
|
|
6219
|
+
"302": {
|
|
6220
|
+
"description": "Redirect to Atlassian OAuth"
|
|
6221
|
+
},
|
|
6222
|
+
"500": {
|
|
6223
|
+
"description": "Failed to generate authorization URL"
|
|
6224
|
+
},
|
|
6225
|
+
"503": {
|
|
6226
|
+
"description": "Jira integration not configured"
|
|
6227
|
+
}
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
6230
|
+
},
|
|
6231
|
+
"/api/trackers/jira/callback": {
|
|
6232
|
+
"get": {
|
|
6233
|
+
"summary": "Handle Jira OAuth callback (resolves cloudId via accessible-resources)",
|
|
6234
|
+
"tags": [
|
|
6235
|
+
"Trackers"
|
|
6236
|
+
],
|
|
6237
|
+
"parameters": [
|
|
6238
|
+
{
|
|
6239
|
+
"schema": {
|
|
6240
|
+
"type": "string"
|
|
6241
|
+
},
|
|
6242
|
+
"required": true,
|
|
6243
|
+
"name": "code",
|
|
6244
|
+
"in": "query"
|
|
6245
|
+
},
|
|
6246
|
+
{
|
|
6247
|
+
"schema": {
|
|
6248
|
+
"type": "string"
|
|
6249
|
+
},
|
|
6250
|
+
"required": true,
|
|
6251
|
+
"name": "state",
|
|
6252
|
+
"in": "query"
|
|
6253
|
+
}
|
|
6254
|
+
],
|
|
6255
|
+
"responses": {
|
|
6256
|
+
"200": {
|
|
6257
|
+
"description": "OAuth complete"
|
|
6258
|
+
},
|
|
6259
|
+
"400": {
|
|
6260
|
+
"description": "Invalid state or code"
|
|
6261
|
+
},
|
|
6262
|
+
"500": {
|
|
6263
|
+
"description": "Token exchange or accessible-resources fetch failed"
|
|
6264
|
+
}
|
|
6265
|
+
}
|
|
6266
|
+
}
|
|
6267
|
+
},
|
|
6268
|
+
"/api/trackers/jira/status": {
|
|
6269
|
+
"get": {
|
|
6270
|
+
"summary": "Jira connection status, cloudId/siteUrl, token expiry, expected webhook URL, scope/token-config flags",
|
|
6271
|
+
"tags": [
|
|
6272
|
+
"Trackers"
|
|
6273
|
+
],
|
|
6274
|
+
"security": [
|
|
6275
|
+
{
|
|
6276
|
+
"bearerAuth": []
|
|
6277
|
+
}
|
|
6278
|
+
],
|
|
6279
|
+
"responses": {
|
|
6280
|
+
"200": {
|
|
6281
|
+
"description": "Connection status"
|
|
6282
|
+
},
|
|
6283
|
+
"503": {
|
|
6284
|
+
"description": "Jira integration not configured"
|
|
6285
|
+
}
|
|
6286
|
+
}
|
|
6287
|
+
}
|
|
6288
|
+
},
|
|
6289
|
+
"/api/trackers/jira/webhook/{token}": {
|
|
6290
|
+
"post": {
|
|
6291
|
+
"summary": "Receive Jira webhook events (URL-token authenticated). Phase 2 stub — Phase 3 fills in dispatch.",
|
|
6292
|
+
"tags": [
|
|
6293
|
+
"Trackers"
|
|
6294
|
+
],
|
|
6295
|
+
"parameters": [
|
|
6296
|
+
{
|
|
6297
|
+
"schema": {
|
|
6298
|
+
"type": "string"
|
|
6299
|
+
},
|
|
6300
|
+
"required": true,
|
|
6301
|
+
"name": "token",
|
|
6302
|
+
"in": "path"
|
|
6303
|
+
}
|
|
6304
|
+
],
|
|
6305
|
+
"responses": {
|
|
6306
|
+
"200": {
|
|
6307
|
+
"description": "Event accepted"
|
|
6308
|
+
},
|
|
6309
|
+
"401": {
|
|
6310
|
+
"description": "Invalid URL token"
|
|
6311
|
+
},
|
|
6312
|
+
"503": {
|
|
6313
|
+
"description": "Jira webhook handler not configured"
|
|
6314
|
+
}
|
|
6315
|
+
}
|
|
6316
|
+
}
|
|
6317
|
+
},
|
|
6318
|
+
"/api/trackers/jira/webhook-register": {
|
|
6319
|
+
"post": {
|
|
6320
|
+
"summary": "Register a Jira dynamic webhook (admin only)",
|
|
6321
|
+
"tags": [
|
|
6322
|
+
"Trackers"
|
|
6323
|
+
],
|
|
6324
|
+
"security": [
|
|
6325
|
+
{
|
|
6326
|
+
"bearerAuth": []
|
|
6327
|
+
}
|
|
6328
|
+
],
|
|
6329
|
+
"requestBody": {
|
|
6330
|
+
"content": {
|
|
6331
|
+
"application/json": {
|
|
6332
|
+
"schema": {
|
|
6333
|
+
"type": "object",
|
|
6334
|
+
"properties": {
|
|
6335
|
+
"jqlFilter": {
|
|
6336
|
+
"type": "string",
|
|
6337
|
+
"minLength": 1
|
|
6338
|
+
}
|
|
6339
|
+
},
|
|
6340
|
+
"required": [
|
|
6341
|
+
"jqlFilter"
|
|
6342
|
+
]
|
|
6343
|
+
}
|
|
6344
|
+
}
|
|
6345
|
+
}
|
|
6346
|
+
},
|
|
6347
|
+
"responses": {
|
|
6348
|
+
"200": {
|
|
6349
|
+
"description": "Webhook registered"
|
|
6350
|
+
},
|
|
6351
|
+
"400": {
|
|
6352
|
+
"description": "Invalid jqlFilter"
|
|
6353
|
+
},
|
|
6354
|
+
"503": {
|
|
6355
|
+
"description": "Jira not connected or JIRA_WEBHOOK_TOKEN missing"
|
|
6356
|
+
}
|
|
6357
|
+
}
|
|
6358
|
+
}
|
|
6359
|
+
},
|
|
6360
|
+
"/api/trackers/jira/webhook/{id}": {
|
|
6361
|
+
"delete": {
|
|
6362
|
+
"summary": "Delete a Jira dynamic webhook (admin only)",
|
|
6363
|
+
"tags": [
|
|
6364
|
+
"Trackers"
|
|
6365
|
+
],
|
|
6366
|
+
"security": [
|
|
6367
|
+
{
|
|
6368
|
+
"bearerAuth": []
|
|
6369
|
+
}
|
|
6370
|
+
],
|
|
6371
|
+
"parameters": [
|
|
6372
|
+
{
|
|
6373
|
+
"schema": {
|
|
6374
|
+
"type": "integer",
|
|
6375
|
+
"exclusiveMinimum": 0
|
|
6376
|
+
},
|
|
6377
|
+
"required": true,
|
|
6378
|
+
"name": "id",
|
|
6379
|
+
"in": "path"
|
|
6380
|
+
}
|
|
6381
|
+
],
|
|
6382
|
+
"responses": {
|
|
6383
|
+
"200": {
|
|
6384
|
+
"description": "Webhook deleted"
|
|
6385
|
+
},
|
|
6386
|
+
"400": {
|
|
6387
|
+
"description": "Invalid webhook id"
|
|
6388
|
+
},
|
|
6389
|
+
"503": {
|
|
6390
|
+
"description": "Jira not connected"
|
|
6391
|
+
}
|
|
6392
|
+
}
|
|
6393
|
+
}
|
|
6394
|
+
},
|
|
6154
6395
|
"/api/trackers/linear/authorize": {
|
|
6155
6396
|
"get": {
|
|
6156
6397
|
"summary": "Redirect to Linear OAuth consent screen",
|
package/package.json
CHANGED
|
@@ -39,9 +39,14 @@ export function upsertOAuthApp(
|
|
|
39
39
|
metadata?: string;
|
|
40
40
|
},
|
|
41
41
|
): void {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
// metadata is treated as a runtime-owned column (cloudId, webhookIds, etc.
|
|
43
|
+
// are written by OAuth callback + webhook-register flows). On INSERT we
|
|
44
|
+
// seed it with whatever the caller passed (or "{}"); on UPDATE we ONLY
|
|
45
|
+
// overwrite when the caller explicitly provided one — otherwise the
|
|
46
|
+
// existing value is preserved across server restarts.
|
|
47
|
+
const metadataProvided = data.metadata !== undefined;
|
|
48
|
+
const sql = metadataProvided
|
|
49
|
+
? `INSERT INTO oauth_apps (provider, clientId, clientSecret, authorizeUrl, tokenUrl, redirectUri, scopes, metadata)
|
|
45
50
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
46
51
|
ON CONFLICT(provider) DO UPDATE SET
|
|
47
52
|
clientId = excluded.clientId,
|
|
@@ -51,18 +56,43 @@ export function upsertOAuthApp(
|
|
|
51
56
|
redirectUri = excluded.redirectUri,
|
|
52
57
|
scopes = excluded.scopes,
|
|
53
58
|
metadata = excluded.metadata,
|
|
54
|
-
updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`
|
|
60
|
+
: `INSERT INTO oauth_apps (provider, clientId, clientSecret, authorizeUrl, tokenUrl, redirectUri, scopes, metadata)
|
|
61
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, '{}')
|
|
62
|
+
ON CONFLICT(provider) DO UPDATE SET
|
|
63
|
+
clientId = excluded.clientId,
|
|
64
|
+
clientSecret = excluded.clientSecret,
|
|
65
|
+
authorizeUrl = excluded.authorizeUrl,
|
|
66
|
+
tokenUrl = excluded.tokenUrl,
|
|
67
|
+
redirectUri = excluded.redirectUri,
|
|
68
|
+
scopes = excluded.scopes,
|
|
69
|
+
updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
|
|
70
|
+
if (metadataProvided) {
|
|
71
|
+
getDb()
|
|
72
|
+
.query(sql)
|
|
73
|
+
.run(
|
|
74
|
+
provider,
|
|
75
|
+
data.clientId,
|
|
76
|
+
data.clientSecret,
|
|
77
|
+
data.authorizeUrl,
|
|
78
|
+
data.tokenUrl,
|
|
79
|
+
data.redirectUri,
|
|
80
|
+
data.scopes,
|
|
81
|
+
data.metadata as string,
|
|
82
|
+
);
|
|
83
|
+
} else {
|
|
84
|
+
getDb()
|
|
85
|
+
.query(sql)
|
|
86
|
+
.run(
|
|
87
|
+
provider,
|
|
88
|
+
data.clientId,
|
|
89
|
+
data.clientSecret,
|
|
90
|
+
data.authorizeUrl,
|
|
91
|
+
data.tokenUrl,
|
|
92
|
+
data.redirectUri,
|
|
93
|
+
data.scopes,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
66
96
|
}
|
|
67
97
|
|
|
68
98
|
// ── OAuth Tokens ──
|
|
@@ -41,6 +41,75 @@ export function getTrackerSyncByExternalId(
|
|
|
41
41
|
return row ? normalizeTrackerSync(row) : null;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Idempotent UNIQUE-gated insert into `tracker_sync`. Returns
|
|
46
|
+
* `{ inserted: true, sync }` when a fresh row was created, or
|
|
47
|
+
* `{ inserted: false, sync }` when the `(provider, entityType, externalId)`
|
|
48
|
+
* tuple already had a row. Used by inbound webhook handlers (currently Jira)
|
|
49
|
+
* to gate task creation atomically: insert sync row first, only call
|
|
50
|
+
* `createTaskExtended` if `inserted === true`.
|
|
51
|
+
*
|
|
52
|
+
* Note: this is a "claim" insert — `swarmId` is initially the sentinel value
|
|
53
|
+
* passed in (callers typically pass a placeholder like `""` or a known UUID),
|
|
54
|
+
* then update it with `updateTrackerSyncSwarmId` once the task is created.
|
|
55
|
+
*/
|
|
56
|
+
export function createTrackerSyncIfAbsent(data: {
|
|
57
|
+
provider: string;
|
|
58
|
+
entityType: "task";
|
|
59
|
+
providerEntityType?: string | null;
|
|
60
|
+
swarmId: string;
|
|
61
|
+
externalId: string;
|
|
62
|
+
externalIdentifier?: string | null;
|
|
63
|
+
externalUrl?: string | null;
|
|
64
|
+
lastSyncOrigin?: "swarm" | "external" | null;
|
|
65
|
+
lastDeliveryId?: string | null;
|
|
66
|
+
syncDirection?: "inbound" | "outbound" | "bidirectional";
|
|
67
|
+
}): { inserted: boolean; sync: TrackerSync } {
|
|
68
|
+
const insertResult = getDb()
|
|
69
|
+
.query(
|
|
70
|
+
`INSERT INTO tracker_sync (provider, entityType, providerEntityType, swarmId, externalId, externalIdentifier, externalUrl, lastSyncOrigin, lastDeliveryId, syncDirection)
|
|
71
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
72
|
+
ON CONFLICT (provider, entityType, externalId) DO NOTHING
|
|
73
|
+
RETURNING *`,
|
|
74
|
+
)
|
|
75
|
+
.get(
|
|
76
|
+
data.provider,
|
|
77
|
+
data.entityType,
|
|
78
|
+
data.providerEntityType ?? null,
|
|
79
|
+
data.swarmId,
|
|
80
|
+
data.externalId,
|
|
81
|
+
data.externalIdentifier ?? null,
|
|
82
|
+
data.externalUrl ?? null,
|
|
83
|
+
data.lastSyncOrigin ?? null,
|
|
84
|
+
data.lastDeliveryId ?? null,
|
|
85
|
+
data.syncDirection ?? "inbound",
|
|
86
|
+
) as TrackerSync | null;
|
|
87
|
+
|
|
88
|
+
if (insertResult) {
|
|
89
|
+
return { inserted: true, sync: normalizeTrackerSync(insertResult) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Row already existed — fetch it for the caller.
|
|
93
|
+
const existing = getTrackerSyncByExternalId(data.provider, data.entityType, data.externalId);
|
|
94
|
+
if (!existing) {
|
|
95
|
+
// Should be unreachable: ON CONFLICT means a row exists, but this guard
|
|
96
|
+
// satisfies the type system and surfaces unexpected races loudly.
|
|
97
|
+
throw new Error(
|
|
98
|
+
`[tracker] createTrackerSyncIfAbsent: ON CONFLICT fired but no existing row found for (${data.provider}, ${data.entityType}, ${data.externalId})`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return { inserted: false, sync: existing };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Update the `swarmId` on an existing `tracker_sync` row. Used after the
|
|
106
|
+
* idempotent `createTrackerSyncIfAbsent` returned `{ inserted: true }` and
|
|
107
|
+
* we've now created the swarm task that should own this row.
|
|
108
|
+
*/
|
|
109
|
+
export function updateTrackerSyncSwarmId(id: string, swarmId: string): void {
|
|
110
|
+
getDb().query("UPDATE tracker_sync SET swarmId = ? WHERE id = ?").run(swarmId, id);
|
|
111
|
+
}
|
|
112
|
+
|
|
44
113
|
export function createTrackerSync(data: {
|
|
45
114
|
provider: string;
|
|
46
115
|
entityType: "task";
|
|
@@ -128,6 +197,46 @@ export function deleteTrackerSync(id: string): void {
|
|
|
128
197
|
getDb().query("DELETE FROM tracker_sync WHERE id = ?").run(id);
|
|
129
198
|
}
|
|
130
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Check whether a `tracker_sync` row exists for `provider` with the given
|
|
202
|
+
* `lastDeliveryId`. Used by inbound webhook handlers (Jira) to dedupe
|
|
203
|
+
* deliveries via DB-persisted state instead of a process-local Map.
|
|
204
|
+
*
|
|
205
|
+
* Returns `false` when `deliveryId` is falsy/empty so callers don't have to
|
|
206
|
+
* branch.
|
|
207
|
+
*/
|
|
208
|
+
export function hasTrackerDelivery(
|
|
209
|
+
provider: string,
|
|
210
|
+
deliveryId: string | null | undefined,
|
|
211
|
+
): boolean {
|
|
212
|
+
if (!deliveryId) return false;
|
|
213
|
+
const row = getDb()
|
|
214
|
+
.query("SELECT 1 AS hit FROM tracker_sync WHERE provider = ? AND lastDeliveryId = ? LIMIT 1")
|
|
215
|
+
.get(provider, deliveryId) as { hit: number } | null;
|
|
216
|
+
return !!row;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Mark a delivery as processed by writing `deliveryId` into the relevant
|
|
221
|
+
* `tracker_sync` row identified by `(provider, entityType, externalId)`.
|
|
222
|
+
*
|
|
223
|
+
* No-op when the row doesn't exist yet (the very first inbound event creates
|
|
224
|
+
* the row via `createTrackerSyncIfAbsent` — recording delivery happens after
|
|
225
|
+
* that). Caller is responsible for ordering.
|
|
226
|
+
*/
|
|
227
|
+
export function markTrackerDelivery(
|
|
228
|
+
provider: string,
|
|
229
|
+
entityType: "task",
|
|
230
|
+
externalId: string,
|
|
231
|
+
deliveryId: string,
|
|
232
|
+
): void {
|
|
233
|
+
getDb()
|
|
234
|
+
.query(
|
|
235
|
+
"UPDATE tracker_sync SET lastDeliveryId = ? WHERE provider = ? AND entityType = ? AND externalId = ?",
|
|
236
|
+
)
|
|
237
|
+
.run(deliveryId, provider, entityType, externalId);
|
|
238
|
+
}
|
|
239
|
+
|
|
131
240
|
export function getAllTrackerSyncs(provider?: string, entityType?: "task"): TrackerSync[] {
|
|
132
241
|
const conditions: string[] = [];
|
|
133
242
|
const values: string[] = [];
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
-- Add 'jira' to the agent_tasks.source CHECK constraint.
|
|
2
|
+
-- SQLite cannot ALTER an existing CHECK; we follow the table-rebuild pattern
|
|
3
|
+
-- (per migration 026). Column list mirrors the live post-042 schema verbatim
|
|
4
|
+
-- — including all post-026 additions (credentialKeySuffix/Type, requestedByUserId,
|
|
5
|
+
-- vcsInstallationId, vcsNodeId, slackReplySent, swarmVersion, contextKey).
|
|
6
|
+
-- INSERT uses an explicit column list (no `SELECT *`) to be robust against
|
|
7
|
+
-- column-order drift between SQLite versions.
|
|
8
|
+
PRAGMA foreign_keys=off;
|
|
9
|
+
|
|
10
|
+
CREATE TABLE agent_tasks_new (
|
|
11
|
+
id TEXT PRIMARY KEY,
|
|
12
|
+
agentId TEXT,
|
|
13
|
+
creatorAgentId TEXT,
|
|
14
|
+
task TEXT NOT NULL,
|
|
15
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
16
|
+
source TEXT NOT NULL DEFAULT 'mcp' CHECK(source IN ('mcp', 'slack', 'api', 'github', 'gitlab', 'agentmail', 'system', 'schedule', 'workflow', 'linear', 'jira')),
|
|
17
|
+
taskType TEXT,
|
|
18
|
+
tags TEXT DEFAULT '[]',
|
|
19
|
+
priority INTEGER DEFAULT 50,
|
|
20
|
+
dependsOn TEXT DEFAULT '[]',
|
|
21
|
+
offeredTo TEXT,
|
|
22
|
+
offeredAt TEXT,
|
|
23
|
+
acceptedAt TEXT,
|
|
24
|
+
rejectionReason TEXT,
|
|
25
|
+
slackChannelId TEXT,
|
|
26
|
+
slackThreadTs TEXT,
|
|
27
|
+
slackUserId TEXT,
|
|
28
|
+
mentionMessageId TEXT,
|
|
29
|
+
mentionChannelId TEXT,
|
|
30
|
+
vcsProvider TEXT,
|
|
31
|
+
vcsRepo TEXT,
|
|
32
|
+
vcsEventType TEXT,
|
|
33
|
+
vcsNumber INTEGER,
|
|
34
|
+
vcsCommentId INTEGER,
|
|
35
|
+
vcsAuthor TEXT,
|
|
36
|
+
vcsUrl TEXT,
|
|
37
|
+
parentTaskId TEXT,
|
|
38
|
+
claudeSessionId TEXT,
|
|
39
|
+
agentmailInboxId TEXT,
|
|
40
|
+
agentmailMessageId TEXT,
|
|
41
|
+
agentmailThreadId TEXT,
|
|
42
|
+
model TEXT,
|
|
43
|
+
scheduleId TEXT,
|
|
44
|
+
workflowRunId TEXT REFERENCES workflow_runs(id),
|
|
45
|
+
workflowRunStepId TEXT REFERENCES workflow_run_steps(id),
|
|
46
|
+
createdAt TEXT NOT NULL,
|
|
47
|
+
lastUpdatedAt TEXT NOT NULL,
|
|
48
|
+
finishedAt TEXT,
|
|
49
|
+
failureReason TEXT,
|
|
50
|
+
output TEXT,
|
|
51
|
+
progress TEXT,
|
|
52
|
+
notifiedAt TEXT,
|
|
53
|
+
dir TEXT,
|
|
54
|
+
outputSchema TEXT,
|
|
55
|
+
compactionCount INTEGER DEFAULT 0,
|
|
56
|
+
peakContextPercent REAL,
|
|
57
|
+
totalContextTokensUsed INTEGER,
|
|
58
|
+
contextWindowSize INTEGER,
|
|
59
|
+
was_paused INTEGER NOT NULL DEFAULT 0,
|
|
60
|
+
credentialKeySuffix TEXT,
|
|
61
|
+
credentialKeyType TEXT,
|
|
62
|
+
requestedByUserId TEXT REFERENCES users(id),
|
|
63
|
+
vcsInstallationId INTEGER,
|
|
64
|
+
vcsNodeId TEXT,
|
|
65
|
+
slackReplySent INTEGER DEFAULT 0,
|
|
66
|
+
swarmVersion TEXT,
|
|
67
|
+
contextKey TEXT
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
INSERT INTO agent_tasks_new (
|
|
71
|
+
id, agentId, creatorAgentId, task, status, source, taskType, tags,
|
|
72
|
+
priority, dependsOn, offeredTo, offeredAt, acceptedAt, rejectionReason,
|
|
73
|
+
slackChannelId, slackThreadTs, slackUserId,
|
|
74
|
+
mentionMessageId, mentionChannelId,
|
|
75
|
+
vcsProvider, vcsRepo, vcsEventType, vcsNumber, vcsCommentId, vcsAuthor, vcsUrl,
|
|
76
|
+
parentTaskId, claudeSessionId,
|
|
77
|
+
agentmailInboxId, agentmailMessageId, agentmailThreadId,
|
|
78
|
+
model, scheduleId, workflowRunId, workflowRunStepId,
|
|
79
|
+
createdAt, lastUpdatedAt, finishedAt, failureReason, output, progress, notifiedAt,
|
|
80
|
+
dir, outputSchema, compactionCount, peakContextPercent,
|
|
81
|
+
totalContextTokensUsed, contextWindowSize, was_paused,
|
|
82
|
+
credentialKeySuffix, credentialKeyType, requestedByUserId,
|
|
83
|
+
vcsInstallationId, vcsNodeId, slackReplySent, swarmVersion, contextKey
|
|
84
|
+
)
|
|
85
|
+
SELECT
|
|
86
|
+
id, agentId, creatorAgentId, task, status, source, taskType, tags,
|
|
87
|
+
priority, dependsOn, offeredTo, offeredAt, acceptedAt, rejectionReason,
|
|
88
|
+
slackChannelId, slackThreadTs, slackUserId,
|
|
89
|
+
mentionMessageId, mentionChannelId,
|
|
90
|
+
vcsProvider, vcsRepo, vcsEventType, vcsNumber, vcsCommentId, vcsAuthor, vcsUrl,
|
|
91
|
+
parentTaskId, claudeSessionId,
|
|
92
|
+
agentmailInboxId, agentmailMessageId, agentmailThreadId,
|
|
93
|
+
model, scheduleId, workflowRunId, workflowRunStepId,
|
|
94
|
+
createdAt, lastUpdatedAt, finishedAt, failureReason, output, progress, notifiedAt,
|
|
95
|
+
dir, outputSchema, compactionCount, peakContextPercent,
|
|
96
|
+
totalContextTokensUsed, contextWindowSize, was_paused,
|
|
97
|
+
credentialKeySuffix, credentialKeyType, requestedByUserId,
|
|
98
|
+
vcsInstallationId, vcsNodeId, slackReplySent, swarmVersion, contextKey
|
|
99
|
+
FROM agent_tasks;
|
|
100
|
+
|
|
101
|
+
DROP TABLE agent_tasks;
|
|
102
|
+
ALTER TABLE agent_tasks_new RENAME TO agent_tasks;
|
|
103
|
+
|
|
104
|
+
-- Recreate every index that existed on agent_tasks (per `grep -rn "ON agent_tasks(" src/be/migrations/`):
|
|
105
|
+
-- 001/004/006/009/026: agentId, status, offeredTo, taskType, agentmailThreadId, scheduleId, workflowRunId
|
|
106
|
+
-- 031: requestedByUserId (partial)
|
|
107
|
+
-- 034: parentTaskId
|
|
108
|
+
-- 037: swarmVersion
|
|
109
|
+
-- 040: composite (slackChannelId, slackThreadTs, status)
|
|
110
|
+
-- 042: contextKey + (contextKey, status) composite
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_agentId ON agent_tasks(agentId);
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_status ON agent_tasks(status);
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_offeredTo ON agent_tasks(offeredTo);
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_taskType ON agent_tasks(taskType);
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_agentmailThreadId ON agent_tasks(agentmailThreadId);
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_schedule_id ON agent_tasks(scheduleId);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_workflow_run ON agent_tasks(workflowRunId);
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_requested_by ON agent_tasks(requestedByUserId) WHERE requestedByUserId IS NOT NULL;
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_parentTaskId ON agent_tasks(parentTaskId);
|
|
120
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_swarmVersion ON agent_tasks(swarmVersion);
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_slack_thread
|
|
122
|
+
ON agent_tasks(slackChannelId, slackThreadTs, status);
|
|
123
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_context_key
|
|
124
|
+
ON agent_tasks(contextKey);
|
|
125
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_context_key_status
|
|
126
|
+
ON agent_tasks(contextKey, status);
|
|
127
|
+
|
|
128
|
+
PRAGMA foreign_keys=on;
|
package/src/commands/runner.ts
CHANGED
|
@@ -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",
|
|
@@ -2206,8 +2207,13 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2206
2207
|
configureHttpResolver(apiUrl, process.env.API_KEY);
|
|
2207
2208
|
}
|
|
2208
2209
|
|
|
2209
|
-
// Initialize anonymized telemetry (opt-out via ANONYMIZED_TELEMETRY=false)
|
|
2210
|
-
// Workers use HTTP-based config access (cannot import DB directly)
|
|
2210
|
+
// Initialize anonymized telemetry (opt-out via ANONYMIZED_TELEMETRY=false).
|
|
2211
|
+
// Workers use HTTP-based config access (cannot import DB directly).
|
|
2212
|
+
// IMPORTANT: workers must NOT pass `generateIfMissing` — the api-server is
|
|
2213
|
+
// the sole authority for `telemetry_installation_id`. If the API hasn't
|
|
2214
|
+
// persisted one yet (network blip, fresh boot, API down), the worker simply
|
|
2215
|
+
// skips telemetry instead of minting a fresh `install_<hex>` ID per
|
|
2216
|
+
// restart, which floods prod metrics with phantom installs.
|
|
2211
2217
|
{
|
|
2212
2218
|
const telemetryApiKey = process.env.API_KEY;
|
|
2213
2219
|
await initTelemetry(
|
package/src/hooks/hook.ts
CHANGED
|
@@ -355,8 +355,10 @@ export async function handleHook(): Promise<void> {
|
|
|
355
355
|
}
|
|
356
356
|
};
|
|
357
357
|
|
|
358
|
-
// Minimum length for SOUL.md and IDENTITY.md to prevent accidental corruption
|
|
359
|
-
|
|
358
|
+
// Minimum length for SOUL.md and IDENTITY.md to prevent accidental corruption.
|
|
359
|
+
// Raised from 100 to 500 after Picateclas profile corruption recurrences where
|
|
360
|
+
// a 234-char test sentinel payload was syncing into the real agent's DB row.
|
|
361
|
+
const IDENTITY_FILE_MIN_LENGTH = 500;
|
|
360
362
|
|
|
361
363
|
/**
|
|
362
364
|
* Sync SOUL.md and IDENTITY.md content back to the server
|