@agent-native/core 0.131.9 → 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 +99 -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 +28 -11
- package/corpus/core/src/audit/redact.ts +104 -18
- package/corpus/core/src/client/AgentPanel.tsx +2 -7
- 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/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/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/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 +23 -10
- 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/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/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/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/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/observability/routes.d.ts +3 -3
- 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/transcribe-voice.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/docs/content/durable-background-runs.mdx +15 -6
- package/docs/design/durable-agent-runs.md +15 -11
- package/package.json +2 -2
- 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 +28 -11
- package/src/audit/redact.ts +104 -18
- package/src/client/AgentPanel.tsx +2 -7
- 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/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/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
|
package/src/mcp-client/routes.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { getSession } from "../server/auth.js";
|
|
|
32
32
|
import { getH3App } from "../server/framework-request-handler.js";
|
|
33
33
|
import { readBody } from "../server/h3-helpers.js";
|
|
34
34
|
import { runWithRequestContext } from "../server/request-context.js";
|
|
35
|
-
import { getAllSettings } from "../settings/store.js";
|
|
35
|
+
import { getAllSettings, getSettingsEmitter } from "../settings/store.js";
|
|
36
36
|
import {
|
|
37
37
|
areBuiltinMcpCapabilitiesSupported,
|
|
38
38
|
BUILTIN_MCP_CAPABILITIES,
|
|
@@ -291,6 +291,13 @@ function sortedConfigSignature(config: McpConfig | null): string {
|
|
|
291
291
|
return JSON.stringify(entries);
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
/**
|
|
295
|
+
* How long the refresh may skip the settings read on the strength of "no
|
|
296
|
+
* in-process settings write since the last one". Bounds how stale a remote MCP
|
|
297
|
+
* server list added by ANOTHER process can be.
|
|
298
|
+
*/
|
|
299
|
+
const MCP_CONFIG_REFRESH_BACKSTOP_MS = 5 * 60 * 1000;
|
|
300
|
+
|
|
294
301
|
function mcpConfigRefreshIntervalMs(): number {
|
|
295
302
|
const raw = process.env.AGENT_NATIVE_MCP_CONFIG_REFRESH_MS;
|
|
296
303
|
if (raw?.trim() === "0") return 0;
|
|
@@ -307,8 +314,24 @@ export function startMcpConfigRefresh(
|
|
|
307
314
|
|
|
308
315
|
let currentSignature = sortedConfigSignature(manager.getConfig());
|
|
309
316
|
let refreshing = false;
|
|
317
|
+
// `buildMergedConfig` reads the entire settings table just to diff a
|
|
318
|
+
// signature, which on an idle app is a full-table round trip every minute
|
|
319
|
+
// forever. Only pay it when an in-process settings write says something might
|
|
320
|
+
// have changed, plus a periodic backstop for writes from another process.
|
|
321
|
+
let settingsDirty = true;
|
|
322
|
+
let lastFullRefresh = 0;
|
|
323
|
+
const markDirty = () => {
|
|
324
|
+
settingsDirty = true;
|
|
325
|
+
};
|
|
326
|
+
getSettingsEmitter().on("settings", markDirty);
|
|
310
327
|
const refresh = async () => {
|
|
311
328
|
if (refreshing) return;
|
|
329
|
+
if (
|
|
330
|
+
!settingsDirty &&
|
|
331
|
+
Date.now() - lastFullRefresh < MCP_CONFIG_REFRESH_BACKSTOP_MS
|
|
332
|
+
) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
312
335
|
refreshing = true;
|
|
313
336
|
try {
|
|
314
337
|
const next = await buildMergedConfig();
|
|
@@ -317,7 +340,12 @@ export function startMcpConfigRefresh(
|
|
|
317
340
|
await manager.reconfigure(next);
|
|
318
341
|
currentSignature = nextSignature;
|
|
319
342
|
}
|
|
343
|
+
settingsDirty = false;
|
|
344
|
+
lastFullRefresh = Date.now();
|
|
320
345
|
} catch (err: any) {
|
|
346
|
+
// Keep this dirty so a transient database or manager failure is retried
|
|
347
|
+
// on the next interval instead of being hidden by the backstop window.
|
|
348
|
+
settingsDirty = true;
|
|
321
349
|
console.warn(
|
|
322
350
|
`[mcp-client] config refresh failed: ${err?.message ?? err}`,
|
|
323
351
|
);
|
|
@@ -328,7 +356,10 @@ export function startMcpConfigRefresh(
|
|
|
328
356
|
|
|
329
357
|
const timer = setInterval(refresh, intervalMs);
|
|
330
358
|
(timer as { unref?: () => void }).unref?.();
|
|
331
|
-
return () =>
|
|
359
|
+
return () => {
|
|
360
|
+
clearInterval(timer);
|
|
361
|
+
getSettingsEmitter().off("settings", markDirty);
|
|
362
|
+
};
|
|
332
363
|
}
|
|
333
364
|
|
|
334
365
|
async function resolveContextForRequest(event: H3Event): Promise<{
|
package/src/onboarding/plugin.ts
CHANGED
|
@@ -81,30 +81,34 @@ async function serializeSteps(
|
|
|
81
81
|
options: { preview?: boolean } = {},
|
|
82
82
|
): Promise<OnboardingStepStatus[]> {
|
|
83
83
|
const steps = listOnboardingSteps();
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
84
|
+
// Steps are independent of each other, and each `isComplete()` is itself a
|
|
85
|
+
// chain of credential/settings reads — walking them one at a time made this
|
|
86
|
+
// route cost the SUM of every step's round trips against a remote database
|
|
87
|
+
// instead of the slowest one. `Promise.all` preserves `steps` order.
|
|
88
|
+
return Promise.all(
|
|
89
|
+
steps.map(async (step) => {
|
|
90
|
+
let complete = false;
|
|
91
|
+
if (!options.preview) {
|
|
92
|
+
try {
|
|
93
|
+
complete = (await step.isComplete(context)) === true;
|
|
94
|
+
} catch {
|
|
95
|
+
complete = false;
|
|
96
|
+
}
|
|
97
|
+
if (!complete) {
|
|
98
|
+
complete = await hasOverride(context.sessionId, step.id);
|
|
99
|
+
}
|
|
95
100
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
return out;
|
|
101
|
+
return {
|
|
102
|
+
id: step.id,
|
|
103
|
+
title: step.title,
|
|
104
|
+
description: step.description,
|
|
105
|
+
order: step.order,
|
|
106
|
+
required: step.required ?? false,
|
|
107
|
+
complete,
|
|
108
|
+
methods: step.methods,
|
|
109
|
+
};
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
function withOnboardingRequestContext<T>(
|
|
@@ -240,11 +244,13 @@ export function createOnboardingPlugin(
|
|
|
240
244
|
// doesn't surface as a 500 to the client.
|
|
241
245
|
try {
|
|
242
246
|
return await withOnboardingRequestContext(context, async () => {
|
|
243
|
-
const value = await
|
|
247
|
+
const [value, statuses] = await Promise.all([
|
|
248
|
+
appStateGet(context.sessionId, DISMISSED_KEY),
|
|
249
|
+
serializeSteps(context),
|
|
250
|
+
]);
|
|
244
251
|
const dismissed = !!(
|
|
245
252
|
value && (value as { dismissed?: boolean }).dismissed
|
|
246
253
|
);
|
|
247
|
-
const statuses = await serializeSteps(context);
|
|
248
254
|
return {
|
|
249
255
|
dismissed,
|
|
250
256
|
allComplete: allRequiredComplete(statuses),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getDbExec } from "../db/client.js";
|
|
2
2
|
import { setActiveOrgId } from "./active-org.js";
|
|
3
|
+
import { invalidateRequestMemberOrgIds } from "./request-org-cache.js";
|
|
3
4
|
|
|
4
5
|
const nanoid = (): string =>
|
|
5
6
|
globalThis.crypto?.randomUUID?.().replace(/-/g, "") ??
|
|
@@ -75,6 +76,7 @@ export async function acceptPendingInvitationsForEmail(
|
|
|
75
76
|
ON CONFLICT (org_id, LOWER(email)) DO NOTHING`,
|
|
76
77
|
args: [nanoid(), inv.orgId, email, role, Date.now()],
|
|
77
78
|
});
|
|
79
|
+
invalidateRequestMemberOrgIds();
|
|
78
80
|
}
|
|
79
81
|
await db.execute({
|
|
80
82
|
sql: `UPDATE org_invitations SET status = 'accepted' WHERE id = ?`,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getDbExec } from "../db/client.js";
|
|
2
2
|
import { getUserSetting } from "../settings/user-settings.js";
|
|
3
3
|
import { setActiveOrgId } from "./active-org.js";
|
|
4
|
+
import { invalidateRequestMemberOrgIds } from "./request-org-cache.js";
|
|
4
5
|
|
|
5
6
|
const nanoid = (): string =>
|
|
6
7
|
globalThis.crypto?.randomUUID?.().replace(/-/g, "") ??
|
|
@@ -87,6 +88,7 @@ export async function autoJoinDomainMatchingOrgs(
|
|
|
87
88
|
args: [nanoid(), m.orgId, email, Date.now()],
|
|
88
89
|
});
|
|
89
90
|
joined.push({ orgId: m.orgId });
|
|
91
|
+
invalidateRequestMemberOrgIds();
|
|
90
92
|
} catch {
|
|
91
93
|
// Race with a parallel join (e.g. user accepted an invite to the
|
|
92
94
|
// same org milliseconds earlier). The unique constraint keeps the
|
package/src/org/context.ts
CHANGED
|
@@ -7,6 +7,10 @@ import { getSetting } from "../settings/store.js";
|
|
|
7
7
|
import { getUserSetting } from "../settings/user-settings.js";
|
|
8
8
|
import { setActiveOrgId } from "./active-org.js";
|
|
9
9
|
import { autoJoinDomainMatchingOrgs } from "./auto-join-domain.js";
|
|
10
|
+
import {
|
|
11
|
+
invalidateRequestMemberOrgIds,
|
|
12
|
+
requestMemberOrgIds,
|
|
13
|
+
} from "./request-org-cache.js";
|
|
10
14
|
import type { OrgContext, OrgRole } from "./types.js";
|
|
11
15
|
|
|
12
16
|
const EMPTY_CONTEXT: OrgContext = {
|
|
@@ -210,6 +214,12 @@ async function resolveOrgContextUncached(event: H3Event): Promise<OrgContext> {
|
|
|
210
214
|
|
|
211
215
|
const exec = getDbExec();
|
|
212
216
|
|
|
217
|
+
// Started before the memberships await so the two round trips overlap; the
|
|
218
|
+
// `catch` only covers the early returns below, the later `await` still
|
|
219
|
+
// propagates a real failure.
|
|
220
|
+
const activeOrgSettingPromise = loadActiveOrgSettingForEvent(event, email);
|
|
221
|
+
activeOrgSettingPromise.catch(() => {});
|
|
222
|
+
|
|
213
223
|
let memberships: MembershipRow[] | null;
|
|
214
224
|
try {
|
|
215
225
|
memberships = await loadMembershipsForEvent(event, email);
|
|
@@ -238,7 +248,7 @@ async function resolveOrgContextUncached(event: H3Event): Promise<OrgContext> {
|
|
|
238
248
|
return { email, orgId: null, orgName: null, role: null };
|
|
239
249
|
}
|
|
240
250
|
|
|
241
|
-
const activeOrgSetting = await
|
|
251
|
+
const activeOrgSetting = await activeOrgSettingPromise;
|
|
242
252
|
const explicitPersonal = activeOrgSetting?.orgId === null;
|
|
243
253
|
|
|
244
254
|
const emailDomain = emailDomainOf(email);
|
|
@@ -410,17 +420,26 @@ async function loadMemberships(email: string): Promise<MembershipRow[] | null> {
|
|
|
410
420
|
export async function resolveOrgIdForEmail(
|
|
411
421
|
email: string,
|
|
412
422
|
): Promise<string | null> {
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
423
|
+
const idsPromise = requestMemberOrgIds(email, async () => {
|
|
424
|
+
const rows = await queryOrgMembers({
|
|
425
|
+
sql: `SELECT org_id FROM org_members WHERE LOWER(email) = ?
|
|
426
|
+
${MEMBERSHIP_FALLBACK_ORDER_BY}`,
|
|
427
|
+
args: [email.toLowerCase()],
|
|
428
|
+
});
|
|
429
|
+
return rows?.map((r: any) => String(r.org_id)) ?? null;
|
|
417
430
|
});
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
431
|
+
// Both reads depend only on the email, so they overlap instead of queueing.
|
|
432
|
+
// The `catch` only keeps the early return below from surfacing an unhandled
|
|
433
|
+
// rejection; the `await` further down still propagates the failure.
|
|
434
|
+
const settingPromise = getUserSetting(
|
|
421
435
|
email,
|
|
422
436
|
"active-org-id",
|
|
423
|
-
)
|
|
437
|
+
) as Promise<ActiveOrgSetting>;
|
|
438
|
+
settingPromise.catch(() => {});
|
|
439
|
+
|
|
440
|
+
const ids = await idsPromise;
|
|
441
|
+
if (!ids?.length) return null;
|
|
442
|
+
const activeOrgSetting = await settingPromise;
|
|
424
443
|
if (activeOrgSetting?.orgId === null) return null;
|
|
425
444
|
if (activeOrgSetting?.orgId && ids.includes(activeOrgSetting.orgId)) {
|
|
426
445
|
return activeOrgSetting.orgId;
|
|
@@ -438,9 +457,13 @@ export async function resolveOrgIdForEmailViaEvent(
|
|
|
438
457
|
event: H3Event,
|
|
439
458
|
email: string,
|
|
440
459
|
): Promise<string | null> {
|
|
460
|
+
// Overlapped, not queued: each is a separate round trip and this pair runs
|
|
461
|
+
// on the session-backfill path of every authenticated request.
|
|
462
|
+
const settingPromise = loadActiveOrgSettingForEvent(event, email);
|
|
463
|
+
settingPromise.catch(() => {});
|
|
441
464
|
const memberships = await loadMembershipsForEvent(event, email);
|
|
442
465
|
if (!memberships || memberships.length === 0) return null;
|
|
443
|
-
const activeOrgSetting = await
|
|
466
|
+
const activeOrgSetting = await settingPromise;
|
|
444
467
|
if (activeOrgSetting?.orgId === null) return null;
|
|
445
468
|
if (
|
|
446
469
|
activeOrgSetting?.orgId &&
|
|
@@ -485,6 +508,7 @@ export async function createOrganization(
|
|
|
485
508
|
sql: `INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, ?, ?)`,
|
|
486
509
|
args: [nanoid(), id, email, role, createdAt],
|
|
487
510
|
});
|
|
511
|
+
invalidateRequestMemberOrgIds();
|
|
488
512
|
|
|
489
513
|
await warnOnAdditionalOrganization(exec, email, id, trimmedName);
|
|
490
514
|
|
|
@@ -669,6 +693,7 @@ async function tryCreateDefaultOrg(
|
|
|
669
693
|
sql: `INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, ?, ?)`,
|
|
670
694
|
args: [nanoid(), orgId, email, "owner", now],
|
|
671
695
|
});
|
|
696
|
+
invalidateRequestMemberOrgIds();
|
|
672
697
|
|
|
673
698
|
await setActiveOrgId(email, orgId, "auto-created default organization");
|
|
674
699
|
|
package/src/org/handlers.ts
CHANGED
|
@@ -46,6 +46,7 @@ import { readBody } from "../server/h3-helpers.js";
|
|
|
46
46
|
import { setActiveOrgId } from "./active-org.js";
|
|
47
47
|
import { getOrgContext, createOrganization } from "./context.js";
|
|
48
48
|
import { isFreeEmailProvider } from "./free-email-providers.js";
|
|
49
|
+
import { invalidateRequestMemberOrgIds } from "./request-org-cache.js";
|
|
49
50
|
import type { OrgRole } from "./types.js";
|
|
50
51
|
import { parseWorkspaceUrl } from "./workspace-url.js";
|
|
51
52
|
|
|
@@ -505,6 +506,7 @@ export const acceptInvitationHandler = defineEventHandler(
|
|
|
505
506
|
sql: `INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, ?, ?)`,
|
|
506
507
|
args: [nanoid(), invOrgId, email, inviteRole, Date.now()],
|
|
507
508
|
});
|
|
509
|
+
invalidateRequestMemberOrgIds();
|
|
508
510
|
|
|
509
511
|
await e.execute({
|
|
510
512
|
sql: `UPDATE org_invitations SET status = 'accepted' WHERE id = ?`,
|
|
@@ -574,6 +576,7 @@ export const removeMemberHandler = defineEventHandler(
|
|
|
574
576
|
sql: `DELETE FROM org_members WHERE org_id = ? AND LOWER(email) = ?`,
|
|
575
577
|
args: [ctx.orgId, memberEmailLower],
|
|
576
578
|
});
|
|
579
|
+
invalidateRequestMemberOrgIds();
|
|
577
580
|
|
|
578
581
|
return { success: true };
|
|
579
582
|
},
|
|
@@ -772,6 +775,8 @@ export const deleteOrgHandler = defineEventHandler(async (event: H3Event) => {
|
|
|
772
775
|
});
|
|
773
776
|
}
|
|
774
777
|
|
|
778
|
+
invalidateRequestMemberOrgIds();
|
|
779
|
+
|
|
775
780
|
const nextRes = await e.execute({
|
|
776
781
|
sql: `SELECT org_id AS "orgId" FROM org_members WHERE LOWER(email) = ? LIMIT 1`,
|
|
777
782
|
args: [ctx.email.toLowerCase()],
|
|
@@ -875,6 +880,7 @@ export const joinByDomainHandler = defineEventHandler(
|
|
|
875
880
|
sql: `INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, 'member', ?)`,
|
|
876
881
|
args: [nanoid(), orgId, email, Date.now()],
|
|
877
882
|
});
|
|
883
|
+
invalidateRequestMemberOrgIds();
|
|
878
884
|
|
|
879
885
|
await setActiveOrgId(email, orgId, "joined domain-matched organization");
|
|
880
886
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { getRequestContext } from "../server/request-context.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-request memo of the `org_members` read behind `resolveOrgIdForEmail`,
|
|
5
|
+
* keyed on the active AsyncLocalStorage `RequestContext` (WeakMap → freed with
|
|
6
|
+
* the request) and then on the lowercased email. Mirrors the settings cache in
|
|
7
|
+
* `settings/store.ts`.
|
|
8
|
+
*
|
|
9
|
+
* The `event.context` caches in `context.ts` only cover call chains that carry
|
|
10
|
+
* an h3 event. Identity resolution for credential lookups, agent runs, A2A,
|
|
11
|
+
* MCP, and adapter-authenticated action calls has no event, so every one of
|
|
12
|
+
* those callers used to pay its own round trip for the same answer — on a
|
|
13
|
+
* remote Postgres that is ~83ms each.
|
|
14
|
+
*
|
|
15
|
+
* TRAP: the key is the email, never "the current request's user". A single
|
|
16
|
+
* request legitimately resolves several addresses (the signed-in caller plus a
|
|
17
|
+
* run owner or credential subject), so a context-only key would answer one
|
|
18
|
+
* identity with another's memberships.
|
|
19
|
+
*/
|
|
20
|
+
const requestOrgIds = new WeakMap<
|
|
21
|
+
object,
|
|
22
|
+
Map<string, Promise<string[] | null>>
|
|
23
|
+
>();
|
|
24
|
+
|
|
25
|
+
function cacheForRequest(
|
|
26
|
+
create: boolean,
|
|
27
|
+
): Map<string, Promise<string[] | null>> | null {
|
|
28
|
+
const ctx = getRequestContext();
|
|
29
|
+
if (!ctx || typeof ctx !== "object") return null;
|
|
30
|
+
let cache = requestOrgIds.get(ctx);
|
|
31
|
+
if (!cache && create) {
|
|
32
|
+
cache = new Map();
|
|
33
|
+
requestOrgIds.set(ctx, cache);
|
|
34
|
+
}
|
|
35
|
+
return cache ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the org ids `email` belongs to, once per request. `null` means the
|
|
40
|
+
* membership rows were unreadable and is cached like any other answer; a
|
|
41
|
+
* rejection is evicted so one transient failure cannot answer every later
|
|
42
|
+
* lookup in the same request.
|
|
43
|
+
*/
|
|
44
|
+
export function requestMemberOrgIds(
|
|
45
|
+
email: string,
|
|
46
|
+
load: () => Promise<string[] | null>,
|
|
47
|
+
): Promise<string[] | null> {
|
|
48
|
+
const cache = cacheForRequest(true);
|
|
49
|
+
if (!cache) return load();
|
|
50
|
+
const key = email.trim().toLowerCase();
|
|
51
|
+
let pending = cache.get(key);
|
|
52
|
+
if (!pending) {
|
|
53
|
+
pending = load().catch((err) => {
|
|
54
|
+
cache.delete(key);
|
|
55
|
+
throw err;
|
|
56
|
+
});
|
|
57
|
+
cache.set(key, pending);
|
|
58
|
+
}
|
|
59
|
+
return pending;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Drop this request's memoized memberships after a write to `org_members`.
|
|
64
|
+
* Clears every email rather than one: deleting an organization or removing a
|
|
65
|
+
* member changes the answer for accounts other than the one being written.
|
|
66
|
+
* Requests already in flight elsewhere keep their own snapshot for the rest of
|
|
67
|
+
* their (short) lifetime, the same tradeoff the settings cache documents.
|
|
68
|
+
*/
|
|
69
|
+
export function invalidateRequestMemberOrgIds(): void {
|
|
70
|
+
cacheForRequest(false)?.clear();
|
|
71
|
+
}
|
package/src/resources/store.ts
CHANGED
|
@@ -1494,18 +1494,19 @@ export async function resourceEffectiveContext(
|
|
|
1494
1494
|
): Promise<EffectiveResourceContext> {
|
|
1495
1495
|
await ensureTable();
|
|
1496
1496
|
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
});
|
|
1497
|
+
// Four independent reads of the same path under different owners; the
|
|
1498
|
+
// precedence below is pure JS, so serialising them only bought four round
|
|
1499
|
+
// trips of latency. Mirrors `resourceListAccessible`, which already fans out.
|
|
1501
1500
|
const organizationOwner = sharedResourceOwner(options?.orgId);
|
|
1502
|
-
const organization =
|
|
1501
|
+
const [workspace, organization, legacyShared, personal] = await Promise.all([
|
|
1502
|
+
resourceGetByPath(WORKSPACE_OWNER, path, { ...options, userEmail }),
|
|
1503
1503
|
organizationOwner === SHARED_OWNER
|
|
1504
|
-
? null
|
|
1505
|
-
:
|
|
1506
|
-
|
|
1504
|
+
? Promise.resolve(null)
|
|
1505
|
+
: resourceGetByPath(organizationOwner, path),
|
|
1506
|
+
resourceGetByPath(SHARED_OWNER, path),
|
|
1507
|
+
resourceGetByPath(userEmail, path),
|
|
1508
|
+
]);
|
|
1507
1509
|
const shared = organization ?? legacyShared;
|
|
1508
|
-
const personal = await resourceGetByPath(userEmail, path);
|
|
1509
1510
|
const effective = personal ?? shared ?? workspace ?? null;
|
|
1510
1511
|
const effectiveScope: ResourceInheritanceScope | null = personal
|
|
1511
1512
|
? "personal"
|