@agent-native/core 0.98.7 → 0.98.9
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 +17 -11
- package/corpus/core/docs/content/locales/ar-SA/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/de-DE/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/es-ES/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/fr-FR/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/hi-IN/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/ja-JP/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/ko-KR/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/pt-BR/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/zh-CN/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/zh-TW/external-agents.mdx +6 -2
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/session-replay.ts +29 -7
- package/corpus/core/src/integrations/adapters/slack.ts +13 -2
- 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 +183 -48
- package/corpus/core/src/integrations/types.ts +7 -0
- package/corpus/core/src/mcp/build-server.ts +99 -26
- package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +31 -2
- package/corpus/templates/calendar/AGENTS.md +11 -0
- package/corpus/templates/calendar/README.md +2 -1
- package/corpus/templates/calendar/actions/create-event.ts +7 -6
- package/corpus/templates/calendar/actions/event-action-helpers.ts +49 -0
- package/corpus/templates/calendar/actions/update-event.ts +225 -14
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +185 -70
- package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +26 -4
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +112 -69
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +621 -524
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +275 -164
- package/corpus/templates/calendar/app/components/calendar/WorkingLocationEditor.tsx +222 -0
- package/corpus/templates/calendar/app/hooks/use-events.ts +151 -79
- package/corpus/templates/calendar/app/i18n/zh-TW.ts +6 -0
- package/corpus/templates/calendar/app/i18n-data.ts +60 -0
- package/corpus/templates/calendar/app/lib/all-day-layout.ts +126 -0
- package/corpus/templates/calendar/app/lib/event-form-utils.ts +18 -1
- package/corpus/templates/calendar/app/lib/event-mutation-inputs.ts +1 -1
- package/corpus/templates/calendar/app/lib/working-location.ts +163 -0
- package/corpus/templates/calendar/app/pages/CalendarView.tsx +21 -2
- package/corpus/templates/calendar/changelog/2026-07-07-working-locations-from-google-calendar-now-appear-as-native-.md +6 -0
- package/corpus/templates/calendar/server/lib/calendar-availability.ts +2 -0
- package/corpus/templates/calendar/server/lib/google-api.ts +6 -1
- package/corpus/templates/calendar/server/lib/google-calendar.ts +49 -19
- package/corpus/templates/calendar/shared/api.ts +2 -0
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +24 -7
- package/dist/client/session-replay.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/integrations/adapters/slack.js +11 -2
- package/dist/integrations/adapters/slack.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 +140 -34
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/types.d.ts +7 -0
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +82 -28
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/secrets/routes.d.ts +9 -9
- package/docs/content/external-agents.mdx +17 -11
- package/docs/content/locales/ar-SA/external-agents.mdx +6 -2
- package/docs/content/locales/de-DE/external-agents.mdx +6 -2
- package/docs/content/locales/es-ES/external-agents.mdx +6 -2
- package/docs/content/locales/fr-FR/external-agents.mdx +6 -2
- package/docs/content/locales/hi-IN/external-agents.mdx +6 -2
- package/docs/content/locales/ja-JP/external-agents.mdx +6 -2
- package/docs/content/locales/ko-KR/external-agents.mdx +6 -2
- package/docs/content/locales/pt-BR/external-agents.mdx +6 -2
- package/docs/content/locales/zh-CN/external-agents.mdx +6 -2
- package/docs/content/locales/zh-TW/external-agents.mdx +6 -2
- package/package.json +1 -1
|
@@ -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":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AAErE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,iBAAiB,GAAG,MAAM,CAAC;AACjC,8EAA8E;AAC9E,MAAM,sBAAsB,GAAG,MAAM,CAAC;AACtC,4EAA4E;AAC5E,MAAM,iCAAiC,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AACxD,MAAM,oCAAoC,GAAG,MAAM,CAAC;AACpD,mEAAmE;AACnE,MAAM,cAAc,GAAG,GAAG,sBAAsB,4BAA4B,CAAC;AAE7E,IAAI,aAAa,GAA0C,IAAI,CAAC;AAChE,IAAI,YAAY,GAAyC,IAAI,CAAC;AAC9D,IAAI,oBAAwC,CAAC;AAC7C;;;GAGG;AACH,IAAI,WAAW,GAAmB,IAAI,CAAC;AAQvC;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,cAAuB;IAEvB,MAAM,OAAO,GAAG,cAAc,IAAI,oBAAoB,CAAC;IACvD,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,aAAa,GAAG,GAAG,GAAG,sBAAsB,CAAC;IACnD,MAAM,gBAAgB,GAAG,GAAG,GAAG,yBAAyB,EAAE,CAAC;IAE3D,IAAI,SAAyB,CAAC;IAC9B,IAAI,CAAC;QACH,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC;YACpC,GAAG,EAAE;;;;;OAKJ;YACD,wEAAwE;YACxE,qEAAqE;YACrE,wEAAwE;YACxE,wEAAwE;YACxE,IAAI,EAAE,CAAC,aAAa,EAAE,aAAa,EAAE,gBAAgB,CAAC;SACvD,CAAC,CAAC;QACH,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC3B,EAAE,EAAE,CAAC,CAAC,EAAY;YAClB,MAAM,EAAE,CAAC,CAAC,MAAgB;YAC1B,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC;SAClC,CAAC,CAAC,CAAC;QACJ,WAAW,GAAG,IAAI,CAAC;IACrB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,yEAAyE;QACzE,sEAAsE;QACtE,iCAAiC;QACjC,IAAI,WAAW,KAAK,KAAK,EAAE,CAAC;YAC1B,WAAW,GAAG,KAAK,CAAC;YACpB,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;gBACtB,OAAO,CAAC,GAAG,CACT,yEAAyE,CAC1E,CAAC;YACJ,CAAC;QACH,CAAC;QACD,OAAO;IACT,CAAC;IAED,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAEnC,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAC5B,IAAI,CAAC;YACH,kEAAkE;YAClE,0CAA0C;YAC1C,IAAI,GAAG,CAAC,QAAQ,IAAI,yBAAyB,EAAE,CAAC;gBAC9C,MAAM,MAAM,CAAC,OAAO,CAAC;oBACnB,GAAG,EAAE;;;;;;;;;WASJ;oBACD,IAAI,EAAE;wBACJ,IAAI,CAAC,GAAG,EAAE;wBACV,uBAAuB,yBAAyB,WAAW;wBAC3D,GAAG,CAAC,EAAE;wBACN,GAAG,CAAC,MAAM;qBACX;iBACF,CAAC,CAAC;gBACH,OAAO,CAAC,IAAI,CACV,+BAA+B,GAAG,CAAC,EAAE,aAAa,yBAAyB,4BAA4B,CACxG,CAAC;gBACF,SAAS;YACX,CAAC;YAED,qEAAqE;YACrE,+DAA+D;YAC/D,oDAAoD;YACpD,yEAAyE;YACzE,MAAM,SAAS,GAAG,GAAG,CAAC,MAAM,KAAK,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC;YACvE,MAAM,MAAM,CAAC,OAAO,CAAC;gBACnB,GAAG,EAAE;;;;;SAKJ;gBACD,IAAI,EAAE,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC;aAClD,CAAC,CAAC;YAEH,MAAM,eAAe,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;QACzC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CACX,+CAA+C,GAAG,CAAC,EAAE,GAAG,EACxD,GAAG,CACJ,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,yBAAyB;IAChC,IACE,OAAO,CAAC,GAAG,CAAC,OAAO;QACnB,OAAO,CAAC,GAAG,CAAC,wBAAwB;QACpC,OAAO,CAAC,GAAG,CAAC,MAAM;QAClB,UAAU,IAAI,UAAU,EACxB,CAAC;QACD,OAAO,oCAAoC,CAAC;IAC9C,CAAC;IACD,OAAO,iCAAiC,CAAC;AAC3C,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,eAAe,CAC5B,MAAc,EACd,cAAkC;IAElC,MAAM,OAAO,GACX,cAAc;QACd,OAAO,CAAC,GAAG,CAAC,gBAAgB;QAC5B,OAAO,CAAC,GAAG,CAAC,OAAO;QACnB,OAAO,CAAC,GAAG,CAAC,GAAG;QACf,OAAO,CAAC,GAAG,CAAC,UAAU;QACtB,oBAAoB,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC;IAEjD,MAAM,GAAG,GAAG,GAAG,yBAAyB,CAAC,OAAO,CAAC,GAAG,cAAc,EAAE,CAAC;IAErE,2EAA2E;IAC3E,0EAA0E;IAC1E,yEAAyE;IACzE,wEAAwE;IACxE,sBAAsB;IACtB,MAAM,OAAO,GAA2B;QACtC,cAAc,EAAE,kBAAkB;KACnC,CAAC;IACF,IAAI,CAAC;QACH,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,iBAAiB,CAAC,MAAM,CAAC,EAAE,CAAC;IACnE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;YAC1C,OAAO,CAAC,KAAK,CACX,4CAA4C,MAAM,gCAAgC;gBAChF,mDAAmD,CACtD,CAAC;YACF,OAAO;QACT,CAAC;QACD,sEAAsE;QACtE,6EAA6E;QAC7E,IAAI,GAAG,YAAY,KAAK,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YAC7D,OAAO,CAAC,KAAK,CACX,4DAA4D,MAAM,GAAG,EACrE,GAAG,CACJ,CAAC;QACJ,CAAC;IACH,CAAC;IAED,oEAAoE;IACpE,sEAAsE;IACtE,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,KAAK,CAAC,CAAC;IAE1D,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,GAAG,EAAE;YACf,MAAM,EAAE,MAAM;YACd,OAAO;YACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;YAChC,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC,CAAC;IACL,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAEzC;IACC,IAAI,aAAa;QAAE,OAAO;IAC1B,oBAAoB,GAAG,OAAO,EAAE,cAAc,CAAC;IAE/C,6EAA6E;IAC7E,YAAY,GAAG,UAAU,CAAC,GAAG,EAAE;QAC7B,KAAK,sBAAsB,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YAC1C,OAAO,CAAC,KAAK,CAAC,+CAA+C,EAAE,GAAG,CAAC,CAAC;QACtE,CAAC,CAAC,CAAC;IACL,CAAC,EAAE,MAAM,CAAC,CAAC;IACX,UAAU,CAAC,YAAY,CAAC,CAAC;IAEzB,aAAa,GAAG,WAAW,CAAC,GAAG,EAAE;QAC/B,KAAK,sBAAsB,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YAC1C,OAAO,CAAC,KAAK,CAAC,+CAA+C,EAAE,GAAG,CAAC,CAAC;QACtE,CAAC,CAAC,CAAC;IACL,CAAC,EAAE,iBAAiB,CAAC,CAAC;IACtB,UAAU,CAAC,aAAa,CAAC,CAAC;IAE1B,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;QACtB,OAAO,CAAC,GAAG,CACT,yDACE,iBAAiB,GAAG,IACtB,IAAI,CACL,CAAC;IACJ,CAAC;AACH,CAAC;AAED,2BAA2B;AAC3B,MAAM,UAAU,wBAAwB;IACtC,IAAI,YAAY,EAAE,CAAC;QACjB,YAAY,CAAC,YAAY,CAAC,CAAC;QAC3B,YAAY,GAAG,IAAI,CAAC;IACtB,CAAC;IACD,IAAI,aAAa,EAAE,CAAC;QAClB,aAAa,CAAC,aAAa,CAAC,CAAC;QAC7B,aAAa,GAAG,IAAI,CAAC;IACvB,CAAC;IACD,oBAAoB,GAAG,SAAS,CAAC;AACnC,CAAC;AAED,SAAS,UAAU,CAAC,KAAqC;IACtD,KAA2C,CAAC,KAAK,EAAE,EAAE,CAAC;AACzD,CAAC","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"}
|
|
@@ -31,7 +31,7 @@ import { disconnectIntegrationInstallation, listIntegrationInstallations, resolv
|
|
|
31
31
|
import { forgetIntegrationMemory, integrationMemoryActions, listIntegrationMemory, rememberForIntegrationScope, } from "./integration-memory.js";
|
|
32
32
|
import { extractBearerToken, signInternalToken, verifyInternalToken, } from "./internal-token.js";
|
|
33
33
|
import { startPendingTasksRetryJob } from "./pending-tasks-retry-job.js";
|
|
34
|
-
import { claimPendingTask, getNextPendingTaskIdForThread, insertPendingTask, MAX_PENDING_TASK_ATTEMPTS, markTaskCompleted, markTaskFailed, markTaskRetryable, } from "./pending-tasks-store.js";
|
|
34
|
+
import { claimPendingTask, getNextPendingTaskIdForThread, insertPendingTask, isDuplicateEventError, MAX_PENDING_TASK_ATTEMPTS, markTaskCompleted, markTaskFailed, markTaskRetryable, } from "./pending-tasks-store.js";
|
|
35
35
|
import { claimNextComputerCommand, claimNextRemoteCommand, enqueueComputerCommand, enqueueRemoteCommand as enqueueRemoteCommandRow, isRemoteCommandKind, listRemoteCommandsForOwner, updateRemoteCommandResult, } from "./remote-commands-store.js";
|
|
36
36
|
import { authenticateRemoteDeviceToken, createRemoteDevice, getRemoteComputerCapabilities, getRemoteDeviceForOwner, listRemoteDevicesForOwner, revokeRemoteDeviceForOwner, toPublicRemoteDevice, unregisterRemoteDevice, updateRemoteDeviceDetails, } from "./remote-devices-store.js";
|
|
37
37
|
import { listRemotePushNotificationsForOwner, listRemotePushRegistrationsForOwner, queueRemotePushNotifications, toPublicRemotePushRegistration, unregisterRemotePushRegistrationForOwner, upsertRemotePushRegistration, } from "./remote-push-store.js";
|
|
@@ -128,6 +128,10 @@ const REMOTE_DEVICE_ONLINE_MS = 90_000;
|
|
|
128
128
|
// API outage every message would otherwise get another identical "try again"
|
|
129
129
|
// reply. Short enough that a persistent condition still reminds the sender.
|
|
130
130
|
const DECLINE_NOTICE_DEDUPE_TTL_MS = 5 * 60 * 1_000;
|
|
131
|
+
const SYSTEM_NOTICE_DEDUPE_TTL_MS = 24 * 60 * 60 * 1_000;
|
|
132
|
+
function systemNoticeEventKey(dedupeKey, ttlMs, now = Date.now()) {
|
|
133
|
+
return `system-notice:${dedupeKey}:${Math.floor(now / ttlMs)}`;
|
|
134
|
+
}
|
|
131
135
|
export async function enqueueRemoteCommand(envelope) {
|
|
132
136
|
const ownerEmail = readString(envelope.ownerEmail);
|
|
133
137
|
if (!ownerEmail)
|
|
@@ -462,6 +466,61 @@ export function createIntegrationsPlugin(options) {
|
|
|
462
466
|
const initialToolNames = Object.keys(localActions);
|
|
463
467
|
const h3 = getH3App(nitroApp);
|
|
464
468
|
const P = `${FRAMEWORK_ROUTE_PREFIX}/integrations`;
|
|
469
|
+
async function enqueueSystemNotice(event, incoming, text, opts) {
|
|
470
|
+
if (!text.trim())
|
|
471
|
+
return;
|
|
472
|
+
const taskId = `notice-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
473
|
+
const dedupeTtlMs = Math.max(1, opts?.dedupeTtlMs ?? SYSTEM_NOTICE_DEDUPE_TTL_MS);
|
|
474
|
+
const noticeThreadId = `system-notice:${taskId}`;
|
|
475
|
+
const payload = {
|
|
476
|
+
kind: "system-notice",
|
|
477
|
+
incoming,
|
|
478
|
+
text,
|
|
479
|
+
...(opts?.dedupeKey ? { dedupeKey: opts.dedupeKey } : {}),
|
|
480
|
+
...(opts?.dedupeTtlMs ? { dedupeTtlMs: opts.dedupeTtlMs } : {}),
|
|
481
|
+
};
|
|
482
|
+
try {
|
|
483
|
+
await insertPendingTask({
|
|
484
|
+
id: taskId,
|
|
485
|
+
platform: incoming.platform,
|
|
486
|
+
// System notices are auxiliary delivery work, not the user's agent
|
|
487
|
+
// run. Give each notice its own queue lane so a retrying notice cannot
|
|
488
|
+
// block the real message task for this Slack/Telegram thread.
|
|
489
|
+
externalThreadId: noticeThreadId,
|
|
490
|
+
payload: JSON.stringify(payload),
|
|
491
|
+
ownerEmail: `integration@${incoming.platform}`,
|
|
492
|
+
externalEventKey: opts?.dedupeKey
|
|
493
|
+
? systemNoticeEventKey(opts.dedupeKey, dedupeTtlMs)
|
|
494
|
+
: undefined,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
if (isDuplicateEventError(err))
|
|
499
|
+
return;
|
|
500
|
+
throw err;
|
|
501
|
+
}
|
|
502
|
+
// The SQL row is the durable source of truth. This best-effort self-call
|
|
503
|
+
// only reduces latency; the recurring pending-task sweep retries a row
|
|
504
|
+
// if the serverless host freezes this webhook execution immediately.
|
|
505
|
+
let token;
|
|
506
|
+
try {
|
|
507
|
+
token = signInternalToken(taskId);
|
|
508
|
+
}
|
|
509
|
+
catch (err) {
|
|
510
|
+
if (process.env.NODE_ENV === "production")
|
|
511
|
+
throw err;
|
|
512
|
+
}
|
|
513
|
+
void fetch(`${getBaseUrl(event)}${P}/process-task`, {
|
|
514
|
+
method: "POST",
|
|
515
|
+
headers: {
|
|
516
|
+
"Content-Type": "application/json",
|
|
517
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
518
|
+
},
|
|
519
|
+
body: JSON.stringify({ taskId }),
|
|
520
|
+
}).catch((err) => {
|
|
521
|
+
console.warn("[integrations] System notice dispatch failed:", err);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
465
524
|
async function requireSession(event) {
|
|
466
525
|
const session = await getSession(event).catch(() => null);
|
|
467
526
|
if (session?.email)
|
|
@@ -1238,6 +1297,23 @@ export function createIntegrationsPlugin(options) {
|
|
|
1238
1297
|
...(task.orgId ? { orgId: task.orgId } : {}),
|
|
1239
1298
|
isIntegrationCaller: true,
|
|
1240
1299
|
}, async () => {
|
|
1300
|
+
const taskPayload = JSON.parse(task.payload);
|
|
1301
|
+
if (taskPayload.kind === "system-notice") {
|
|
1302
|
+
if (!adapter.sendSystemNotice) {
|
|
1303
|
+
throw new Error(`Platform ${task.platform} cannot deliver system notices`);
|
|
1304
|
+
}
|
|
1305
|
+
const config = await getIntegrationConfig(task.platform);
|
|
1306
|
+
const credentialContext = await credentialContextForIntegrationConfig(config);
|
|
1307
|
+
await withCredentialContext(credentialContext, () => adapter.sendSystemNotice(taskPayload.incoming, taskPayload.text, {
|
|
1308
|
+
...(taskPayload.dedupeKey
|
|
1309
|
+
? { dedupeKey: taskPayload.dedupeKey }
|
|
1310
|
+
: {}),
|
|
1311
|
+
...(taskPayload.dedupeTtlMs
|
|
1312
|
+
? { dedupeTtlMs: taskPayload.dedupeTtlMs }
|
|
1313
|
+
: {}),
|
|
1314
|
+
}));
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1241
1317
|
const resources = await loadResourcesForPrompt(task.ownerEmail, true, options?.appId, task.orgId);
|
|
1242
1318
|
await processIntegrationTask(task, {
|
|
1243
1319
|
adapter,
|
|
@@ -1911,41 +1987,41 @@ export function createIntegrationsPlugin(options) {
|
|
|
1911
1987
|
}
|
|
1912
1988
|
let defaultExecutionContext = null;
|
|
1913
1989
|
if (incoming.platform === "slack" &&
|
|
1914
|
-
incoming.conversationType === "dm"
|
|
1990
|
+
incoming.conversationType === "dm" &&
|
|
1991
|
+
!options?.resolveExecutionContext) {
|
|
1915
1992
|
try {
|
|
1916
1993
|
defaultExecutionContext = await withCredentialContext(credentialContext, () => resolveDefaultIntegrationExecutionContext(incoming));
|
|
1917
1994
|
}
|
|
1918
1995
|
catch (err) {
|
|
1919
|
-
//
|
|
1920
|
-
//
|
|
1921
|
-
//
|
|
1922
|
-
//
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
// acknowledgement has a stricter deadline than notice
|
|
1931
|
-
// delivery and must not be delayed by a slow postMessage.
|
|
1932
|
-
void withCredentialContext(credentialContext, () => adapter.sendSystemNotice
|
|
1933
|
-
? adapter.sendSystemNotice(incoming, declined.userFacingMessage, {
|
|
1996
|
+
// The legacy owner-only resolver predates org-bound identities
|
|
1997
|
+
// and must not turn a rejected Slack DM into an authenticated
|
|
1998
|
+
// owner run. Custom resolveExecutionContext is checked above and
|
|
1999
|
+
// skips this default ladder entirely so apps can fully own auth
|
|
2000
|
+
// without framework membership checks or identity side effects.
|
|
2001
|
+
const declined = err instanceof IntegrationIdentityDeclinedError ? err : null;
|
|
2002
|
+
if (declined) {
|
|
2003
|
+
console.warn(`[integrations] default Slack DM identity declined message:`, declined.message);
|
|
2004
|
+
if (adapter.sendSystemNotice) {
|
|
2005
|
+
try {
|
|
2006
|
+
await enqueueSystemNotice(event, incoming, declined.userFacingMessage, {
|
|
1934
2007
|
dedupeKey: `decline:${incoming.tenantId ?? "unknown"}:${incoming.senderId ?? "unknown"}:${declined.reason}`,
|
|
1935
2008
|
dedupeTtlMs: DECLINE_NOTICE_DEDUPE_TTL_MS,
|
|
1936
|
-
})
|
|
1937
|
-
|
|
1938
|
-
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
catch (noticeErr) {
|
|
2012
|
+
console.warn(`[integrations] could not persist decline notice:`, noticeErr instanceof Error
|
|
1939
2013
|
? noticeErr.message
|
|
1940
2014
|
: noticeErr);
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
console.error(`[integrations] default Slack DM identity denied message:`, err);
|
|
2015
|
+
setResponseStatus(event, 500);
|
|
2016
|
+
return { error: "notice enqueue failed" };
|
|
2017
|
+
}
|
|
1945
2018
|
}
|
|
1946
|
-
setResponseStatus(event, 200);
|
|
1947
|
-
return "ok";
|
|
1948
2019
|
}
|
|
2020
|
+
else {
|
|
2021
|
+
console.error(`[integrations] default Slack DM identity denied message:`, err);
|
|
2022
|
+
}
|
|
2023
|
+
setResponseStatus(event, 200);
|
|
2024
|
+
return "ok";
|
|
1949
2025
|
}
|
|
1950
2026
|
}
|
|
1951
2027
|
let executionContext = {
|
|
@@ -1966,14 +2042,39 @@ export function createIntegrationsPlugin(options) {
|
|
|
1966
2042
|
else if (defaultExecutionContext) {
|
|
1967
2043
|
executionContext = defaultExecutionContext;
|
|
1968
2044
|
if (defaultExecutionContext.anonymousMember) {
|
|
2045
|
+
if (!options?.allowAnonymousOrgScopedSlackDm) {
|
|
2046
|
+
const senderEmail = typeof incoming.senderEmail === "string" &&
|
|
2047
|
+
incoming.senderEmail.trim()
|
|
2048
|
+
? incoming.senderEmail.trim()
|
|
2049
|
+
: null;
|
|
2050
|
+
const noticeText = senderEmail
|
|
2051
|
+
? `I couldn't match your Slack account to an organization member, so I can't run this request. Ask an organization admin to add ${senderEmail}, then try again.`
|
|
2052
|
+
: "I couldn't verify your Slack account email, so I can't run this request. Ask an organization admin to reconnect Slack with the users:read.email scope, then try again.";
|
|
2053
|
+
if (adapter.sendSystemNotice) {
|
|
2054
|
+
try {
|
|
2055
|
+
await enqueueSystemNotice(event, incoming, noticeText, {
|
|
2056
|
+
dedupeKey: `anonymous-tier-disabled:${incoming.tenantId ?? "unknown"}:${incoming.senderId ?? "unknown"}`,
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
catch (noticeErr) {
|
|
2060
|
+
console.warn(`[integrations] could not persist unlinked-member notice:`, noticeErr instanceof Error
|
|
2061
|
+
? noticeErr.message
|
|
2062
|
+
: noticeErr);
|
|
2063
|
+
setResponseStatus(event, 500);
|
|
2064
|
+
return { error: "notice enqueue failed" };
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
setResponseStatus(event, 200);
|
|
2068
|
+
return "ok";
|
|
2069
|
+
}
|
|
1969
2070
|
// The anonymous tier must never be silent. (1) The agent run
|
|
1970
2071
|
// can tell: the note rides the serialized `incoming` into the
|
|
1971
2072
|
// queued task and surfaces via <integration-context>.
|
|
1972
2073
|
incoming.identityNote =
|
|
1973
2074
|
"Caller is an unlinked Slack workspace member running with organization-wide visibility only; personal or privately-shared data is not accessible. They can get personal access by having an admin add their Slack email to the organization (or by reconnecting Slack with the users:read.email scope).";
|
|
1974
|
-
// (2) The sender gets a one-time heads-up
|
|
1975
|
-
//
|
|
1976
|
-
//
|
|
2075
|
+
// (2) The sender gets a one-time heads-up through the same
|
|
2076
|
+
// durable SQL queue as agent work. The self-dispatch is only a
|
|
2077
|
+
// latency optimization; the retry sweep guarantees delivery.
|
|
1977
2078
|
if (adapter.sendSystemNotice) {
|
|
1978
2079
|
const senderEmail = typeof incoming.senderEmail === "string" &&
|
|
1979
2080
|
incoming.senderEmail.trim()
|
|
@@ -1982,11 +2083,16 @@ export function createIntegrationsPlugin(options) {
|
|
|
1982
2083
|
const noticeText = senderEmail
|
|
1983
2084
|
? `Heads up: I couldn't match your Slack account to an organization member, so I can only use org-wide data. Ask an admin to add ${senderEmail} to the organization for personal access.`
|
|
1984
2085
|
: "Heads up: I couldn't verify your Slack account's email, so I can only use org-wide data. Ask an admin to update the Slack connection with the users:read.email scope for personal access.";
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
}
|
|
2086
|
+
try {
|
|
2087
|
+
await enqueueSystemNotice(event, incoming, noticeText, {
|
|
2088
|
+
dedupeKey: `anonymous-tier:${incoming.tenantId ?? "unknown"}:${incoming.senderId ?? "unknown"}`,
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
catch (noticeErr) {
|
|
2092
|
+
console.warn(`[integrations] could not persist anonymous-tier notice:`, noticeErr instanceof Error ? noticeErr.message : noticeErr);
|
|
2093
|
+
setResponseStatus(event, 500);
|
|
2094
|
+
return { error: "notice enqueue failed" };
|
|
2095
|
+
}
|
|
1990
2096
|
}
|
|
1991
2097
|
}
|
|
1992
2098
|
}
|