@agent-native/core 0.98.6 → 0.98.8
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/docs/content/external-agents.mdx +22 -12
- package/corpus/core/docs/content/locales/ar-SA/external-agents.mdx +8 -2
- package/corpus/core/docs/content/locales/de-DE/external-agents.mdx +8 -2
- package/corpus/core/docs/content/locales/es-ES/external-agents.mdx +8 -2
- package/corpus/core/docs/content/locales/fr-FR/external-agents.mdx +8 -2
- package/corpus/core/docs/content/locales/hi-IN/external-agents.mdx +8 -2
- package/corpus/core/docs/content/locales/ja-JP/external-agents.mdx +8 -2
- package/corpus/core/docs/content/locales/ko-KR/external-agents.mdx +8 -2
- package/corpus/core/docs/content/locales/pt-BR/external-agents.mdx +8 -2
- package/corpus/core/docs/content/locales/zh-CN/external-agents.mdx +8 -2
- package/corpus/core/docs/content/locales/zh-TW/external-agents.mdx +8 -2
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/handlers.ts +6 -5
- package/corpus/core/src/a2a/task-store.ts +63 -0
- package/corpus/core/src/client/session-replay.ts +90 -6
- package/corpus/core/src/integrations/adapters/slack.ts +110 -19
- package/corpus/core/src/integrations/identity.ts +103 -39
- package/corpus/core/src/integrations/pending-tasks-retry-job.ts +2 -1
- package/corpus/core/src/integrations/pending-tasks-store.ts +1 -1
- package/corpus/core/src/integrations/plugin.ts +215 -8
- package/corpus/core/src/integrations/types.ts +37 -0
- package/corpus/core/src/integrations/webhook-handler.ts +1 -0
- package/corpus/core/src/mcp/builtin-tools.ts +5 -2
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +4 -1
- package/corpus/templates/analytics/AGENTS.md +9 -9
- package/corpus/templates/analytics/changelog/2026-07-12-analytics-uses-the-full-in-app-agent-for-multi-step-incident.md +6 -0
- package/corpus/templates/analytics/server/handlers/session-replay.ts +15 -9
- package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +23 -5
- package/corpus/templates/analytics/server/lib/session-replay.ts +26 -5
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -1
- package/corpus/templates/analytics/server/routes/api/session-replay/agent-diagnostics.json.get.ts +8 -5
- package/corpus/templates/analytics/server/routes/api/session-replay/agent-events.json.get.ts +8 -3
- package/corpus/templates/calendar/app/components/calendar/DeleteEventDialog.tsx +12 -2
- package/dist/a2a/handlers.d.ts.map +1 -1
- package/dist/a2a/handlers.js +6 -6
- package/dist/a2a/handlers.js.map +1 -1
- package/dist/a2a/task-store.d.ts +12 -0
- package/dist/a2a/task-store.d.ts.map +1 -1
- package/dist/a2a/task-store.js +54 -0
- package/dist/a2a/task-store.js.map +1 -1
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +71 -9
- package/dist/client/session-replay.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/adapters/slack.d.ts.map +1 -1
- package/dist/integrations/adapters/slack.js +94 -11
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/identity.d.ts +19 -2
- package/dist/integrations/identity.d.ts.map +1 -1
- package/dist/integrations/identity.js +76 -34
- package/dist/integrations/identity.js.map +1 -1
- package/dist/integrations/pending-tasks-retry-job.d.ts.map +1 -1
- package/dist/integrations/pending-tasks-retry-job.js +2 -1
- package/dist/integrations/pending-tasks-retry-job.js.map +1 -1
- package/dist/integrations/pending-tasks-store.js +1 -1
- package/dist/integrations/pending-tasks-store.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +165 -9
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/types.d.ts +32 -0
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/integrations/webhook-handler.js +1 -0
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +5 -2
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/external-agents.mdx +22 -12
- package/docs/content/locales/ar-SA/external-agents.mdx +8 -2
- package/docs/content/locales/de-DE/external-agents.mdx +8 -2
- package/docs/content/locales/es-ES/external-agents.mdx +8 -2
- package/docs/content/locales/fr-FR/external-agents.mdx +8 -2
- package/docs/content/locales/hi-IN/external-agents.mdx +8 -2
- package/docs/content/locales/ja-JP/external-agents.mdx +8 -2
- package/docs/content/locales/ko-KR/external-agents.mdx +8 -2
- package/docs/content/locales/pt-BR/external-agents.mdx +8 -2
- package/docs/content/locales/zh-CN/external-agents.mdx +8 -2
- package/docs/content/locales/zh-TW/external-agents.mdx +8 -2
- package/package.json +1 -1
|
@@ -2,6 +2,22 @@ import { getDbExec } from "../db/client.js";
|
|
|
2
2
|
import { upsertVerifiedIntegrationIdentity } from "./identity-links-store.js";
|
|
3
3
|
import { getActiveIntegrationInstallationByKey, getActiveIntegrationInstallationForTenant, } from "./installations-store.js";
|
|
4
4
|
import { slackInstallationKey } from "./slack-oauth.js";
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when the default Slack DM identity ladder declines to run a message.
|
|
7
|
+
* `reason` is a stable machine-readable discriminator (used e.g. to dedupe
|
|
8
|
+
* decline replies); `userFacingMessage` is safe to send back to the sender as
|
|
9
|
+
* a polite reply; `message` stays log-only.
|
|
10
|
+
*/
|
|
11
|
+
export class IntegrationIdentityDeclinedError extends Error {
|
|
12
|
+
reason;
|
|
13
|
+
userFacingMessage;
|
|
14
|
+
constructor(reason, message, userFacingMessage) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "IntegrationIdentityDeclinedError";
|
|
17
|
+
this.reason = reason;
|
|
18
|
+
this.userFacingMessage = userFacingMessage;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
5
21
|
function serviceOwner(platform) {
|
|
6
22
|
return `integration@${platform}`;
|
|
7
23
|
}
|
|
@@ -33,26 +49,26 @@ async function resolveSlackInstallation(incoming) {
|
|
|
33
49
|
: null;
|
|
34
50
|
}
|
|
35
51
|
async function isMemberOfOrg(email, orgId) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
sql: `SELECT 1 FROM org_members
|
|
52
|
+
const { rows } = await getDbExec().execute({
|
|
53
|
+
sql: `SELECT 1 FROM org_members
|
|
39
54
|
WHERE org_id = ? AND LOWER(email) = ?
|
|
40
55
|
LIMIT 1`,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
56
|
+
args: [orgId, email],
|
|
57
|
+
});
|
|
58
|
+
return rows.length > 0;
|
|
48
59
|
}
|
|
49
60
|
/**
|
|
50
61
|
* Resolve the default integration principal.
|
|
51
62
|
*
|
|
52
63
|
* Slack DMs become a user principal only after the adapter has verified the
|
|
53
64
|
* sender email and the email is already a member of the managed installation's
|
|
54
|
-
* Agent Native organization.
|
|
55
|
-
*
|
|
65
|
+
* Agent Native organization. Hydrated full workspace members whose email is
|
|
66
|
+
* missing, unverified, or not an organization member run as an anonymous
|
|
67
|
+
* org-scoped service principal instead — the same visibility shared channels
|
|
68
|
+
* get. Hydration failures, guests/external members, and workspaces without a
|
|
69
|
+
* connected organization are declined with a user-facing message. Shared
|
|
70
|
+
* channels deliberately stay service-scoped so a channel message cannot borrow
|
|
71
|
+
* one participant's private permissions.
|
|
56
72
|
*/
|
|
57
73
|
export async function resolveDefaultIntegrationExecutionContext(incoming) {
|
|
58
74
|
const installation = incoming.platform === "slack"
|
|
@@ -66,35 +82,61 @@ export async function resolveDefaultIntegrationExecutionContext(incoming) {
|
|
|
66
82
|
...(installation?.id ? { installationId: installation.id } : {}),
|
|
67
83
|
};
|
|
68
84
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
!incoming.tenantId) {
|
|
74
|
-
throw new Error("Slack DM sender identity could not be verified; refusing to run with a service principal.");
|
|
85
|
+
// Hydration check first: a transient users.info failure must land on the
|
|
86
|
+
// retry decline here, never on the anonymous org-scoped tier below.
|
|
87
|
+
if (incoming.actorTrust?.verified !== true) {
|
|
88
|
+
throw new IntegrationIdentityDeclinedError("unverified", "Slack DM sender identity could not be hydrated; declining instead of guessing a principal.", "I couldn't verify your Slack identity just now, so I can't run this request. Please try again in a moment.");
|
|
75
89
|
}
|
|
76
|
-
if (incoming.actorTrust
|
|
77
|
-
incoming.actorTrust
|
|
78
|
-
throw new
|
|
90
|
+
if (incoming.actorTrust.memberType === "guest" ||
|
|
91
|
+
incoming.actorTrust.memberType === "external") {
|
|
92
|
+
throw new IntegrationIdentityDeclinedError("guest", "External or guest Slack members cannot use this integration.", "This assistant is only available to members of this workspace's organization.");
|
|
79
93
|
}
|
|
80
94
|
if (!installation?.orgId) {
|
|
81
|
-
throw new
|
|
95
|
+
throw new IntegrationIdentityDeclinedError("unlinked-workspace", "Slack workspace is not connected to an Agent Native organization.", "This Slack workspace isn't connected to an organization yet.");
|
|
82
96
|
}
|
|
83
|
-
|
|
84
|
-
|
|
97
|
+
const email = normalizedEmail(incoming.senderEmail);
|
|
98
|
+
let isOrgMember = false;
|
|
99
|
+
if (email &&
|
|
100
|
+
incoming.senderVerified === true &&
|
|
101
|
+
incoming.senderId &&
|
|
102
|
+
incoming.tenantId) {
|
|
103
|
+
try {
|
|
104
|
+
isOrgMember = await isMemberOfOrg(email, installation.orgId);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// A membership-store outage is not evidence that the sender is merely
|
|
108
|
+
// unlinked. Fail closed instead of widening them to org-wide access.
|
|
109
|
+
throw new IntegrationIdentityDeclinedError("membership-check-failed", "Slack DM organization membership could not be verified.", "I couldn't verify your organization membership just now, so I can't run this request. Please try again in a moment.");
|
|
110
|
+
}
|
|
85
111
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
112
|
+
if (isOrgMember && email && incoming.senderId && incoming.tenantId) {
|
|
113
|
+
const link = await upsertVerifiedIntegrationIdentity({
|
|
114
|
+
platform: incoming.platform,
|
|
115
|
+
tenantId: incoming.tenantId,
|
|
116
|
+
externalUserId: incoming.senderId,
|
|
117
|
+
userEmail: email,
|
|
118
|
+
orgId: installation.orgId,
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
ownerEmail: link.userEmail,
|
|
122
|
+
orgId: link.orgId,
|
|
123
|
+
principalType: "user",
|
|
124
|
+
installationId: installation.id,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// Hydrated full workspace member (member/admin/owner) whose email is
|
|
128
|
+
// unverified, missing (legacy install without users:read.email), or not an
|
|
129
|
+
// organization member: run with the anonymous org-scoped service principal —
|
|
130
|
+
// the same visibility shared channels get. Nothing user-private is
|
|
131
|
+
// accessible. One structured line keeps 100%-anonymous workspaces (legacy
|
|
132
|
+
// scope missing) visible in logs.
|
|
133
|
+
console.warn(`[integrations] anonymous org-scoped principal used: platform=${incoming.platform} teamId=${incoming.tenantId ?? "unknown"} emailPresent=${Boolean(email)} memberType=${incoming.actorTrust.memberType}`);
|
|
93
134
|
return {
|
|
94
|
-
ownerEmail:
|
|
95
|
-
orgId:
|
|
96
|
-
principalType: "
|
|
135
|
+
ownerEmail: serviceOwner(incoming.platform),
|
|
136
|
+
orgId: installation.orgId,
|
|
137
|
+
principalType: "service",
|
|
97
138
|
installationId: installation.id,
|
|
139
|
+
anonymousMember: true,
|
|
98
140
|
};
|
|
99
141
|
}
|
|
100
142
|
//# sourceMappingURL=identity.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/integrations/identity.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDbExec } from \"../db/client.js\";\nimport { upsertVerifiedIntegrationIdentity } from \"./identity-links-store.js\";\nimport {\n getActiveIntegrationInstallationByKey,\n getActiveIntegrationInstallationForTenant,\n} from \"./installations-store.js\";\nimport { slackInstallationKey } from \"./slack-oauth.js\";\nimport type { IncomingMessage, IntegrationExecutionContext } from \"./types.js\";\n\nfunction serviceOwner(platform: string): string {\n return `integration@${platform}`;\n}\n\nfunction normalizedEmail(value: unknown): string | null {\n if (typeof value !== \"string\") return null;\n const email = value.trim().toLowerCase();\n if (!email || email.length > 320 || !email.includes(\"@\")) return null;\n return email;\n}\n\nasync function resolveSlackInstallation(incoming: IncomingMessage) {\n const teamId =\n typeof incoming.platformContext.teamId === \"string\"\n ? incoming.platformContext.teamId\n : incoming.tenantId;\n const enterpriseId =\n typeof incoming.platformContext.enterpriseId === \"string\"\n ? incoming.platformContext.enterpriseId\n : undefined;\n const apiAppId =\n typeof incoming.platformContext.apiAppId === \"string\"\n ? incoming.platformContext.apiAppId\n : undefined;\n if (apiAppId && (teamId || enterpriseId)) {\n const byKey = await getActiveIntegrationInstallationByKey(\n \"slack\",\n slackInstallationKey({ teamId, enterpriseId, apiAppId }),\n );\n if (byKey) return byKey;\n }\n return teamId || enterpriseId\n ? getActiveIntegrationInstallationForTenant(\n \"slack\",\n teamId ?? enterpriseId!,\n )\n : null;\n}\n\nasync function isMemberOfOrg(email: string, orgId: string): Promise<boolean> {\n try {\n const { rows } = await getDbExec().execute({\n sql: `SELECT 1 FROM org_members\n WHERE org_id = ? AND LOWER(email) = ?\n LIMIT 1`,\n args: [orgId, email],\n });\n return rows.length > 0;\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve the default integration principal.\n *\n * Slack DMs become a user principal only after the adapter has verified the\n * sender email and the email is already a member of the managed installation's\n * Agent Native organization. Shared channels deliberately stay service-scoped\n * so a channel message cannot borrow one participant's private permissions.\n */\nexport async function resolveDefaultIntegrationExecutionContext(\n incoming: IncomingMessage,\n): Promise<IntegrationExecutionContext> {\n const installation =\n incoming.platform === \"slack\"\n ? await resolveSlackInstallation(incoming)\n : null;\n\n if (incoming.platform !== \"slack\" || incoming.conversationType !== \"dm\") {\n return {\n ownerEmail: serviceOwner(incoming.platform),\n orgId: installation?.orgId ?? null,\n principalType: \"service\",\n ...(installation?.id ? { installationId: installation.id } : {}),\n };\n }\n\n const email = normalizedEmail(incoming.senderEmail);\n if (\n !email ||\n incoming.senderVerified !== true ||\n !incoming.senderId ||\n !incoming.tenantId\n ) {\n throw new Error(\n \"Slack DM sender identity could not be verified; refusing to run with a service principal.\",\n );\n }\n if (\n incoming.actorTrust?.memberType === \"guest\" ||\n incoming.actorTrust?.memberType === \"external\"\n ) {\n throw new Error(\n \"External or guest Slack members cannot use personal Agent Native permissions.\",\n );\n }\n if (!installation?.orgId) {\n throw new Error(\n \"Slack workspace is not connected to an Agent Native organization.\",\n );\n }\n\n if (!(await isMemberOfOrg(email, installation.orgId))) {\n throw new Error(\n \"Slack DM sender is not a member of the connected Agent Native organization.\",\n );\n }\n\n const link = await upsertVerifiedIntegrationIdentity({\n platform: incoming.platform,\n tenantId: incoming.tenantId,\n externalUserId: incoming.senderId,\n userEmail: email,\n orgId: installation.orgId,\n });\n return {\n ownerEmail: link.userEmail,\n orgId: link.orgId,\n principalType: \"user\",\n installationId: installation.id,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/integrations/identity.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDbExec } from \"../db/client.js\";\nimport { upsertVerifiedIntegrationIdentity } from \"./identity-links-store.js\";\nimport {\n getActiveIntegrationInstallationByKey,\n getActiveIntegrationInstallationForTenant,\n} from \"./installations-store.js\";\nimport { slackInstallationKey } from \"./slack-oauth.js\";\nimport type { IncomingMessage, IntegrationExecutionContext } from \"./types.js\";\n\nexport type IntegrationIdentityDeclineReason =\n | \"unverified\"\n | \"guest\"\n | \"unlinked-workspace\"\n | \"membership-check-failed\";\n\n/**\n * Thrown when the default Slack DM identity ladder declines to run a message.\n * `reason` is a stable machine-readable discriminator (used e.g. to dedupe\n * decline replies); `userFacingMessage` is safe to send back to the sender as\n * a polite reply; `message` stays log-only.\n */\nexport class IntegrationIdentityDeclinedError extends Error {\n readonly reason: IntegrationIdentityDeclineReason;\n readonly userFacingMessage: string;\n\n constructor(\n reason: IntegrationIdentityDeclineReason,\n message: string,\n userFacingMessage: string,\n ) {\n super(message);\n this.name = \"IntegrationIdentityDeclinedError\";\n this.reason = reason;\n this.userFacingMessage = userFacingMessage;\n }\n}\n\nfunction serviceOwner(platform: string): string {\n return `integration@${platform}`;\n}\n\nfunction normalizedEmail(value: unknown): string | null {\n if (typeof value !== \"string\") return null;\n const email = value.trim().toLowerCase();\n if (!email || email.length > 320 || !email.includes(\"@\")) return null;\n return email;\n}\n\nasync function resolveSlackInstallation(incoming: IncomingMessage) {\n const teamId =\n typeof incoming.platformContext.teamId === \"string\"\n ? incoming.platformContext.teamId\n : incoming.tenantId;\n const enterpriseId =\n typeof incoming.platformContext.enterpriseId === \"string\"\n ? incoming.platformContext.enterpriseId\n : undefined;\n const apiAppId =\n typeof incoming.platformContext.apiAppId === \"string\"\n ? incoming.platformContext.apiAppId\n : undefined;\n if (apiAppId && (teamId || enterpriseId)) {\n const byKey = await getActiveIntegrationInstallationByKey(\n \"slack\",\n slackInstallationKey({ teamId, enterpriseId, apiAppId }),\n );\n if (byKey) return byKey;\n }\n return teamId || enterpriseId\n ? getActiveIntegrationInstallationForTenant(\n \"slack\",\n teamId ?? enterpriseId!,\n )\n : null;\n}\n\nasync function isMemberOfOrg(email: string, orgId: string): Promise<boolean> {\n const { rows } = await getDbExec().execute({\n sql: `SELECT 1 FROM org_members\n WHERE org_id = ? AND LOWER(email) = ?\n LIMIT 1`,\n args: [orgId, email],\n });\n return rows.length > 0;\n}\n\n/**\n * Resolve the default integration principal.\n *\n * Slack DMs become a user principal only after the adapter has verified the\n * sender email and the email is already a member of the managed installation's\n * Agent Native organization. Hydrated full workspace members whose email is\n * missing, unverified, or not an organization member run as an anonymous\n * org-scoped service principal instead — the same visibility shared channels\n * get. Hydration failures, guests/external members, and workspaces without a\n * connected organization are declined with a user-facing message. Shared\n * channels deliberately stay service-scoped so a channel message cannot borrow\n * one participant's private permissions.\n */\nexport async function resolveDefaultIntegrationExecutionContext(\n incoming: IncomingMessage,\n): Promise<IntegrationExecutionContext> {\n const installation =\n incoming.platform === \"slack\"\n ? await resolveSlackInstallation(incoming)\n : null;\n\n if (incoming.platform !== \"slack\" || incoming.conversationType !== \"dm\") {\n return {\n ownerEmail: serviceOwner(incoming.platform),\n orgId: installation?.orgId ?? null,\n principalType: \"service\",\n ...(installation?.id ? { installationId: installation.id } : {}),\n };\n }\n\n // Hydration check first: a transient users.info failure must land on the\n // retry decline here, never on the anonymous org-scoped tier below.\n if (incoming.actorTrust?.verified !== true) {\n throw new IntegrationIdentityDeclinedError(\n \"unverified\",\n \"Slack DM sender identity could not be hydrated; declining instead of guessing a principal.\",\n \"I couldn't verify your Slack identity just now, so I can't run this request. Please try again in a moment.\",\n );\n }\n if (\n incoming.actorTrust.memberType === \"guest\" ||\n incoming.actorTrust.memberType === \"external\"\n ) {\n throw new IntegrationIdentityDeclinedError(\n \"guest\",\n \"External or guest Slack members cannot use this integration.\",\n \"This assistant is only available to members of this workspace's organization.\",\n );\n }\n if (!installation?.orgId) {\n throw new IntegrationIdentityDeclinedError(\n \"unlinked-workspace\",\n \"Slack workspace is not connected to an Agent Native organization.\",\n \"This Slack workspace isn't connected to an organization yet.\",\n );\n }\n\n const email = normalizedEmail(incoming.senderEmail);\n let isOrgMember = false;\n if (\n email &&\n incoming.senderVerified === true &&\n incoming.senderId &&\n incoming.tenantId\n ) {\n try {\n isOrgMember = await isMemberOfOrg(email, installation.orgId);\n } catch {\n // A membership-store outage is not evidence that the sender is merely\n // unlinked. Fail closed instead of widening them to org-wide access.\n throw new IntegrationIdentityDeclinedError(\n \"membership-check-failed\",\n \"Slack DM organization membership could not be verified.\",\n \"I couldn't verify your organization membership just now, so I can't run this request. Please try again in a moment.\",\n );\n }\n }\n if (isOrgMember && email && incoming.senderId && incoming.tenantId) {\n const link = await upsertVerifiedIntegrationIdentity({\n platform: incoming.platform,\n tenantId: incoming.tenantId,\n externalUserId: incoming.senderId,\n userEmail: email,\n orgId: installation.orgId,\n });\n return {\n ownerEmail: link.userEmail,\n orgId: link.orgId,\n principalType: \"user\",\n installationId: installation.id,\n };\n }\n\n // Hydrated full workspace member (member/admin/owner) whose email is\n // unverified, missing (legacy install without users:read.email), or not an\n // organization member: run with the anonymous org-scoped service principal —\n // the same visibility shared channels get. Nothing user-private is\n // accessible. One structured line keeps 100%-anonymous workspaces (legacy\n // scope missing) visible in logs.\n console.warn(\n `[integrations] anonymous org-scoped principal used: platform=${incoming.platform} teamId=${incoming.tenantId ?? \"unknown\"} emailPresent=${Boolean(email)} memberType=${incoming.actorTrust.memberType}`,\n );\n return {\n ownerEmail: serviceOwner(incoming.platform),\n orgId: installation.orgId,\n principalType: \"service\",\n installationId: installation.id,\n anonymousMember: true,\n };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pending-tasks-retry-job.d.ts","sourceRoot":"","sources":["../../src/integrations/pending-tasks-retry-job.ts"],"names":[],"mappings":"AAuDA;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"pending-tasks-retry-job.d.ts","sourceRoot":"","sources":["../../src/integrations/pending-tasks-retry-job.ts"],"names":[],"mappings":"AAuDA;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAiGf;AA8ED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE;IAClD,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GAAG,IAAI,CA0BP;AAED,2BAA2B;AAC3B,wBAAgB,wBAAwB,IAAI,IAAI,CAU/C"}
|
|
@@ -100,7 +100,8 @@ export async function retryStuckPendingTasks(webhookBaseUrl) {
|
|
|
100
100
|
SET status = 'failed',
|
|
101
101
|
updated_at = ?,
|
|
102
102
|
error_message = COALESCE(error_message, ?),
|
|
103
|
-
payload = '{}'
|
|
103
|
+
payload = '{}',
|
|
104
|
+
external_event_key = NULL
|
|
104
105
|
WHERE id = ?
|
|
105
106
|
AND status = ?
|
|
106
107
|
`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pending-tasks-retry-job.js","sourceRoot":"","sources":["../../src/integrations/pending-tasks-retry-job.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDbExec } from \"../db/client.js\";\nimport { withConfiguredAppBasePath } from \"../server/app-base-path.js\";\nimport { FRAMEWORK_ROUTE_PREFIX } from \"../server/core-routes-plugin.js\";\nimport { signInternalToken } from \"./internal-token.js\";\nimport { MAX_PENDING_TASK_ATTEMPTS } from \"./pending-tasks-store.js\";\n\n/**\n * Retries stuck integration webhook tasks.\n *\n * The integration webhook flow enqueues work into `integration_pending_tasks`\n * (see `pending-tasks-store.ts`) and then fires a self-webhook to the\n * `/_agent-native/integrations/process-task` endpoint to drain the queue.\n * If that fire-and-forget dispatch fails (e.g. transient network blip), the\n * row stays in `pending` forever. Likewise, if the processor is killed mid-\n * processing (function timeout, container shutdown), a row can remain in\n * `processing` forever.\n *\n * This job runs every 60s and re-fires the processor endpoint for tasks that\n * look stuck:\n * - status='pending' AND created_at older than 90s (initial dispatch lost)\n * - status='processing' AND updated_at older than the host-specific\n * function budget (75s on serverless, 5min elsewhere)\n *\n * Retries are capped at MAX_ATTEMPTS attempts; after that the row is marked\n * `failed` permanently so it stops being retried.\n *\n * If the `integration_pending_tasks` table does not yet exist (e.g. older\n * deploy that hasn't run the new webhook flow), this job no-ops silently\n * rather than spamming logs.\n */\n\nconst RETRY_INTERVAL_MS = 60_000;\n/** Tasks pending longer than this are considered stuck on initial dispatch */\nconst PENDING_STUCK_AFTER_MS = 90_000;\n/** Tasks \"processing\" longer than this are considered killed mid-flight. */\nconst DEFAULT_PROCESSING_STUCK_AFTER_MS = 5 * 60 * 1000;\nconst SERVERLESS_PROCESSING_STUCK_AFTER_MS = 75_000;\n/** After this many attempts we give up and mark the task failed */\nconst PROCESSOR_PATH = `${FRAMEWORK_ROUTE_PREFIX}/integrations/process-task`;\n\nlet retryInterval: ReturnType<typeof setInterval> | null = null;\nlet initialTimer: ReturnType<typeof setTimeout> | null = null;\nlet activeWebhookBaseUrl: string | undefined;\n/**\n * Whether the table exists. Cached after first probe so we don't log every\n * minute when the queue isn't in use yet on a given deployment.\n */\nlet tableExists: boolean | null = null;\n\ninterface StuckTaskRow {\n id: string;\n status: string;\n attempts: number;\n}\n\n/**\n * One pass: find stuck tasks and re-fire the processor for each.\n * Exported for tests and for manual triggers.\n */\nexport async function retryStuckPendingTasks(\n webhookBaseUrl?: string,\n): Promise<void> {\n const baseUrl = webhookBaseUrl ?? activeWebhookBaseUrl;\n const client = getDbExec();\n const now = Date.now();\n const pendingCutoff = now - PENDING_STUCK_AFTER_MS;\n const processingCutoff = now - getProcessingStuckAfterMs();\n\n let stuckRows: StuckTaskRow[];\n try {\n const { rows } = await client.execute({\n sql: `\n SELECT id, status, attempts\n FROM integration_pending_tasks\n WHERE (status = 'pending' AND created_at <= ? AND updated_at <= ?)\n OR (status = 'processing' AND updated_at <= ?)\n `,\n // `updated_at` is initialized to `created_at` on insert, so a genuinely\n // stuck pending row still matches on the first sweep. The retry path\n // below touches `updated_at`, which (with this predicate) keeps the row\n // from being re-selected — and re-firing the processor — on every tick.\n args: [pendingCutoff, pendingCutoff, processingCutoff],\n });\n stuckRows = rows.map((r) => ({\n id: r.id as string,\n status: r.status as string,\n attempts: Number(r.attempts ?? 0),\n }));\n tableExists = true;\n } catch (err) {\n // Most common case: the table hasn't been created yet because no inbound\n // integration webhook has been processed on this deployment. Silently\n // no-op until the table appears.\n if (tableExists !== false) {\n tableExists = false;\n if (process.env.DEBUG) {\n console.log(\n \"[integrations] pending-tasks retry job: table not present yet, skipping\",\n );\n }\n }\n return;\n }\n\n if (stuckRows.length === 0) return;\n\n for (const row of stuckRows) {\n try {\n // Cap retries — mark failed and move on so the row stops bouncing\n // between pending and processing forever.\n if (row.attempts >= MAX_PENDING_TASK_ATTEMPTS) {\n await client.execute({\n sql: `\n UPDATE integration_pending_tasks\n SET status = 'failed',\n updated_at = ?,\n error_message = COALESCE(error_message, ?),\n payload = '{}'\n WHERE id = ?\n AND status = ?\n `,\n args: [\n Date.now(),\n `Retry job: exceeded ${MAX_PENDING_TASK_ATTEMPTS} attempts`,\n row.id,\n row.status,\n ],\n });\n console.warn(\n `[integrations] Pending task ${row.id} exceeded ${MAX_PENDING_TASK_ATTEMPTS} attempts — marking failed`,\n );\n continue;\n }\n\n // Reset stuck `processing` rows back to `pending` so the processor's\n // atomic claim (which only matches pending) can re-acquire it.\n // Without this, processing rows stay stuck forever.\n // For pending rows, just touch updated_at to avoid re-firing every tick.\n const newStatus = row.status === \"processing\" ? \"pending\" : row.status;\n await client.execute({\n sql: `\n UPDATE integration_pending_tasks\n SET status = ?, updated_at = ?\n WHERE id = ?\n AND status = ?\n `,\n args: [newStatus, Date.now(), row.id, row.status],\n });\n\n await refireProcessor(row.id, baseUrl);\n } catch (err) {\n console.error(\n `[integrations] Failed to retry pending task ${row.id}:`,\n err,\n );\n }\n }\n}\n\nfunction getProcessingStuckAfterMs(): number {\n if (\n process.env.NETLIFY ||\n process.env.AWS_LAMBDA_FUNCTION_NAME ||\n process.env.VERCEL ||\n \"__cf_env\" in globalThis\n ) {\n return SERVERLESS_PROCESSING_STUCK_AFTER_MS;\n }\n return DEFAULT_PROCESSING_STUCK_AFTER_MS;\n}\n\n/**\n * Fire-and-forget POST to the processor endpoint for a single task id.\n * Mirrors the original dispatch from the webhook handler, including the\n * short-lived HMAC bearer token bound to this taskId.\n */\nasync function refireProcessor(\n taskId: string,\n webhookBaseUrl: string | undefined,\n): Promise<void> {\n const baseUrl =\n webhookBaseUrl ||\n process.env.WEBHOOK_BASE_URL ||\n process.env.APP_URL ||\n process.env.URL ||\n process.env.DEPLOY_URL ||\n `http://localhost:${process.env.PORT || 3000}`;\n\n const url = `${withConfiguredAppBasePath(baseUrl)}${PROCESSOR_PATH}`;\n\n // Sign with HMAC if A2A_SECRET is configured. In production we MUST sign —\n // an unsigned dispatch in production lets attackers re-trigger any queued\n // task with a guessable id (C3 in the webhook security audit). In dev we\n // fall back to unsigned so contributors can iterate without configuring\n // A2A_SECRET locally.\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n try {\n headers[\"Authorization\"] = `Bearer ${signInternalToken(taskId)}`;\n } catch (err) {\n if (process.env.NODE_ENV === \"production\") {\n console.error(\n `[integrations] Refusing to dispatch task ${taskId} — A2A_SECRET not configured. ` +\n \"Set A2A_SECRET to enable signed retry dispatches.\",\n );\n return;\n }\n // Dev: proceed unsigned. Log the underlying error path so a malformed\n // secret (different from \"not set\") doesn't fail silently (L5 in the audit).\n if (err instanceof Error && !/A2A_SECRET/i.test(err.message)) {\n console.error(\n `[integrations] signInternalToken failed unexpectedly for ${taskId}:`,\n err,\n );\n }\n }\n\n // Don't await the body — we just want the request to leave the box.\n // A short timeout avoids tying up the retry loop on a hung processor.\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), 5_000);\n\n try {\n await fetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ taskId }),\n signal: controller.signal,\n });\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Start the periodic retry loop. Safe to call multiple times — second call\n * is a no-op.\n */\nexport function startPendingTasksRetryJob(options?: {\n webhookBaseUrl?: string;\n}): void {\n if (retryInterval) return;\n activeWebhookBaseUrl = options?.webhookBaseUrl;\n\n // Stagger the first run a bit so we don't hammer the DB immediately on boot.\n initialTimer = setTimeout(() => {\n void retryStuckPendingTasks().catch((err) => {\n console.error(\"[integrations] Pending-tasks retry job error:\", err);\n });\n }, 10_000);\n unrefTimer(initialTimer);\n\n retryInterval = setInterval(() => {\n void retryStuckPendingTasks().catch((err) => {\n console.error(\"[integrations] Pending-tasks retry job error:\", err);\n });\n }, RETRY_INTERVAL_MS);\n unrefTimer(retryInterval);\n\n if (process.env.DEBUG) {\n console.log(\n `[integrations] Pending-tasks retry job started (every ${\n RETRY_INTERVAL_MS / 1000\n }s)`,\n );\n }\n}\n\n/** Stop the retry loop. */\nexport function stopPendingTasksRetryJob(): void {\n if (initialTimer) {\n clearTimeout(initialTimer);\n initialTimer = null;\n }\n if (retryInterval) {\n clearInterval(retryInterval);\n retryInterval = null;\n }\n activeWebhookBaseUrl = undefined;\n}\n\nfunction unrefTimer(timer: ReturnType<typeof setInterval>): void {\n (timer as unknown as { unref?: () => void }).unref?.();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"pending-tasks-retry-job.js","sourceRoot":"","sources":["../../src/integrations/pending-tasks-retry-job.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDbExec } from \"../db/client.js\";\nimport { withConfiguredAppBasePath } from \"../server/app-base-path.js\";\nimport { FRAMEWORK_ROUTE_PREFIX } from \"../server/core-routes-plugin.js\";\nimport { signInternalToken } from \"./internal-token.js\";\nimport { MAX_PENDING_TASK_ATTEMPTS } from \"./pending-tasks-store.js\";\n\n/**\n * Retries stuck integration webhook tasks.\n *\n * The integration webhook flow enqueues work into `integration_pending_tasks`\n * (see `pending-tasks-store.ts`) and then fires a self-webhook to the\n * `/_agent-native/integrations/process-task` endpoint to drain the queue.\n * If that fire-and-forget dispatch fails (e.g. transient network blip), the\n * row stays in `pending` forever. Likewise, if the processor is killed mid-\n * processing (function timeout, container shutdown), a row can remain in\n * `processing` forever.\n *\n * This job runs every 60s and re-fires the processor endpoint for tasks that\n * look stuck:\n * - status='pending' AND created_at older than 90s (initial dispatch lost)\n * - status='processing' AND updated_at older than the host-specific\n * function budget (75s on serverless, 5min elsewhere)\n *\n * Retries are capped at MAX_ATTEMPTS attempts; after that the row is marked\n * `failed` permanently so it stops being retried.\n *\n * If the `integration_pending_tasks` table does not yet exist (e.g. older\n * deploy that hasn't run the new webhook flow), this job no-ops silently\n * rather than spamming logs.\n */\n\nconst RETRY_INTERVAL_MS = 60_000;\n/** Tasks pending longer than this are considered stuck on initial dispatch */\nconst PENDING_STUCK_AFTER_MS = 90_000;\n/** Tasks \"processing\" longer than this are considered killed mid-flight. */\nconst DEFAULT_PROCESSING_STUCK_AFTER_MS = 5 * 60 * 1000;\nconst SERVERLESS_PROCESSING_STUCK_AFTER_MS = 75_000;\n/** After this many attempts we give up and mark the task failed */\nconst PROCESSOR_PATH = `${FRAMEWORK_ROUTE_PREFIX}/integrations/process-task`;\n\nlet retryInterval: ReturnType<typeof setInterval> | null = null;\nlet initialTimer: ReturnType<typeof setTimeout> | null = null;\nlet activeWebhookBaseUrl: string | undefined;\n/**\n * Whether the table exists. Cached after first probe so we don't log every\n * minute when the queue isn't in use yet on a given deployment.\n */\nlet tableExists: boolean | null = null;\n\ninterface StuckTaskRow {\n id: string;\n status: string;\n attempts: number;\n}\n\n/**\n * One pass: find stuck tasks and re-fire the processor for each.\n * Exported for tests and for manual triggers.\n */\nexport async function retryStuckPendingTasks(\n webhookBaseUrl?: string,\n): Promise<void> {\n const baseUrl = webhookBaseUrl ?? activeWebhookBaseUrl;\n const client = getDbExec();\n const now = Date.now();\n const pendingCutoff = now - PENDING_STUCK_AFTER_MS;\n const processingCutoff = now - getProcessingStuckAfterMs();\n\n let stuckRows: StuckTaskRow[];\n try {\n const { rows } = await client.execute({\n sql: `\n SELECT id, status, attempts\n FROM integration_pending_tasks\n WHERE (status = 'pending' AND created_at <= ? AND updated_at <= ?)\n OR (status = 'processing' AND updated_at <= ?)\n `,\n // `updated_at` is initialized to `created_at` on insert, so a genuinely\n // stuck pending row still matches on the first sweep. The retry path\n // below touches `updated_at`, which (with this predicate) keeps the row\n // from being re-selected — and re-firing the processor — on every tick.\n args: [pendingCutoff, pendingCutoff, processingCutoff],\n });\n stuckRows = rows.map((r) => ({\n id: r.id as string,\n status: r.status as string,\n attempts: Number(r.attempts ?? 0),\n }));\n tableExists = true;\n } catch (err) {\n // Most common case: the table hasn't been created yet because no inbound\n // integration webhook has been processed on this deployment. Silently\n // no-op until the table appears.\n if (tableExists !== false) {\n tableExists = false;\n if (process.env.DEBUG) {\n console.log(\n \"[integrations] pending-tasks retry job: table not present yet, skipping\",\n );\n }\n }\n return;\n }\n\n if (stuckRows.length === 0) return;\n\n for (const row of stuckRows) {\n try {\n // Cap retries — mark failed and move on so the row stops bouncing\n // between pending and processing forever.\n if (row.attempts >= MAX_PENDING_TASK_ATTEMPTS) {\n await client.execute({\n sql: `\n UPDATE integration_pending_tasks\n SET status = 'failed',\n updated_at = ?,\n error_message = COALESCE(error_message, ?),\n payload = '{}',\n external_event_key = NULL\n WHERE id = ?\n AND status = ?\n `,\n args: [\n Date.now(),\n `Retry job: exceeded ${MAX_PENDING_TASK_ATTEMPTS} attempts`,\n row.id,\n row.status,\n ],\n });\n console.warn(\n `[integrations] Pending task ${row.id} exceeded ${MAX_PENDING_TASK_ATTEMPTS} attempts — marking failed`,\n );\n continue;\n }\n\n // Reset stuck `processing` rows back to `pending` so the processor's\n // atomic claim (which only matches pending) can re-acquire it.\n // Without this, processing rows stay stuck forever.\n // For pending rows, just touch updated_at to avoid re-firing every tick.\n const newStatus = row.status === \"processing\" ? \"pending\" : row.status;\n await client.execute({\n sql: `\n UPDATE integration_pending_tasks\n SET status = ?, updated_at = ?\n WHERE id = ?\n AND status = ?\n `,\n args: [newStatus, Date.now(), row.id, row.status],\n });\n\n await refireProcessor(row.id, baseUrl);\n } catch (err) {\n console.error(\n `[integrations] Failed to retry pending task ${row.id}:`,\n err,\n );\n }\n }\n}\n\nfunction getProcessingStuckAfterMs(): number {\n if (\n process.env.NETLIFY ||\n process.env.AWS_LAMBDA_FUNCTION_NAME ||\n process.env.VERCEL ||\n \"__cf_env\" in globalThis\n ) {\n return SERVERLESS_PROCESSING_STUCK_AFTER_MS;\n }\n return DEFAULT_PROCESSING_STUCK_AFTER_MS;\n}\n\n/**\n * Fire-and-forget POST to the processor endpoint for a single task id.\n * Mirrors the original dispatch from the webhook handler, including the\n * short-lived HMAC bearer token bound to this taskId.\n */\nasync function refireProcessor(\n taskId: string,\n webhookBaseUrl: string | undefined,\n): Promise<void> {\n const baseUrl =\n webhookBaseUrl ||\n process.env.WEBHOOK_BASE_URL ||\n process.env.APP_URL ||\n process.env.URL ||\n process.env.DEPLOY_URL ||\n `http://localhost:${process.env.PORT || 3000}`;\n\n const url = `${withConfiguredAppBasePath(baseUrl)}${PROCESSOR_PATH}`;\n\n // Sign with HMAC if A2A_SECRET is configured. In production we MUST sign —\n // an unsigned dispatch in production lets attackers re-trigger any queued\n // task with a guessable id (C3 in the webhook security audit). In dev we\n // fall back to unsigned so contributors can iterate without configuring\n // A2A_SECRET locally.\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n try {\n headers[\"Authorization\"] = `Bearer ${signInternalToken(taskId)}`;\n } catch (err) {\n if (process.env.NODE_ENV === \"production\") {\n console.error(\n `[integrations] Refusing to dispatch task ${taskId} — A2A_SECRET not configured. ` +\n \"Set A2A_SECRET to enable signed retry dispatches.\",\n );\n return;\n }\n // Dev: proceed unsigned. Log the underlying error path so a malformed\n // secret (different from \"not set\") doesn't fail silently (L5 in the audit).\n if (err instanceof Error && !/A2A_SECRET/i.test(err.message)) {\n console.error(\n `[integrations] signInternalToken failed unexpectedly for ${taskId}:`,\n err,\n );\n }\n }\n\n // Don't await the body — we just want the request to leave the box.\n // A short timeout avoids tying up the retry loop on a hung processor.\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), 5_000);\n\n try {\n await fetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ taskId }),\n signal: controller.signal,\n });\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Start the periodic retry loop. Safe to call multiple times — second call\n * is a no-op.\n */\nexport function startPendingTasksRetryJob(options?: {\n webhookBaseUrl?: string;\n}): void {\n if (retryInterval) return;\n activeWebhookBaseUrl = options?.webhookBaseUrl;\n\n // Stagger the first run a bit so we don't hammer the DB immediately on boot.\n initialTimer = setTimeout(() => {\n void retryStuckPendingTasks().catch((err) => {\n console.error(\"[integrations] Pending-tasks retry job error:\", err);\n });\n }, 10_000);\n unrefTimer(initialTimer);\n\n retryInterval = setInterval(() => {\n void retryStuckPendingTasks().catch((err) => {\n console.error(\"[integrations] Pending-tasks retry job error:\", err);\n });\n }, RETRY_INTERVAL_MS);\n unrefTimer(retryInterval);\n\n if (process.env.DEBUG) {\n console.log(\n `[integrations] Pending-tasks retry job started (every ${\n RETRY_INTERVAL_MS / 1000\n }s)`,\n );\n }\n}\n\n/** Stop the retry loop. */\nexport function stopPendingTasksRetryJob(): void {\n if (initialTimer) {\n clearTimeout(initialTimer);\n initialTimer = null;\n }\n if (retryInterval) {\n clearInterval(retryInterval);\n retryInterval = null;\n }\n activeWebhookBaseUrl = undefined;\n}\n\nfunction unrefTimer(timer: ReturnType<typeof setInterval>): void {\n (timer as unknown as { unref?: () => void }).unref?.();\n}\n"]}
|
|
@@ -279,7 +279,7 @@ export async function markTaskFailed(id, errorMessage) {
|
|
|
279
279
|
const now = Date.now();
|
|
280
280
|
await client.execute({
|
|
281
281
|
sql: `UPDATE integration_pending_tasks
|
|
282
|
-
SET status = ?, updated_at = ?, error_message = ?, payload =
|
|
282
|
+
SET status = ?, updated_at = ?, error_message = ?, payload = ?, external_event_key = NULL
|
|
283
283
|
WHERE id = ?`,
|
|
284
284
|
args: ["failed", now, errorMessage.slice(0, 2000), "{}", id],
|
|
285
285
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pending-tasks-store.js","sourceRoot":"","sources":["../../src/integrations/pending-tasks-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SQL-backed pending task queue for integration webhooks.\n *\n * Why this exists: serverless platforms (Netlify Lambda, Vercel, Cloudflare\n * Workers) freeze the function execution as soon as the HTTP response is\n * returned. Fire-and-forget background `Promise`s get killed mid-flight,\n * meaning agent loops triggered from a Slack/Telegram webhook never finish.\n *\n * Solution: persist the inbound message to SQL inside the webhook handler,\n * then dispatch a fresh HTTP POST to a separate processor endpoint. Each\n * invocation gets its own fresh function timeout budget.\n */\nimport { getDbExec, isPostgres, intType } from \"../db/client.js\";\nimport {\n ensureTableExists,\n ensureColumnExists,\n ensureIndexExists,\n} from \"../db/ddl-guard.js\";\n\nlet _initPromise: Promise<void> | undefined;\nexport const MAX_PENDING_TASK_ATTEMPTS = 3;\n\nasync function ensureTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n const createSql = `CREATE TABLE IF NOT EXISTS integration_pending_tasks (\n id TEXT PRIMARY KEY,\n platform TEXT NOT NULL,\n external_thread_id TEXT NOT NULL,\n payload TEXT NOT NULL,\n owner_email TEXT NOT NULL,\n org_id TEXT,\n status TEXT NOT NULL,\n attempts ${intType()} NOT NULL DEFAULT 0,\n error_message TEXT,\n created_at ${intType()} NOT NULL,\n updated_at ${intType()} NOT NULL,\n completed_at ${intType()}\n)`;\n\n if (isPostgres()) {\n // PG guard: probe via information_schema, only issue DDL if missing, bounded lock_timeout\n await ensureTableExists(\"integration_pending_tasks\", createSql);\n await ensureColumnExists(\n \"integration_pending_tasks\",\n \"external_event_key\",\n `ALTER TABLE integration_pending_tasks ADD COLUMN IF NOT EXISTS external_event_key TEXT`,\n );\n await ensureIndexExists(\n \"idx_pending_tasks_status_created\",\n `CREATE INDEX IF NOT EXISTS idx_pending_tasks_status_created ON integration_pending_tasks(status, created_at)`,\n );\n await ensureIndexExists(\n \"idx_pending_tasks_event_key\",\n `CREATE UNIQUE INDEX IF NOT EXISTS idx_pending_tasks_event_key ON integration_pending_tasks(platform, external_event_key)`,\n );\n return;\n }\n\n await client.execute(createSql);\n await client.execute(\n `CREATE INDEX IF NOT EXISTS idx_pending_tasks_status_created ON integration_pending_tasks(status, created_at)`,\n );\n // Additive migration: add a stable per-event dedup key so duplicate\n // webhook deliveries from the same platform get rejected at the SQL\n // layer instead of via an in-memory Map (which doesn't survive\n // serverless cold starts — H3 in the webhook security audit). The\n // unique index ensures a duplicate INSERT raises an error we can\n // catch as \"already-enqueued\".\n await ensureExternalEventKey(client);\n await client.execute(\n `CREATE UNIQUE INDEX IF NOT EXISTS idx_pending_tasks_event_key ON integration_pending_tasks(platform, external_event_key)`,\n );\n })().catch((err) => {\n // Retry init on the next call after a failed startup.\n _initPromise = undefined;\n throw err;\n });\n }\n return _initPromise;\n}\n\nasync function ensureExternalEventKey(\n client: ReturnType<typeof getDbExec>,\n): Promise<void> {\n if (isPostgres()) {\n await client.execute(\n `ALTER TABLE integration_pending_tasks ADD COLUMN IF NOT EXISTS external_event_key TEXT`,\n );\n return;\n }\n // SQLite doesn't support `ADD COLUMN IF NOT EXISTS` until 3.35; swallow\n // the duplicate-column error so reruns are idempotent.\n try {\n await client.execute(\n `ALTER TABLE integration_pending_tasks ADD COLUMN external_event_key TEXT`,\n );\n } catch (err: any) {\n if (\n !String(err?.message ?? err)\n .toLowerCase()\n .includes(\"duplicate\")\n ) {\n throw err;\n }\n }\n}\n\n/** Status values for an integration pending task. */\nexport type PendingTaskStatus =\n | \"pending\"\n | \"processing\"\n | \"completed\"\n | \"failed\";\n\nexport interface PendingTask {\n id: string;\n platform: string;\n externalThreadId: string;\n payload: string;\n ownerEmail: string;\n orgId: string | null;\n status: PendingTaskStatus;\n attempts: number;\n errorMessage: string | null;\n createdAt: number;\n updatedAt: number;\n completedAt: number | null;\n}\n\n/**\n * Whether a provider thread currently has queued or executing work.\n *\n * Messaging adapters use this to accept unmentioned replies only while an\n * agent task is active. This prevents broad message subscriptions from turning\n * every channel message into an agent invocation.\n */\nexport async function hasActivePendingTask(\n platform: string,\n externalThreadId: string,\n): Promise<boolean> {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT 1 AS active\n FROM integration_pending_tasks\n WHERE platform = ?\n AND external_thread_id = ?\n AND status IN ('pending', 'processing')\n LIMIT 1`,\n args: [platform, externalThreadId],\n });\n return rows.length > 0;\n}\n\nfunction rowToTask(row: Record<string, unknown>): PendingTask {\n return {\n id: row.id as string,\n platform: row.platform as string,\n externalThreadId: row.external_thread_id as string,\n payload: row.payload as string,\n ownerEmail: row.owner_email as string,\n orgId: (row.org_id as string | null) ?? null,\n status: row.status as PendingTaskStatus,\n attempts: Number(row.attempts ?? 0),\n errorMessage: (row.error_message as string | null) ?? null,\n createdAt: Number(row.created_at ?? 0),\n updatedAt: Number(row.updated_at ?? 0),\n completedAt:\n row.completed_at == null ? null : Number(row.completed_at as number),\n };\n}\n\n/**\n * Insert a new pending task. Returns the generated task id.\n *\n * If `externalEventKey` is supplied, the unique index on\n * `(platform, external_event_key)` will reject duplicates — callers should\n * catch the resulting constraint-violation error and treat it as\n * \"already enqueued\" instead of a hard failure (H3 in the webhook security\n * audit). This is the SQL-backed replacement for the in-memory dedup map.\n */\nexport async function insertPendingTask(input: {\n id: string;\n platform: string;\n externalThreadId: string;\n payload: string;\n ownerEmail: string;\n orgId?: string | null;\n externalEventKey?: string | null;\n}): Promise<void> {\n await ensureTable();\n const client = getDbExec();\n const now = Date.now();\n await client.execute({\n sql: `INSERT INTO integration_pending_tasks\n (id, platform, external_thread_id, payload, owner_email, org_id, status, attempts, created_at, updated_at, external_event_key)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n args: [\n input.id,\n input.platform,\n input.externalThreadId,\n input.payload,\n input.ownerEmail,\n input.orgId ?? null,\n \"pending\",\n 0,\n now,\n now,\n input.externalEventKey ?? null,\n ],\n });\n}\n\n/**\n * Returns whether a duplicate-event error from `insertPendingTask` looks\n * like a unique-constraint violation on `(platform, external_event_key)`.\n *\n * Postgres surfaces these as `error.code === \"23505\"`, while SQLite uses\n * a substring match on the error text. Used by the webhook handler to\n * distinguish \"already enqueued\" (silently OK) from genuine insert failures.\n */\nexport function isDuplicateEventError(err: unknown): boolean {\n const e = err as { code?: string; message?: string } | null;\n if (!e) return false;\n if (e.code === \"23505\") return true; // Postgres unique-violation\n const msg = String(e.message ?? \"\").toLowerCase();\n return (\n msg.includes(\"unique\") ||\n msg.includes(\"duplicate entry\") ||\n msg.includes(\"duplicate key\")\n );\n}\n\n/** Fetch a pending task by id. */\nexport async function getPendingTask(id: string): Promise<PendingTask | null> {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT id, platform, external_thread_id, payload, owner_email, org_id, status, attempts, error_message, created_at, updated_at, completed_at\n FROM integration_pending_tasks WHERE id = ? LIMIT 1`,\n args: [id],\n });\n if (rows.length === 0) return null;\n return rowToTask(rows[0] as Record<string, unknown>);\n}\n\n/**\n * Atomically claim a task: transition pending → processing and increment\n * attempts. Returns the updated task if the transition succeeded, otherwise\n * null (e.g. the task was already claimed by a concurrent worker).\n */\nexport async function claimPendingTask(\n id: string,\n): Promise<PendingTask | null> {\n await ensureTable();\n const client = getDbExec();\n const now = Date.now();\n\n // Conditional update: only flip if currently pending. Failed tasks are\n // terminal unless an explicit retry path resets them to pending first.\n const result = await client.execute({\n sql: isPostgres()\n ? `UPDATE integration_pending_tasks\n SET status = ?, attempts = attempts + 1, updated_at = ?\n WHERE id = ? AND status = 'pending'\n AND NOT EXISTS (\n SELECT 1 FROM integration_pending_tasks active\n WHERE active.platform = integration_pending_tasks.platform\n AND active.external_thread_id = integration_pending_tasks.external_thread_id\n AND active.status = 'processing'\n AND active.id <> integration_pending_tasks.id\n )\n RETURNING id, platform, external_thread_id, payload, owner_email, org_id, status, attempts, error_message, created_at, updated_at, completed_at`\n : `UPDATE integration_pending_tasks\n SET status = ?, attempts = attempts + 1, updated_at = ?\n WHERE id = ? AND status = 'pending'\n AND NOT EXISTS (\n SELECT 1 FROM integration_pending_tasks active\n WHERE active.platform = integration_pending_tasks.platform\n AND active.external_thread_id = integration_pending_tasks.external_thread_id\n AND active.status = 'processing'\n AND active.id <> integration_pending_tasks.id\n )`,\n args: [\"processing\", now, id],\n });\n const rows = result.rows ?? [];\n\n if (isPostgres()) {\n if (rows.length === 0) return null;\n return rowToTask(rows[0] as Record<string, unknown>);\n }\n\n // SQLite: no RETURNING, so re-read after the update. Confirm we actually\n // moved it into 'processing' (vs. lost the race).\n const affected =\n (result as { rowsAffected?: number; rowCount?: number }).rowsAffected ??\n (result as { rowsAffected?: number; rowCount?: number }).rowCount;\n if (affected === 0) return null;\n const fetched = await getPendingTask(id);\n if (!fetched || fetched.status !== \"processing\") return null;\n return fetched;\n}\n\n/** Next queued turn for a provider thread after its current task completes. */\nexport async function getNextPendingTaskIdForThread(\n platform: string,\n externalThreadId: string,\n): Promise<string | null> {\n await ensureTable();\n const { rows } = await getDbExec().execute({\n sql: `SELECT id FROM integration_pending_tasks\n WHERE platform = ? AND external_thread_id = ? AND status = 'pending'\n ORDER BY created_at ASC LIMIT 1`,\n args: [platform, externalThreadId],\n });\n return rows[0]?.id ? String(rows[0].id) : null;\n}\n\n/** Mark a task as completed. */\nexport async function markTaskCompleted(id: string): Promise<void> {\n await ensureTable();\n const client = getDbExec();\n const now = Date.now();\n await client.execute({\n sql: `UPDATE integration_pending_tasks\n SET status = ?, updated_at = ?, completed_at = ?, payload = ?\n WHERE id = ?`,\n // The payload can contain short-lived provider credentials such as a\n // Discord interaction token. Once terminal, no retry needs the inbound\n // body, so erase it instead of retaining secrets or user text indefinitely.\n args: [\"completed\", now, now, \"{}\", id],\n });\n}\n\n/**\n * Return a transiently failed task to the retryable queue without erasing its\n * payload. The payload may contain the only copy of the inbound message and is\n * scrubbed only when the task reaches a permanent terminal state.\n */\nexport async function markTaskRetryable(\n id: string,\n errorMessage: string,\n): Promise<void> {\n await ensureTable();\n const client = getDbExec();\n const now = Date.now();\n await client.execute({\n sql: `UPDATE integration_pending_tasks\n SET status = ?, updated_at = ?, error_message = ?\n WHERE id = ? AND status = 'processing'`,\n args: [\"pending\", now, errorMessage.slice(0, 2000), id],\n });\n}\n\n/** Mark a task as failed and stash an error message. */\nexport async function markTaskFailed(\n id: string,\n errorMessage: string,\n): Promise<void> {\n await ensureTable();\n const client = getDbExec();\n const now = Date.now();\n await client.execute({\n sql: `UPDATE integration_pending_tasks\n SET status = ?, updated_at = ?, error_message = ?, payload = ?\n WHERE id = ?`,\n args: [\"failed\", now, errorMessage.slice(0, 2000), \"{}\", id],\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"pending-tasks-store.js","sourceRoot":"","sources":["../../src/integrations/pending-tasks-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SQL-backed pending task queue for integration webhooks.\n *\n * Why this exists: serverless platforms (Netlify Lambda, Vercel, Cloudflare\n * Workers) freeze the function execution as soon as the HTTP response is\n * returned. Fire-and-forget background `Promise`s get killed mid-flight,\n * meaning agent loops triggered from a Slack/Telegram webhook never finish.\n *\n * Solution: persist the inbound message to SQL inside the webhook handler,\n * then dispatch a fresh HTTP POST to a separate processor endpoint. Each\n * invocation gets its own fresh function timeout budget.\n */\nimport { getDbExec, isPostgres, intType } from \"../db/client.js\";\nimport {\n ensureTableExists,\n ensureColumnExists,\n ensureIndexExists,\n} from \"../db/ddl-guard.js\";\n\nlet _initPromise: Promise<void> | undefined;\nexport const MAX_PENDING_TASK_ATTEMPTS = 3;\n\nasync function ensureTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n const createSql = `CREATE TABLE IF NOT EXISTS integration_pending_tasks (\n id TEXT PRIMARY KEY,\n platform TEXT NOT NULL,\n external_thread_id TEXT NOT NULL,\n payload TEXT NOT NULL,\n owner_email TEXT NOT NULL,\n org_id TEXT,\n status TEXT NOT NULL,\n attempts ${intType()} NOT NULL DEFAULT 0,\n error_message TEXT,\n created_at ${intType()} NOT NULL,\n updated_at ${intType()} NOT NULL,\n completed_at ${intType()}\n)`;\n\n if (isPostgres()) {\n // PG guard: probe via information_schema, only issue DDL if missing, bounded lock_timeout\n await ensureTableExists(\"integration_pending_tasks\", createSql);\n await ensureColumnExists(\n \"integration_pending_tasks\",\n \"external_event_key\",\n `ALTER TABLE integration_pending_tasks ADD COLUMN IF NOT EXISTS external_event_key TEXT`,\n );\n await ensureIndexExists(\n \"idx_pending_tasks_status_created\",\n `CREATE INDEX IF NOT EXISTS idx_pending_tasks_status_created ON integration_pending_tasks(status, created_at)`,\n );\n await ensureIndexExists(\n \"idx_pending_tasks_event_key\",\n `CREATE UNIQUE INDEX IF NOT EXISTS idx_pending_tasks_event_key ON integration_pending_tasks(platform, external_event_key)`,\n );\n return;\n }\n\n await client.execute(createSql);\n await client.execute(\n `CREATE INDEX IF NOT EXISTS idx_pending_tasks_status_created ON integration_pending_tasks(status, created_at)`,\n );\n // Additive migration: add a stable per-event dedup key so duplicate\n // webhook deliveries from the same platform get rejected at the SQL\n // layer instead of via an in-memory Map (which doesn't survive\n // serverless cold starts — H3 in the webhook security audit). The\n // unique index ensures a duplicate INSERT raises an error we can\n // catch as \"already-enqueued\".\n await ensureExternalEventKey(client);\n await client.execute(\n `CREATE UNIQUE INDEX IF NOT EXISTS idx_pending_tasks_event_key ON integration_pending_tasks(platform, external_event_key)`,\n );\n })().catch((err) => {\n // Retry init on the next call after a failed startup.\n _initPromise = undefined;\n throw err;\n });\n }\n return _initPromise;\n}\n\nasync function ensureExternalEventKey(\n client: ReturnType<typeof getDbExec>,\n): Promise<void> {\n if (isPostgres()) {\n await client.execute(\n `ALTER TABLE integration_pending_tasks ADD COLUMN IF NOT EXISTS external_event_key TEXT`,\n );\n return;\n }\n // SQLite doesn't support `ADD COLUMN IF NOT EXISTS` until 3.35; swallow\n // the duplicate-column error so reruns are idempotent.\n try {\n await client.execute(\n `ALTER TABLE integration_pending_tasks ADD COLUMN external_event_key TEXT`,\n );\n } catch (err: any) {\n if (\n !String(err?.message ?? err)\n .toLowerCase()\n .includes(\"duplicate\")\n ) {\n throw err;\n }\n }\n}\n\n/** Status values for an integration pending task. */\nexport type PendingTaskStatus =\n | \"pending\"\n | \"processing\"\n | \"completed\"\n | \"failed\";\n\nexport interface PendingTask {\n id: string;\n platform: string;\n externalThreadId: string;\n payload: string;\n ownerEmail: string;\n orgId: string | null;\n status: PendingTaskStatus;\n attempts: number;\n errorMessage: string | null;\n createdAt: number;\n updatedAt: number;\n completedAt: number | null;\n}\n\n/**\n * Whether a provider thread currently has queued or executing work.\n *\n * Messaging adapters use this to accept unmentioned replies only while an\n * agent task is active. This prevents broad message subscriptions from turning\n * every channel message into an agent invocation.\n */\nexport async function hasActivePendingTask(\n platform: string,\n externalThreadId: string,\n): Promise<boolean> {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT 1 AS active\n FROM integration_pending_tasks\n WHERE platform = ?\n AND external_thread_id = ?\n AND status IN ('pending', 'processing')\n LIMIT 1`,\n args: [platform, externalThreadId],\n });\n return rows.length > 0;\n}\n\nfunction rowToTask(row: Record<string, unknown>): PendingTask {\n return {\n id: row.id as string,\n platform: row.platform as string,\n externalThreadId: row.external_thread_id as string,\n payload: row.payload as string,\n ownerEmail: row.owner_email as string,\n orgId: (row.org_id as string | null) ?? null,\n status: row.status as PendingTaskStatus,\n attempts: Number(row.attempts ?? 0),\n errorMessage: (row.error_message as string | null) ?? null,\n createdAt: Number(row.created_at ?? 0),\n updatedAt: Number(row.updated_at ?? 0),\n completedAt:\n row.completed_at == null ? null : Number(row.completed_at as number),\n };\n}\n\n/**\n * Insert a new pending task. Returns the generated task id.\n *\n * If `externalEventKey` is supplied, the unique index on\n * `(platform, external_event_key)` will reject duplicates — callers should\n * catch the resulting constraint-violation error and treat it as\n * \"already enqueued\" instead of a hard failure (H3 in the webhook security\n * audit). This is the SQL-backed replacement for the in-memory dedup map.\n */\nexport async function insertPendingTask(input: {\n id: string;\n platform: string;\n externalThreadId: string;\n payload: string;\n ownerEmail: string;\n orgId?: string | null;\n externalEventKey?: string | null;\n}): Promise<void> {\n await ensureTable();\n const client = getDbExec();\n const now = Date.now();\n await client.execute({\n sql: `INSERT INTO integration_pending_tasks\n (id, platform, external_thread_id, payload, owner_email, org_id, status, attempts, created_at, updated_at, external_event_key)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n args: [\n input.id,\n input.platform,\n input.externalThreadId,\n input.payload,\n input.ownerEmail,\n input.orgId ?? null,\n \"pending\",\n 0,\n now,\n now,\n input.externalEventKey ?? null,\n ],\n });\n}\n\n/**\n * Returns whether a duplicate-event error from `insertPendingTask` looks\n * like a unique-constraint violation on `(platform, external_event_key)`.\n *\n * Postgres surfaces these as `error.code === \"23505\"`, while SQLite uses\n * a substring match on the error text. Used by the webhook handler to\n * distinguish \"already enqueued\" (silently OK) from genuine insert failures.\n */\nexport function isDuplicateEventError(err: unknown): boolean {\n const e = err as { code?: string; message?: string } | null;\n if (!e) return false;\n if (e.code === \"23505\") return true; // Postgres unique-violation\n const msg = String(e.message ?? \"\").toLowerCase();\n return (\n msg.includes(\"unique\") ||\n msg.includes(\"duplicate entry\") ||\n msg.includes(\"duplicate key\")\n );\n}\n\n/** Fetch a pending task by id. */\nexport async function getPendingTask(id: string): Promise<PendingTask | null> {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT id, platform, external_thread_id, payload, owner_email, org_id, status, attempts, error_message, created_at, updated_at, completed_at\n FROM integration_pending_tasks WHERE id = ? LIMIT 1`,\n args: [id],\n });\n if (rows.length === 0) return null;\n return rowToTask(rows[0] as Record<string, unknown>);\n}\n\n/**\n * Atomically claim a task: transition pending → processing and increment\n * attempts. Returns the updated task if the transition succeeded, otherwise\n * null (e.g. the task was already claimed by a concurrent worker).\n */\nexport async function claimPendingTask(\n id: string,\n): Promise<PendingTask | null> {\n await ensureTable();\n const client = getDbExec();\n const now = Date.now();\n\n // Conditional update: only flip if currently pending. Failed tasks are\n // terminal unless an explicit retry path resets them to pending first.\n const result = await client.execute({\n sql: isPostgres()\n ? `UPDATE integration_pending_tasks\n SET status = ?, attempts = attempts + 1, updated_at = ?\n WHERE id = ? AND status = 'pending'\n AND NOT EXISTS (\n SELECT 1 FROM integration_pending_tasks active\n WHERE active.platform = integration_pending_tasks.platform\n AND active.external_thread_id = integration_pending_tasks.external_thread_id\n AND active.status = 'processing'\n AND active.id <> integration_pending_tasks.id\n )\n RETURNING id, platform, external_thread_id, payload, owner_email, org_id, status, attempts, error_message, created_at, updated_at, completed_at`\n : `UPDATE integration_pending_tasks\n SET status = ?, attempts = attempts + 1, updated_at = ?\n WHERE id = ? AND status = 'pending'\n AND NOT EXISTS (\n SELECT 1 FROM integration_pending_tasks active\n WHERE active.platform = integration_pending_tasks.platform\n AND active.external_thread_id = integration_pending_tasks.external_thread_id\n AND active.status = 'processing'\n AND active.id <> integration_pending_tasks.id\n )`,\n args: [\"processing\", now, id],\n });\n const rows = result.rows ?? [];\n\n if (isPostgres()) {\n if (rows.length === 0) return null;\n return rowToTask(rows[0] as Record<string, unknown>);\n }\n\n // SQLite: no RETURNING, so re-read after the update. Confirm we actually\n // moved it into 'processing' (vs. lost the race).\n const affected =\n (result as { rowsAffected?: number; rowCount?: number }).rowsAffected ??\n (result as { rowsAffected?: number; rowCount?: number }).rowCount;\n if (affected === 0) return null;\n const fetched = await getPendingTask(id);\n if (!fetched || fetched.status !== \"processing\") return null;\n return fetched;\n}\n\n/** Next queued turn for a provider thread after its current task completes. */\nexport async function getNextPendingTaskIdForThread(\n platform: string,\n externalThreadId: string,\n): Promise<string | null> {\n await ensureTable();\n const { rows } = await getDbExec().execute({\n sql: `SELECT id FROM integration_pending_tasks\n WHERE platform = ? AND external_thread_id = ? AND status = 'pending'\n ORDER BY created_at ASC LIMIT 1`,\n args: [platform, externalThreadId],\n });\n return rows[0]?.id ? String(rows[0].id) : null;\n}\n\n/** Mark a task as completed. */\nexport async function markTaskCompleted(id: string): Promise<void> {\n await ensureTable();\n const client = getDbExec();\n const now = Date.now();\n await client.execute({\n sql: `UPDATE integration_pending_tasks\n SET status = ?, updated_at = ?, completed_at = ?, payload = ?\n WHERE id = ?`,\n // The payload can contain short-lived provider credentials such as a\n // Discord interaction token. Once terminal, no retry needs the inbound\n // body, so erase it instead of retaining secrets or user text indefinitely.\n args: [\"completed\", now, now, \"{}\", id],\n });\n}\n\n/**\n * Return a transiently failed task to the retryable queue without erasing its\n * payload. The payload may contain the only copy of the inbound message and is\n * scrubbed only when the task reaches a permanent terminal state.\n */\nexport async function markTaskRetryable(\n id: string,\n errorMessage: string,\n): Promise<void> {\n await ensureTable();\n const client = getDbExec();\n const now = Date.now();\n await client.execute({\n sql: `UPDATE integration_pending_tasks\n SET status = ?, updated_at = ?, error_message = ?\n WHERE id = ? AND status = 'processing'`,\n args: [\"pending\", now, errorMessage.slice(0, 2000), id],\n });\n}\n\n/** Mark a task as failed and stash an error message. */\nexport async function markTaskFailed(\n id: string,\n errorMessage: string,\n): Promise<void> {\n await ensureTable();\n const client = getDbExec();\n const now = Date.now();\n await client.execute({\n sql: `UPDATE integration_pending_tasks\n SET status = ?, updated_at = ?, error_message = ?, payload = ?, external_event_key = NULL\n WHERE id = ?`,\n args: [\"failed\", now, errorMessage.slice(0, 2000), \"{}\", id],\n });\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/integrations/plugin.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/integrations/plugin.ts"],"names":[],"mappings":"AA0IA,OAAO,KAAK,EAEV,yBAAyB,EAI1B,MAAM,YAAY,CAAC;AAOpB,KAAK,cAAc,GAAG,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AA4F9D,KAAK,yBAAyB,GAAG;IAC/B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAgCF,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,yBAAyB,GAClC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAsFlC;AA+ND;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,CAAC,EAAE,yBAAyB,GAClC,cAAc,CAipEhB;AAED;;GAEG;AACH,eAAO,MAAM,yBAAyB,gBAA6B,CAAC"}
|