@agent-native/core 0.131.7 → 0.132.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +123 -0
- package/corpus/core/docs/content/durable-background-runs.mdx +15 -6
- package/corpus/core/docs/design/durable-agent-runs.md +15 -11
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/activity.ts +127 -10
- package/corpus/core/src/a2a/index.ts +3 -0
- package/corpus/core/src/agent/durable-background.ts +38 -28
- package/corpus/core/src/agent/harness/background.ts +12 -2
- package/corpus/core/src/agent/production-agent.ts +18 -15
- package/corpus/core/src/agent/run-store.ts +45 -3
- package/corpus/core/src/application-state/handlers.ts +45 -0
- package/corpus/core/src/application-state/index.ts +3 -0
- package/corpus/core/src/application-state/store.ts +38 -34
- package/corpus/core/src/audit/redact.ts +104 -18
- package/corpus/core/src/cli/create.ts +115 -16
- package/corpus/core/src/client/AgentPanel.tsx +2 -7
- package/corpus/core/src/client/AssistantChat.tsx +10 -2
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +3 -0
- package/corpus/core/src/client/RunStuckBanner.tsx +29 -6
- package/corpus/core/src/client/agent-chat.ts +4 -9
- package/corpus/core/src/client/application-state.ts +115 -12
- package/corpus/core/src/client/dynamic-suggestions.ts +4 -12
- package/corpus/core/src/client/frame-protocol.ts +5 -1
- package/corpus/core/src/client/guided-questions.tsx +12 -23
- package/corpus/core/src/db/client.ts +148 -28
- package/corpus/core/src/db/create-get-db.ts +39 -6
- package/corpus/core/src/db/runtime-diagnostics.ts +41 -6
- package/corpus/core/src/deploy/build.ts +26 -33
- package/corpus/core/src/feature-flags/store.ts +10 -5
- package/corpus/core/src/integrations/a2a-continuations-store.ts +11 -0
- package/corpus/core/src/integrations/config-store.ts +13 -0
- package/corpus/core/src/integrations/google-docs-poller.ts +26 -2
- package/corpus/core/src/mcp-client/routes.ts +33 -2
- package/corpus/core/src/onboarding/plugin.ts +31 -25
- package/corpus/core/src/org/accept-pending.ts +2 -0
- package/corpus/core/src/org/auto-join-domain.ts +2 -0
- package/corpus/core/src/org/context.ts +35 -10
- package/corpus/core/src/org/handlers.ts +6 -0
- package/corpus/core/src/org/request-org-cache.ts +71 -0
- package/corpus/core/src/resources/store.ts +10 -9
- package/corpus/core/src/scripts/runner.ts +6 -3
- package/corpus/core/src/secrets/storage.ts +97 -5
- package/corpus/core/src/server/action-change.ts +33 -0
- package/corpus/core/src/server/action-routes.ts +11 -9
- package/corpus/core/src/server/agent-teams.ts +10 -2
- package/corpus/core/src/server/better-auth-instance.ts +39 -12
- package/corpus/core/src/server/core-routes-plugin.ts +19 -2
- package/corpus/core/src/server/credential-provider.ts +57 -14
- package/corpus/core/src/server/http-response-telemetry.ts +1 -0
- package/corpus/core/src/server/poll.ts +95 -57
- package/corpus/core/src/server/voice-providers-status.ts +19 -4
- package/corpus/core/src/workspace-files/tool.ts +18 -11
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +38 -0
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +1 -1
- package/corpus/templates/analytics/actions/ensure-native-v2-dashboards.ts +299 -0
- package/corpus/templates/analytics/actions/list-native-v2-dashboard-templates.ts +34 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +82 -0
- package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +2 -2
- package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +40 -34
- package/corpus/templates/analytics/app/hooks/use-user-pref.ts +3 -3
- package/corpus/templates/analytics/app/i18n-data.ts +40 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +18 -10
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +14 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +1 -0
- package/corpus/templates/analytics/app/routes/chart.tsx +3 -0
- package/corpus/templates/analytics/changelog/2026-07-30-analytics-dashboards-now-expose-native-funnel-heatmap-callou.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-30-analytics-now-ships-reusable-native-v2-dashboard-manifests-w.md +6 -0
- package/corpus/templates/analytics/server/lib/dashboard-report-render.ts +81 -1
- package/corpus/templates/analytics/server/lib/native-v2-dashboards.ts +457 -0
- package/corpus/templates/analytics/server/lib/report-chart-svg.ts +73 -2
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/analytics/shared/dashboard-funnel.ts +83 -0
- package/corpus/templates/clips/actions/generate-workflow.ts +6 -4
- package/corpus/templates/clips/actions/reconcile-workflow-generation.ts +257 -0
- package/corpus/templates/clips/app/hooks/use-auto-title.ts +166 -4
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +1 -1
- package/corpus/templates/clips/changelog/2026-07-27-stopped-agent-runs-no-longer-leave-generated-workflow-cards-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-30-workflow-generation-now-stops-retrying-after-repeated-failur.md +6 -0
- package/corpus/templates/clips/shared/workflow.ts +5 -0
- package/corpus/templates/content/changelog/2026-07-30-long-running-delegated-requests-now-continue-reliably.md +6 -0
- package/corpus/templates/content/netlify.toml +3 -0
- package/corpus/templates/content/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +2 -1
- package/corpus/templates/slides/actions/patch-deck.ts +25 -1
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +17 -4
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +9 -1
- package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/slides/app/context/DeckContext.tsx +11 -3
- package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +39 -11
- package/corpus/templates/slides/app/pages/Index.tsx +2 -2
- package/corpus/templates/slides/changelog/2026-07-30-slide-previews-keep-their-aspect-ratio-isolate-embedded-styl.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-30-slides-no-longer-lose-decks-while-organization-access-is-loa.md +6 -0
- package/corpus/toolkit/CHANGELOG.md +8 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/composer/runtime-adapters.tsx +16 -11
- package/dist/a2a/activity.d.ts +22 -4
- package/dist/a2a/activity.d.ts.map +1 -1
- package/dist/a2a/activity.js +97 -11
- package/dist/a2a/activity.js.map +1 -1
- package/dist/a2a/index.d.ts +1 -1
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js +1 -1
- package/dist/a2a/index.js.map +1 -1
- package/dist/agent/durable-background.d.ts +13 -8
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +36 -28
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/harness/background.d.ts.map +1 -1
- package/dist/agent/harness/background.js +12 -2
- package/dist/agent/harness/background.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +18 -15
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-store.d.ts +2 -0
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +44 -3
- package/dist/agent/run-store.js.map +1 -1
- package/dist/application-state/handlers.d.ts +15 -0
- package/dist/application-state/handlers.d.ts.map +1 -1
- package/dist/application-state/handlers.js +42 -2
- package/dist/application-state/handlers.js.map +1 -1
- package/dist/application-state/index.d.ts +2 -2
- package/dist/application-state/index.d.ts.map +1 -1
- package/dist/application-state/index.js +2 -2
- package/dist/application-state/index.js.map +1 -1
- package/dist/application-state/store.d.ts +11 -0
- package/dist/application-state/store.d.ts.map +1 -1
- package/dist/application-state/store.js +37 -40
- package/dist/application-state/store.js.map +1 -1
- package/dist/audit/redact.d.ts +25 -2
- package/dist/audit/redact.d.ts.map +1 -1
- package/dist/audit/redact.js +85 -21
- package/dist/audit/redact.js.map +1 -1
- package/dist/cli/create.d.ts +8 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +98 -16
- package/dist/cli/create.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +2 -7
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +10 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +1 -1
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/RunStuckBanner.d.ts +11 -1
- package/dist/client/RunStuckBanner.d.ts.map +1 -1
- package/dist/client/RunStuckBanner.js +20 -7
- package/dist/client/RunStuckBanner.js.map +1 -1
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +4 -6
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/application-state.d.ts +11 -0
- package/dist/client/application-state.d.ts.map +1 -1
- package/dist/client/application-state.js +87 -9
- package/dist/client/application-state.js.map +1 -1
- package/dist/client/dynamic-suggestions.d.ts.map +1 -1
- package/dist/client/dynamic-suggestions.js +4 -13
- package/dist/client/dynamic-suggestions.js.map +1 -1
- package/dist/client/frame-protocol.d.ts +1 -0
- package/dist/client/frame-protocol.d.ts.map +1 -1
- package/dist/client/frame-protocol.js.map +1 -1
- package/dist/client/guided-questions.d.ts.map +1 -1
- package/dist/client/guided-questions.js +6 -22
- package/dist/client/guided-questions.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/db/client.d.ts +57 -10
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +122 -31
- package/dist/db/client.js.map +1 -1
- package/dist/db/create-get-db.d.ts.map +1 -1
- package/dist/db/create-get-db.js +31 -7
- package/dist/db/create-get-db.js.map +1 -1
- package/dist/db/runtime-diagnostics.d.ts +1 -0
- package/dist/db/runtime-diagnostics.d.ts.map +1 -1
- package/dist/db/runtime-diagnostics.js +31 -6
- package/dist/db/runtime-diagnostics.js.map +1 -1
- package/dist/deploy/build.d.ts +5 -20
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +18 -30
- package/dist/deploy/build.js.map +1 -1
- package/dist/feature-flags/store.d.ts.map +1 -1
- package/dist/feature-flags/store.js +10 -5
- package/dist/feature-flags/store.js.map +1 -1
- package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
- package/dist/integrations/a2a-continuations-store.js +12 -0
- package/dist/integrations/a2a-continuations-store.js.map +1 -1
- package/dist/integrations/config-store.d.ts +1 -0
- package/dist/integrations/config-store.d.ts.map +1 -1
- package/dist/integrations/config-store.js +11 -0
- package/dist/integrations/config-store.js.map +1 -1
- package/dist/integrations/google-docs-poller.d.ts.map +1 -1
- package/dist/integrations/google-docs-poller.js +19 -2
- package/dist/integrations/google-docs-poller.js.map +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
- package/dist/mcp-client/routes.d.ts.map +1 -1
- package/dist/mcp-client/routes.js +30 -2
- package/dist/mcp-client/routes.js.map +1 -1
- package/dist/notifications/routes.d.ts +6 -6
- package/dist/onboarding/plugin.d.ts.map +1 -1
- package/dist/onboarding/plugin.js +12 -8
- package/dist/onboarding/plugin.js.map +1 -1
- package/dist/org/accept-pending.d.ts.map +1 -1
- package/dist/org/accept-pending.js +2 -0
- package/dist/org/accept-pending.js.map +1 -1
- package/dist/org/auto-join-domain.d.ts.map +1 -1
- package/dist/org/auto-join-domain.js +2 -0
- package/dist/org/auto-join-domain.js.map +1 -1
- package/dist/org/context.d.ts.map +1 -1
- package/dist/org/context.js +29 -9
- package/dist/org/context.js.map +1 -1
- package/dist/org/handlers.d.ts.map +1 -1
- package/dist/org/handlers.js +5 -0
- package/dist/org/handlers.js.map +1 -1
- package/dist/org/request-org-cache.d.ts +16 -0
- package/dist/org/request-org-cache.d.ts.map +1 -0
- package/dist/org/request-org-cache.js +62 -0
- package/dist/org/request-org-cache.js.map +1 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/resources/store.d.ts.map +1 -1
- package/dist/resources/store.js +11 -9
- package/dist/resources/store.js.map +1 -1
- package/dist/scripts/runner.d.ts.map +1 -1
- package/dist/scripts/runner.js +3 -3
- package/dist/scripts/runner.js.map +1 -1
- package/dist/secrets/storage.d.ts.map +1 -1
- package/dist/secrets/storage.js +82 -5
- package/dist/secrets/storage.js.map +1 -1
- package/dist/server/action-change.d.ts +20 -0
- package/dist/server/action-change.d.ts.map +1 -1
- package/dist/server/action-change.js +29 -0
- package/dist/server/action-change.js.map +1 -1
- package/dist/server/action-routes.d.ts.map +1 -1
- package/dist/server/action-routes.js +7 -8
- package/dist/server/action-routes.js.map +1 -1
- package/dist/server/agent-teams.d.ts.map +1 -1
- package/dist/server/agent-teams.js +10 -2
- package/dist/server/agent-teams.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +31 -14
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +1 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +18 -4
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/credential-provider.d.ts +16 -0
- package/dist/server/credential-provider.d.ts.map +1 -1
- package/dist/server/credential-provider.js +49 -13
- package/dist/server/credential-provider.js.map +1 -1
- package/dist/server/http-response-telemetry.d.ts.map +1 -1
- package/dist/server/http-response-telemetry.js +1 -0
- package/dist/server/http-response-telemetry.js.map +1 -1
- package/dist/server/poll.d.ts +6 -0
- package/dist/server/poll.d.ts.map +1 -1
- package/dist/server/poll.js +84 -47
- package/dist/server/poll.js.map +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/voice-providers-status.d.ts.map +1 -1
- package/dist/server/voice-providers-status.js +12 -3
- package/dist/server/voice-providers-status.js.map +1 -1
- package/dist/workspace-files/tool.d.ts.map +1 -1
- package/dist/workspace-files/tool.js +18 -11
- package/dist/workspace-files/tool.js.map +1 -1
- package/docs/content/durable-background-runs.mdx +15 -6
- package/docs/design/durable-agent-runs.md +15 -11
- package/package.json +3 -3
- package/src/a2a/activity.ts +127 -10
- package/src/a2a/index.ts +3 -0
- package/src/agent/durable-background.ts +38 -28
- package/src/agent/harness/background.ts +12 -2
- package/src/agent/production-agent.ts +18 -15
- package/src/agent/run-store.ts +45 -3
- package/src/application-state/handlers.ts +45 -0
- package/src/application-state/index.ts +3 -0
- package/src/application-state/store.ts +38 -34
- package/src/audit/redact.ts +104 -18
- package/src/cli/create.ts +115 -16
- package/src/client/AgentPanel.tsx +2 -7
- package/src/client/AssistantChat.tsx +10 -2
- package/src/client/MultiTabAssistantChat.tsx +3 -0
- package/src/client/RunStuckBanner.tsx +29 -6
- package/src/client/agent-chat.ts +4 -9
- package/src/client/application-state.ts +115 -12
- package/src/client/dynamic-suggestions.ts +4 -12
- package/src/client/frame-protocol.ts +5 -1
- package/src/client/guided-questions.tsx +12 -23
- package/src/db/client.ts +148 -28
- package/src/db/create-get-db.ts +39 -6
- package/src/db/runtime-diagnostics.ts +41 -6
- package/src/deploy/build.ts +26 -33
- package/src/feature-flags/store.ts +10 -5
- package/src/integrations/a2a-continuations-store.ts +11 -0
- package/src/integrations/config-store.ts +13 -0
- package/src/integrations/google-docs-poller.ts +26 -2
- package/src/mcp-client/routes.ts +33 -2
- package/src/onboarding/plugin.ts +31 -25
- package/src/org/accept-pending.ts +2 -0
- package/src/org/auto-join-domain.ts +2 -0
- package/src/org/context.ts +35 -10
- package/src/org/handlers.ts +6 -0
- package/src/org/request-org-cache.ts +71 -0
- package/src/resources/store.ts +10 -9
- package/src/scripts/runner.ts +6 -3
- package/src/secrets/storage.ts +97 -5
- package/src/server/action-change.ts +33 -0
- package/src/server/action-routes.ts +11 -9
- package/src/server/agent-teams.ts +10 -2
- package/src/server/better-auth-instance.ts +39 -12
- package/src/server/core-routes-plugin.ts +19 -2
- package/src/server/credential-provider.ts +57 -14
- package/src/server/http-response-telemetry.ts +1 -0
- package/src/server/poll.ts +95 -57
- package/src/server/voice-providers-status.ts +19 -4
- package/src/workspace-files/tool.ts +18 -11
package/src/db/client.ts
CHANGED
|
@@ -954,19 +954,19 @@ export function isServerlessRuntime(): boolean {
|
|
|
954
954
|
}
|
|
955
955
|
|
|
956
956
|
/**
|
|
957
|
-
* postgres.js pool options tuned per runtime.
|
|
958
|
-
* one request at a time, so a single connection is enough. Keeping the
|
|
959
|
-
* foreground pool at 1 is important because each instance can also open an
|
|
960
|
-
* app and Better Auth pool; a max of 2 on each pool still exhausted Neon
|
|
961
|
-
* under a burst of warm instances. idle_timeout is shortened on
|
|
957
|
+
* postgres.js pool options tuned per runtime. idle_timeout is shortened on
|
|
962
958
|
* serverless so a thawed-but-idle instance releases its connections quickly.
|
|
963
|
-
* Long-lived Node servers keep the
|
|
959
|
+
* Long-lived Node servers keep the larger pool for throughput.
|
|
960
|
+
*
|
|
961
|
+
* The cap assumes {@link sharedDbPool}: one pool per URL for the whole
|
|
962
|
+
* process, not one per consumer. See {@link neonPoolMax} for why the cap must
|
|
963
|
+
* leave room for concurrency.
|
|
964
964
|
*/
|
|
965
965
|
export function pgPoolOptions(url: string): Record<string, unknown> {
|
|
966
966
|
const serverless = isServerlessRuntime();
|
|
967
967
|
return {
|
|
968
968
|
onnotice: () => {},
|
|
969
|
-
max: serverless ?
|
|
969
|
+
max: serverless ? 4 : 20,
|
|
970
970
|
idle_timeout: serverless ? 20 : 240,
|
|
971
971
|
max_lifetime: 60 * 30,
|
|
972
972
|
connect_timeout: 10,
|
|
@@ -977,13 +977,21 @@ export function pgPoolOptions(url: string): Record<string, unknown> {
|
|
|
977
977
|
}
|
|
978
978
|
|
|
979
979
|
/**
|
|
980
|
-
* Connection cap for the @neondatabase/serverless `Pool`.
|
|
981
|
-
*
|
|
982
|
-
*
|
|
983
|
-
*
|
|
980
|
+
* Connection cap for the @neondatabase/serverless `Pool`.
|
|
981
|
+
*
|
|
982
|
+
* TRAP: this number is the ceiling on how many statements one request can have
|
|
983
|
+
* in flight at once. It used to be 1 on serverless, which made every
|
|
984
|
+
* `Promise.all([...reads])` in a request serialize behind the single slot —
|
|
985
|
+
* against a remote endpoint that is ~83ms of pure wait per query, so ten
|
|
986
|
+
* "concurrent" reads took ~880ms instead of ~86ms. The cap was 1 only because
|
|
987
|
+
* every consumer built its OWN pool (the DbExec singleton, Better Auth, and one
|
|
988
|
+
* per `createGetDb()` schema module), so the real per-instance connection count
|
|
989
|
+
* was that number multiplied by ~6. {@link sharedDbPool} collapses those into
|
|
990
|
+
* one pool per URL, so the same aggregate budget buys in-request concurrency.
|
|
991
|
+
* Keep pools shared if you raise this.
|
|
984
992
|
*/
|
|
985
993
|
export function neonPoolMax(): number {
|
|
986
|
-
if (!isServerlessRuntime()) return
|
|
994
|
+
if (!isServerlessRuntime()) return 20;
|
|
987
995
|
// The durable background-function worker is a SINGLE process per run (unlike
|
|
988
996
|
// the many warm request-instances the foreground serverless has), so it can
|
|
989
997
|
// safely hold a larger pool without risking Neon's connection cap. The agent's
|
|
@@ -996,7 +1004,7 @@ export function neonPoolMax(): number {
|
|
|
996
1004
|
// burst; keep the foreground serverless pool tiny to avoid "Max client
|
|
997
1005
|
// connections reached" across many warm instances.
|
|
998
1006
|
if (isBackgroundFunctionPoolContext()) return 8;
|
|
999
|
-
return
|
|
1007
|
+
return 4;
|
|
1000
1008
|
}
|
|
1001
1009
|
|
|
1002
1010
|
/**
|
|
@@ -1110,6 +1118,113 @@ export function attachNeonPoolErrorLogger(
|
|
|
1110
1118
|
});
|
|
1111
1119
|
}
|
|
1112
1120
|
|
|
1121
|
+
// ---------------------------------------------------------------------------
|
|
1122
|
+
// Shared connection pools
|
|
1123
|
+
// ---------------------------------------------------------------------------
|
|
1124
|
+
|
|
1125
|
+
interface ClosablePool {
|
|
1126
|
+
end(): Promise<unknown>;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
const _sharedDbPools = new Map<string, ClosablePool>();
|
|
1130
|
+
const _sharedDbPoolCloseHooks = new Set<() => void>();
|
|
1131
|
+
const _sharedDbPoolReplacementHooks = new Map<string, Set<() => void>>();
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* One connection pool per (driver, URL) for the whole process.
|
|
1135
|
+
*
|
|
1136
|
+
* Opening a connection to a remote database costs a full TCP + TLS + auth
|
|
1137
|
+
* round-trip chain (~300-800ms measured against Neon in-region), and it used
|
|
1138
|
+
* to be paid once per CONSUMER: the `getDbExec` singleton, Better Auth, and one
|
|
1139
|
+
* more for every `createGetDb()` schema module (four in core alone, plus each
|
|
1140
|
+
* app's). Their `max` values also multiplied against the provider's connection
|
|
1141
|
+
* cap, which forced each pool down to a size too small to run a request's reads
|
|
1142
|
+
* concurrently.
|
|
1143
|
+
*
|
|
1144
|
+
* TRAP: only consumers that live as long as the process may share. Anything
|
|
1145
|
+
* handed a `close()` — `createDbExec()`, the migration exec on the direct
|
|
1146
|
+
* endpoint — must keep a private pool, or closing it takes the whole process's
|
|
1147
|
+
* database access down with it.
|
|
1148
|
+
*/
|
|
1149
|
+
export function sharedDbPool<T extends ClosablePool>(
|
|
1150
|
+
driver: string,
|
|
1151
|
+
url: string,
|
|
1152
|
+
create: () => T,
|
|
1153
|
+
): T {
|
|
1154
|
+
const key = `${driver}${url}`;
|
|
1155
|
+
const existing = _sharedDbPools.get(key);
|
|
1156
|
+
if (existing) return existing as T;
|
|
1157
|
+
const created = create();
|
|
1158
|
+
_sharedDbPools.set(key, created);
|
|
1159
|
+
return created;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Swap the pool registered for a key, for the postgres.js recycle path that
|
|
1164
|
+
* replaces a pool whose query timed out. Without this the registry would keep
|
|
1165
|
+
* handing out the discarded pool.
|
|
1166
|
+
*/
|
|
1167
|
+
export function replaceSharedDbPool<T extends ClosablePool>(
|
|
1168
|
+
driver: string,
|
|
1169
|
+
url: string,
|
|
1170
|
+
previous: T,
|
|
1171
|
+
next: T,
|
|
1172
|
+
): void {
|
|
1173
|
+
const key = `${driver}${url}`;
|
|
1174
|
+
if (_sharedDbPools.get(key) !== previous) return;
|
|
1175
|
+
_sharedDbPools.set(key, next);
|
|
1176
|
+
for (const hook of _sharedDbPoolReplacementHooks.get(key) ?? []) {
|
|
1177
|
+
try {
|
|
1178
|
+
hook();
|
|
1179
|
+
} catch {
|
|
1180
|
+
// A consumer's reset must not block the replacement from being used.
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Run `hook` when one shared pool is replaced, so derived consumers rebuild
|
|
1187
|
+
* their handles instead of continuing to use the timed-out pool.
|
|
1188
|
+
*/
|
|
1189
|
+
export function onSharedDbPoolReplaced(
|
|
1190
|
+
driver: string,
|
|
1191
|
+
url: string,
|
|
1192
|
+
hook: () => void,
|
|
1193
|
+
): void {
|
|
1194
|
+
const key = `${driver}${String.fromCharCode(0)}${url}`;
|
|
1195
|
+
const hooks = _sharedDbPoolReplacementHooks.get(key) ?? new Set();
|
|
1196
|
+
hooks.add(hook);
|
|
1197
|
+
_sharedDbPoolReplacementHooks.set(key, hooks);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* Run `hook` when the shared pools are closed, so consumers holding a derived
|
|
1202
|
+
* handle (a Drizzle instance bound to the pool, the Better Auth adapter) drop
|
|
1203
|
+
* it instead of issuing queries on a closed pool.
|
|
1204
|
+
*/
|
|
1205
|
+
export function onSharedDbPoolsClosed(hook: () => void): void {
|
|
1206
|
+
_sharedDbPoolCloseHooks.add(hook);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
export async function closeSharedDbPools(): Promise<void> {
|
|
1210
|
+
const pools = [..._sharedDbPools.values()];
|
|
1211
|
+
_sharedDbPools.clear();
|
|
1212
|
+
for (const hook of _sharedDbPoolCloseHooks) {
|
|
1213
|
+
try {
|
|
1214
|
+
hook();
|
|
1215
|
+
} catch {
|
|
1216
|
+
// A consumer's reset must not block the rest from being released.
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
await Promise.all(
|
|
1220
|
+
pools.map((pool) =>
|
|
1221
|
+
Promise.resolve(pool.end()).catch(() => {
|
|
1222
|
+
// Already closed (process exiting, provider hung up) — nothing to do.
|
|
1223
|
+
}),
|
|
1224
|
+
),
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1113
1228
|
function disposePostgresPoolEventually(
|
|
1114
1229
|
pool: { end: () => Promise<unknown> },
|
|
1115
1230
|
label: string,
|
|
@@ -1130,8 +1245,6 @@ function disposePostgresPoolEventually(
|
|
|
1130
1245
|
// ---------------------------------------------------------------------------
|
|
1131
1246
|
|
|
1132
1247
|
let _exec: DbExec | undefined;
|
|
1133
|
-
let _pgPool: any;
|
|
1134
|
-
let _neonPool: any;
|
|
1135
1248
|
let _sqlite: any;
|
|
1136
1249
|
let _initPromise: Promise<void> | undefined;
|
|
1137
1250
|
|
|
@@ -1231,9 +1344,14 @@ async function createDbExecInternal(
|
|
|
1231
1344
|
// connections, so its direct executions use stateless Neon HTTP instead.
|
|
1232
1345
|
// The foreground and transaction surface keep the WebSocket pool.
|
|
1233
1346
|
const bgHttp = isBackgroundFunctionPoolContext();
|
|
1234
|
-
const
|
|
1347
|
+
const makePool = () =>
|
|
1348
|
+
new Pool({ connectionString: url, max: neonPoolMax() });
|
|
1349
|
+
// The singleton exec shares the process pool; `createDbExec()` callers own
|
|
1350
|
+
// a `close()` and so must not be handed it.
|
|
1351
|
+
const pool = trackSingletonResources
|
|
1352
|
+
? sharedDbPool("neon", url, makePool)
|
|
1353
|
+
: makePool();
|
|
1235
1354
|
attachNeonPoolErrorLogger(pool);
|
|
1236
|
-
if (trackSingletonResources) _neonPool = pool;
|
|
1237
1355
|
const httpSql = bgHttp ? neon(url, { fullResults: true }) : null;
|
|
1238
1356
|
async function queryNeonClient(
|
|
1239
1357
|
client: any,
|
|
@@ -1461,6 +1579,7 @@ async function createDbExecInternal(
|
|
|
1461
1579
|
}, 1);
|
|
1462
1580
|
},
|
|
1463
1581
|
async close() {
|
|
1582
|
+
if (trackSingletonResources) return closeSharedDbPools();
|
|
1464
1583
|
await pool.end();
|
|
1465
1584
|
},
|
|
1466
1585
|
};
|
|
@@ -1566,12 +1685,17 @@ async function createDbExecInternal(
|
|
|
1566
1685
|
// server-side timeout, avoiding ECONNRESET when the server hangs up.
|
|
1567
1686
|
const createPool = () => postgres(url, pgPoolOptions(url));
|
|
1568
1687
|
type PostgresPool = ReturnType<typeof createPool>;
|
|
1569
|
-
|
|
1570
|
-
|
|
1688
|
+
// Same rule as the Neon path: the singleton exec shares the process pool,
|
|
1689
|
+
// `createDbExec()` callers own a `close()` and get a private one.
|
|
1690
|
+
let pool = trackSingletonResources
|
|
1691
|
+
? sharedDbPool("postgres-js", url, createPool)
|
|
1692
|
+
: createPool();
|
|
1571
1693
|
const recyclePool = (timedOutPool: PostgresPool) => {
|
|
1572
1694
|
if (pool === timedOutPool) {
|
|
1573
1695
|
pool = createPool();
|
|
1574
|
-
if (trackSingletonResources)
|
|
1696
|
+
if (trackSingletonResources) {
|
|
1697
|
+
replaceSharedDbPool("postgres-js", url, timedOutPool, pool);
|
|
1698
|
+
}
|
|
1575
1699
|
}
|
|
1576
1700
|
disposePostgresPoolEventually(timedOutPool, "timed-out pooled query");
|
|
1577
1701
|
};
|
|
@@ -1626,6 +1750,7 @@ async function createDbExecInternal(
|
|
|
1626
1750
|
return result as T;
|
|
1627
1751
|
},
|
|
1628
1752
|
async close() {
|
|
1753
|
+
if (trackSingletonResources) return closeSharedDbPools();
|
|
1629
1754
|
await pool.end();
|
|
1630
1755
|
},
|
|
1631
1756
|
};
|
|
@@ -1884,14 +2009,9 @@ export function getDbExec(): DbExec {
|
|
|
1884
2009
|
|
|
1885
2010
|
/** Close the database connection (for scripts that need cleanup). */
|
|
1886
2011
|
export async function closeDbExec(): Promise<void> {
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
}
|
|
1891
|
-
if (_neonPool) {
|
|
1892
|
-
await _neonPool.end();
|
|
1893
|
-
_neonPool = undefined;
|
|
1894
|
-
}
|
|
2012
|
+
// Both Postgres pools live in the shared registry, which also notifies the
|
|
2013
|
+
// Drizzle / Better Auth consumers bound to them.
|
|
2014
|
+
await closeSharedDbPools();
|
|
1895
2015
|
if (_sqlite) {
|
|
1896
2016
|
_sqlite.close();
|
|
1897
2017
|
_sqlite = undefined;
|
package/src/db/create-get-db.ts
CHANGED
|
@@ -21,6 +21,9 @@ import {
|
|
|
21
21
|
withDbTimeout,
|
|
22
22
|
retryOnConnectionError,
|
|
23
23
|
dbOpTimeoutMs,
|
|
24
|
+
sharedDbPool,
|
|
25
|
+
onSharedDbPoolsClosed,
|
|
26
|
+
onSharedDbPoolReplaced,
|
|
24
27
|
} from "./client.js";
|
|
25
28
|
|
|
26
29
|
// Lazy driver loaders — cached promises so dynamic import only runs once.
|
|
@@ -444,6 +447,27 @@ export function createGetDb<T extends Record<string, unknown>>(schema: T) {
|
|
|
444
447
|
let _db: any;
|
|
445
448
|
let _dbReady: Promise<any> | undefined;
|
|
446
449
|
|
|
450
|
+
// The Drizzle instance is bound to a shared pool, so a `closeDbExec()` (test
|
|
451
|
+
// teardown, script cleanup) must invalidate it rather than leave this store
|
|
452
|
+
// issuing queries on a closed pool. Registered lazily from the pooled
|
|
453
|
+
// branches only — `createGetDb` is called at module scope by every store, and
|
|
454
|
+
// core's specs widely mock `db/client.js`.
|
|
455
|
+
let _closeHookRegistered = false;
|
|
456
|
+
function resetOnPoolClose(driver?: string, url?: string): void {
|
|
457
|
+
if (_closeHookRegistered) return;
|
|
458
|
+
_closeHookRegistered = true;
|
|
459
|
+
onSharedDbPoolsClosed(() => {
|
|
460
|
+
_db = undefined;
|
|
461
|
+
_dbReady = undefined;
|
|
462
|
+
});
|
|
463
|
+
if (driver && url) {
|
|
464
|
+
onSharedDbPoolReplaced(driver, url, () => {
|
|
465
|
+
_db = undefined;
|
|
466
|
+
_dbReady = undefined;
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
447
471
|
function startInit(): Promise<any> {
|
|
448
472
|
if (_dbReady) return _dbReady;
|
|
449
473
|
|
|
@@ -473,10 +497,15 @@ export function createGetDb<T extends Record<string, unknown>>(schema: T) {
|
|
|
473
497
|
if (dialect === "postgres") {
|
|
474
498
|
if (isNeonUrl(url)) {
|
|
475
499
|
_dbReady = getNeonServerlessDrizzle().then(({ drizzle, Pool }) => {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
500
|
+
// Shared with the DbExec singleton, Better Auth, and every other
|
|
501
|
+
// `createGetDb` store: one connect per process instead of one per
|
|
502
|
+
// schema module. See `sharedDbPool` in client.ts.
|
|
503
|
+
resetOnPoolClose("neon", url);
|
|
504
|
+
const rawPool = sharedDbPool(
|
|
505
|
+
"neon",
|
|
506
|
+
url,
|
|
507
|
+
() => new Pool({ connectionString: url, max: neonPoolMax() }),
|
|
508
|
+
);
|
|
480
509
|
attachNeonPoolErrorLogger(rawPool);
|
|
481
510
|
// Wrap the pool with the resilience layer so Drizzle queries get the
|
|
482
511
|
// same withDbTimeout + retryOnConnectionError protection as the raw
|
|
@@ -489,8 +518,12 @@ export function createGetDb<T extends Record<string, unknown>>(schema: T) {
|
|
|
489
518
|
_dbReady = getPgDrizzle().then(({ drizzle, postgres }) => {
|
|
490
519
|
// pgPoolOptions caps the pool to a small size on serverless so
|
|
491
520
|
// concurrent frozen instances don't exhaust Neon/Postgres'
|
|
492
|
-
// connection limit ("Max client connections reached").
|
|
493
|
-
|
|
521
|
+
// connection limit ("Max client connections reached"). Shared across
|
|
522
|
+
// consumers — see `sharedDbPool` in client.ts.
|
|
523
|
+
resetOnPoolClose("postgres-js", url);
|
|
524
|
+
const client = sharedDbPool("postgres-js", url, () =>
|
|
525
|
+
postgres(url, pgPoolOptions(url)),
|
|
526
|
+
);
|
|
494
527
|
_db = drizzle(buildResilientPostgresJsClient(client), { schema });
|
|
495
528
|
});
|
|
496
529
|
}
|
|
@@ -51,7 +51,7 @@ export interface DatabaseSchemaHealthResult {
|
|
|
51
51
|
error?: string;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
const DEFAULT_REQUIRED_SCHEMA: RequiredSchemaTable[] = [
|
|
54
|
+
export const DEFAULT_REQUIRED_SCHEMA: RequiredSchemaTable[] = [
|
|
55
55
|
{
|
|
56
56
|
table: "agent_runs",
|
|
57
57
|
columns: [
|
|
@@ -253,6 +253,24 @@ async function tableColumns(
|
|
|
253
253
|
: sqliteTableColumns(exec, table);
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Memo for the default probe, which several client surfaces re-run on every
|
|
258
|
+
* page load at one `information_schema` round trip per required table.
|
|
259
|
+
*
|
|
260
|
+
* Only a clean, completed result is memoized: a probe reporting a missing table
|
|
261
|
+
* or an unreadable database must never be served from cache, or the repair that
|
|
262
|
+
* follows stays invisible for the whole window. Schema changes are additive and
|
|
263
|
+
* applied by migrations, so a clean answer cannot silently go bad — but the
|
|
264
|
+
* window is still kept to seconds, short enough that anything a deploy changes
|
|
265
|
+
* shows up on the next page load rather than being pinned. Callers that
|
|
266
|
+
* override `exec`, `dialect`, or `required` bypass it entirely.
|
|
267
|
+
*/
|
|
268
|
+
const SCHEMA_HEALTH_MEMO_MS = 5_000;
|
|
269
|
+
let schemaHealthMemo: {
|
|
270
|
+
at: number;
|
|
271
|
+
result: DatabaseSchemaHealthResult;
|
|
272
|
+
} | null = null;
|
|
273
|
+
|
|
256
274
|
export async function runDatabaseSchemaHealthCheck(
|
|
257
275
|
options: {
|
|
258
276
|
exec?: DbExec;
|
|
@@ -260,6 +278,15 @@ export async function runDatabaseSchemaHealthCheck(
|
|
|
260
278
|
required?: RequiredSchemaTable[];
|
|
261
279
|
} = {},
|
|
262
280
|
): Promise<DatabaseSchemaHealthResult> {
|
|
281
|
+
const memoizable = !options.exec && !options.dialect && !options.required;
|
|
282
|
+
if (
|
|
283
|
+
memoizable &&
|
|
284
|
+
schemaHealthMemo &&
|
|
285
|
+
Date.now() - schemaHealthMemo.at < SCHEMA_HEALTH_MEMO_MS
|
|
286
|
+
) {
|
|
287
|
+
return schemaHealthMemo.result;
|
|
288
|
+
}
|
|
289
|
+
|
|
263
290
|
const dialect = options.dialect ?? getDialect();
|
|
264
291
|
const exec = options.exec ?? getDbExec();
|
|
265
292
|
const required = options.required ?? DEFAULT_REQUIRED_SCHEMA;
|
|
@@ -267,18 +294,24 @@ export async function runDatabaseSchemaHealthCheck(
|
|
|
267
294
|
const missingColumns: Array<{ table: string; column: string }> = [];
|
|
268
295
|
|
|
269
296
|
try {
|
|
270
|
-
|
|
271
|
-
|
|
297
|
+
// Independent probes: serially they cost one network round trip each.
|
|
298
|
+
const found = await Promise.all(
|
|
299
|
+
required.map((requirement) =>
|
|
300
|
+
tableColumns(exec, dialect, requirement.table),
|
|
301
|
+
),
|
|
302
|
+
);
|
|
303
|
+
required.forEach((requirement, index) => {
|
|
304
|
+
const columns = found[index];
|
|
272
305
|
if (!columns) {
|
|
273
306
|
missingTables.push(requirement.table);
|
|
274
|
-
|
|
307
|
+
return;
|
|
275
308
|
}
|
|
276
309
|
for (const column of requirement.columns) {
|
|
277
310
|
if (!columns.has(column)) {
|
|
278
311
|
missingColumns.push({ table: requirement.table, column });
|
|
279
312
|
}
|
|
280
313
|
}
|
|
281
|
-
}
|
|
314
|
+
});
|
|
282
315
|
} catch (err) {
|
|
283
316
|
return {
|
|
284
317
|
ok: false,
|
|
@@ -290,13 +323,15 @@ export async function runDatabaseSchemaHealthCheck(
|
|
|
290
323
|
};
|
|
291
324
|
}
|
|
292
325
|
|
|
293
|
-
|
|
326
|
+
const result: DatabaseSchemaHealthResult = {
|
|
294
327
|
ok: missingTables.length === 0 && missingColumns.length === 0,
|
|
295
328
|
checked: true,
|
|
296
329
|
dialect,
|
|
297
330
|
missingTables,
|
|
298
331
|
missingColumns,
|
|
299
332
|
};
|
|
333
|
+
if (memoizable && result.ok) schemaHealthMemo = { at: Date.now(), result };
|
|
334
|
+
return result;
|
|
300
335
|
}
|
|
301
336
|
|
|
302
337
|
export function formatRuntimeDebugFingerprint(
|
package/src/deploy/build.ts
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
AGENT_BACKGROUND_PROCESSOR_ROUTE,
|
|
31
31
|
AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD,
|
|
32
32
|
AGENT_CHAT_PROCESS_RUN_PATH,
|
|
33
|
-
|
|
33
|
+
isDurableBackgroundFlagExplicitlyDisabled,
|
|
34
34
|
} from "../agent/durable-background.js";
|
|
35
35
|
import {
|
|
36
36
|
INTEGRATION_RECOVERY_RUNTIME_MARKER,
|
|
@@ -2576,29 +2576,14 @@ export function findInstalledResvgPackages(
|
|
|
2576
2576
|
|
|
2577
2577
|
/**
|
|
2578
2578
|
* Deploy-time gate for emitting the second `-background` Netlify function.
|
|
2579
|
-
*
|
|
2580
|
-
*
|
|
2581
|
-
*
|
|
2582
|
-
*
|
|
2583
|
-
*
|
|
2584
|
-
* unset/empty/unknown means DISABLED; an app opts IN
|
|
2585
|
-
* only with an explicit truthy value (`true`/`1`/`yes`/`on`). A premature
|
|
2586
|
-
* fleet-wide default-on caused real-user incidents (2026-06-24) before the
|
|
2587
|
-
* async worker path was proven, so durable is opt-in until verified live. This
|
|
2588
|
-
* gate is what emits the 15-min `-background` function so the `_process-run`
|
|
2589
|
-
* dispatch lands on it (async 202 → the worker runs with the real 15-min budget
|
|
2590
|
-
* → its ~13-min soft-timeout fits). The deploy gate and runtime gate MUST agree:
|
|
2591
|
-
* if the deploy emitted no `-background` function but the runtime still routed
|
|
2592
|
-
* the worker into the ~13-min timeout regime, the worker would overshoot the
|
|
2593
|
-
* ~60s synchronous wall and re-dispatch in a loop. (The runtime now also guards
|
|
2594
|
-
* the ~13-min budget on the real function name via `isInBackgroundFunctionRuntime`,
|
|
2595
|
-
* so a missing emit does not loop.) A missing emit is NOT benign, though: the
|
|
2596
|
-
* app then runs every turn on the ~60s synchronous wall for the life of the
|
|
2597
|
-
* deploy, so an opted-in build that cannot emit the function FAILS rather than
|
|
2598
|
-
* warning.
|
|
2579
|
+
* Netlify deploys are default-on; an explicit falsy
|
|
2580
|
+
* `AGENT_CHAT_DURABLE_BACKGROUND` value opts out. This is called only from the
|
|
2581
|
+
* Netlify preset, so non-Netlify builds remain unaffected. The gate and runtime
|
|
2582
|
+
* default must agree: otherwise the runtime could target a worker that the
|
|
2583
|
+
* deploy did not emit.
|
|
2599
2584
|
*/
|
|
2600
2585
|
export function isDurableBackgroundDeployEnabled(): boolean {
|
|
2601
|
-
return
|
|
2586
|
+
return !isDurableBackgroundFlagExplicitlyDisabled();
|
|
2602
2587
|
}
|
|
2603
2588
|
|
|
2604
2589
|
/**
|
|
@@ -2649,9 +2634,15 @@ export function emitSingleTemplateNetlifyKeepWarmFunction(
|
|
|
2649
2634
|
// dispatch (18.4s observed to reach the agent loop). A POST with no runId is
|
|
2650
2635
|
// rejected by the `_process-run` route before any DB work, so this only keeps
|
|
2651
2636
|
// the container alive.
|
|
2652
|
-
const
|
|
2653
|
-
|
|
2654
|
-
|
|
2637
|
+
const backgroundEntryPath = path.join(
|
|
2638
|
+
internalDir,
|
|
2639
|
+
AGENT_BACKGROUND_FUNCTION_NAME,
|
|
2640
|
+
`${AGENT_BACKGROUND_FUNCTION_NAME}.mjs`,
|
|
2641
|
+
);
|
|
2642
|
+
const backgroundWarmPath =
|
|
2643
|
+
isDurableBackgroundEmitRequired() && fs.existsSync(backgroundEntryPath)
|
|
2644
|
+
? JSON.stringify(AGENT_BACKGROUND_FUNCTION_URL_PATH)
|
|
2645
|
+
: "null";
|
|
2655
2646
|
const entry = `const HEALTH_PATH = "/_agent-native/health";
|
|
2656
2647
|
const BACKGROUND_WARM_PATH = ${backgroundWarmPath};
|
|
2657
2648
|
const REQUEST_TIMEOUT_MS = 25_000;
|
|
@@ -4148,14 +4139,12 @@ export default bundle;
|
|
|
4148
4139
|
}
|
|
4149
4140
|
|
|
4150
4141
|
if (preset === "netlify") {
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
//
|
|
4154
|
-
//
|
|
4155
|
-
// function
|
|
4156
|
-
//
|
|
4157
|
-
// function. When not opted in this is a no-op and the single-function
|
|
4158
|
-
// deploy is byte-for-byte unchanged.
|
|
4142
|
+
// Durable background agent runs are default-on for Netlify; a falsy
|
|
4143
|
+
// AGENT_CHAT_DURABLE_BACKGROUND value opts out. Additive ONLY: emits a
|
|
4144
|
+
// SECOND Netlify function whose name ends in `-background` re-exporting the
|
|
4145
|
+
// same handler bundle, so the chat `_process-run` POST lands on Netlify's
|
|
4146
|
+
// async (15-min) function. When opted out this is a no-op and the
|
|
4147
|
+
// single-function deploy is byte-for-byte unchanged.
|
|
4159
4148
|
// NOT wrapped in try/catch: this block only runs when the runtime depends
|
|
4160
4149
|
// on the function, and a swallowed failure ships an app that loses the
|
|
4161
4150
|
// background budget for the life of the deploy with nothing in the log.
|
|
@@ -4163,6 +4152,10 @@ export default bundle;
|
|
|
4163
4152
|
emitSingleTemplateNetlifyBackgroundFunction(cwd);
|
|
4164
4153
|
}
|
|
4165
4154
|
|
|
4155
|
+
// Emit keep-warm after the background artifact so it only pings a function
|
|
4156
|
+
// that this build actually produced.
|
|
4157
|
+
emitSingleTemplateNetlifyKeepWarmFunction(cwd);
|
|
4158
|
+
|
|
4166
4159
|
if (isIntegrationDurableDispatchDeployEnabled()) {
|
|
4167
4160
|
try {
|
|
4168
4161
|
emitSingleTemplateNetlifyIntegrationRecoveryFunction(cwd);
|
|
@@ -89,11 +89,16 @@ export async function getFeatureFlagRules(
|
|
|
89
89
|
if (!getFeatureFlagDefinition(key)) return defaultFeatureFlagRules();
|
|
90
90
|
// An organization-specific rule overrides the global rule. The fallback is
|
|
91
91
|
// what makes global exact-org targeting meaningful for callers in an org.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
// Most flags have no org override, so `??` made the common path two serial
|
|
93
|
+
// round trips; both settings rows are independent, so read them together.
|
|
94
|
+
const orgId = scope.orgId?.trim();
|
|
95
|
+
if (!orgId)
|
|
96
|
+
return normalizeFeatureFlagRules(await getSetting(settingKey(key)));
|
|
97
|
+
const [orgStored, globalStored] = await Promise.all([
|
|
98
|
+
getOrgSetting(orgId, settingKey(key)),
|
|
99
|
+
getSetting(settingKey(key)),
|
|
100
|
+
]);
|
|
101
|
+
return normalizeFeatureFlagRules(orgStored ?? globalStored);
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
/**
|
|
@@ -777,6 +777,17 @@ export async function recoverDueA2AContinuationIds(
|
|
|
777
777
|
const receiptFilter = confirmedDeliveryOnly
|
|
778
778
|
? " AND terminal_delivery_confirmed_at IS NOT NULL"
|
|
779
779
|
: "";
|
|
780
|
+
// The two lease resets below and the due SELECT all only ever touch rows in
|
|
781
|
+
// these three statuses, so one probe short-circuits all three. Without it the
|
|
782
|
+
// 60s retry job pays two blind UPDATE round trips per app forever on a queue
|
|
783
|
+
// that has been empty since boot.
|
|
784
|
+
const live = await client.execute({
|
|
785
|
+
sql: `SELECT id FROM integration_a2a_continuations
|
|
786
|
+
WHERE status IN ('pending', 'processing', 'delivering')${taskFilter}${receiptFilter}
|
|
787
|
+
LIMIT 1`,
|
|
788
|
+
args: [...taskArgs],
|
|
789
|
+
});
|
|
790
|
+
if ((live.rows?.length ?? 0) === 0) return [];
|
|
780
791
|
// If a processor dies after a provider receipt, retry history-only custody
|
|
781
792
|
// as soon as its short follow-up deadline passes. A pre-receipt delivery
|
|
782
793
|
// claim retains the longer stale cutoff before an at-least-once resend.
|
|
@@ -33,6 +33,17 @@ async function ensureTable(): Promise<void> {
|
|
|
33
33
|
return _initPromise;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Bumped on every in-process config write. Pollers that back off while their
|
|
38
|
+
* integration is disabled watch this so enabling one takes effect on the next
|
|
39
|
+
* tick instead of at the end of their backoff window.
|
|
40
|
+
*/
|
|
41
|
+
let _configWriteEpoch = 0;
|
|
42
|
+
|
|
43
|
+
export function integrationConfigWriteEpoch(): number {
|
|
44
|
+
return _configWriteEpoch;
|
|
45
|
+
}
|
|
46
|
+
|
|
36
47
|
export interface IntegrationConfig {
|
|
37
48
|
platform: string;
|
|
38
49
|
configKey: string;
|
|
@@ -88,6 +99,7 @@ export async function saveIntegrationConfig(
|
|
|
88
99
|
Date.now(),
|
|
89
100
|
],
|
|
90
101
|
});
|
|
102
|
+
_configWriteEpoch += 1;
|
|
91
103
|
}
|
|
92
104
|
|
|
93
105
|
/**
|
|
@@ -103,6 +115,7 @@ export async function deleteIntegrationConfig(
|
|
|
103
115
|
sql: `DELETE FROM integration_configs WHERE platform = ? AND config_key = ?`,
|
|
104
116
|
args: [platform, configKey],
|
|
105
117
|
});
|
|
118
|
+
_configWriteEpoch += 1;
|
|
106
119
|
}
|
|
107
120
|
|
|
108
121
|
/**
|
|
@@ -27,7 +27,11 @@ import {
|
|
|
27
27
|
listChanges,
|
|
28
28
|
listDocComments,
|
|
29
29
|
} from "./adapters/google-docs.js";
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
getIntegrationConfig,
|
|
32
|
+
integrationConfigWriteEpoch,
|
|
33
|
+
saveIntegrationConfig,
|
|
34
|
+
} from "./config-store.js";
|
|
31
35
|
import { getThreadMapping, saveThreadMapping } from "./thread-mapping-store.js";
|
|
32
36
|
import type { IncomingMessage } from "./types.js";
|
|
33
37
|
|
|
@@ -634,14 +638,34 @@ export async function startGoogleDocsPoller(
|
|
|
634
638
|
}
|
|
635
639
|
}
|
|
636
640
|
|
|
641
|
+
/** Backoff for the config probe while google-docs is disabled. */
|
|
642
|
+
const DISABLED_CONFIG_RECHECK_MS = 5 * 60 * 1000;
|
|
643
|
+
|
|
637
644
|
function startPollLoop(
|
|
638
645
|
options: GoogleDocsPollerOptions,
|
|
639
646
|
intervalMs: number,
|
|
640
647
|
): void {
|
|
648
|
+
let disabledUntil = 0;
|
|
649
|
+
let disabledAtConfigWriteEpoch = -1;
|
|
650
|
+
|
|
641
651
|
async function poll() {
|
|
642
652
|
try {
|
|
653
|
+
// The loop starts even when google-docs was never configured (so it picks
|
|
654
|
+
// up a later enable), which used to mean one `integration_configs` read
|
|
655
|
+
// every 30s per app forever. While disabled, re-read only every
|
|
656
|
+
// DISABLED_CONFIG_RECHECK_MS — or immediately after any in-process config
|
|
657
|
+
// write, so enabling the integration still takes effect on the next tick.
|
|
658
|
+
const epoch = integrationConfigWriteEpoch();
|
|
659
|
+
if (Date.now() < disabledUntil && epoch === disabledAtConfigWriteEpoch) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
643
662
|
const config = await getIntegrationConfig(PLATFORM);
|
|
644
|
-
if (!config?.configData?.enabled)
|
|
663
|
+
if (!config?.configData?.enabled) {
|
|
664
|
+
disabledUntil = Date.now() + DISABLED_CONFIG_RECHECK_MS;
|
|
665
|
+
disabledAtConfigWriteEpoch = epoch;
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
disabledUntil = 0;
|
|
645
669
|
await processChanges(options);
|
|
646
670
|
} catch (err) {
|
|
647
671
|
// Unwrap ErrorEvent (Neon WS driver emits these on network failure) so logs show the real cause
|