@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.
Files changed (292) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +99 -0
  3. package/corpus/core/docs/content/durable-background-runs.mdx +15 -6
  4. package/corpus/core/docs/design/durable-agent-runs.md +15 -11
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/a2a/activity.ts +127 -10
  7. package/corpus/core/src/a2a/index.ts +3 -0
  8. package/corpus/core/src/agent/durable-background.ts +38 -28
  9. package/corpus/core/src/agent/harness/background.ts +12 -2
  10. package/corpus/core/src/agent/production-agent.ts +18 -15
  11. package/corpus/core/src/agent/run-store.ts +45 -3
  12. package/corpus/core/src/application-state/handlers.ts +45 -0
  13. package/corpus/core/src/application-state/index.ts +3 -0
  14. package/corpus/core/src/application-state/store.ts +28 -11
  15. package/corpus/core/src/audit/redact.ts +104 -18
  16. package/corpus/core/src/client/AgentPanel.tsx +2 -7
  17. package/corpus/core/src/client/MultiTabAssistantChat.tsx +3 -0
  18. package/corpus/core/src/client/RunStuckBanner.tsx +29 -6
  19. package/corpus/core/src/client/agent-chat.ts +4 -9
  20. package/corpus/core/src/client/application-state.ts +115 -12
  21. package/corpus/core/src/client/dynamic-suggestions.ts +4 -12
  22. package/corpus/core/src/client/guided-questions.tsx +12 -23
  23. package/corpus/core/src/db/client.ts +148 -28
  24. package/corpus/core/src/db/create-get-db.ts +39 -6
  25. package/corpus/core/src/db/runtime-diagnostics.ts +41 -6
  26. package/corpus/core/src/deploy/build.ts +26 -33
  27. package/corpus/core/src/feature-flags/store.ts +10 -5
  28. package/corpus/core/src/integrations/a2a-continuations-store.ts +11 -0
  29. package/corpus/core/src/integrations/config-store.ts +13 -0
  30. package/corpus/core/src/integrations/google-docs-poller.ts +26 -2
  31. package/corpus/core/src/mcp-client/routes.ts +33 -2
  32. package/corpus/core/src/onboarding/plugin.ts +31 -25
  33. package/corpus/core/src/org/accept-pending.ts +2 -0
  34. package/corpus/core/src/org/auto-join-domain.ts +2 -0
  35. package/corpus/core/src/org/context.ts +35 -10
  36. package/corpus/core/src/org/handlers.ts +6 -0
  37. package/corpus/core/src/org/request-org-cache.ts +71 -0
  38. package/corpus/core/src/resources/store.ts +10 -9
  39. package/corpus/core/src/scripts/runner.ts +6 -3
  40. package/corpus/core/src/secrets/storage.ts +97 -5
  41. package/corpus/core/src/server/action-change.ts +33 -0
  42. package/corpus/core/src/server/action-routes.ts +11 -9
  43. package/corpus/core/src/server/agent-teams.ts +10 -2
  44. package/corpus/core/src/server/better-auth-instance.ts +39 -12
  45. package/corpus/core/src/server/core-routes-plugin.ts +19 -2
  46. package/corpus/core/src/server/credential-provider.ts +57 -14
  47. package/corpus/core/src/server/http-response-telemetry.ts +1 -0
  48. package/corpus/core/src/server/poll.ts +95 -57
  49. package/corpus/core/src/server/voice-providers-status.ts +19 -4
  50. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +38 -0
  51. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +1 -1
  52. package/corpus/templates/analytics/actions/ensure-native-v2-dashboards.ts +299 -0
  53. package/corpus/templates/analytics/actions/list-native-v2-dashboard-templates.ts +34 -0
  54. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +82 -0
  55. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +2 -2
  56. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +40 -34
  57. package/corpus/templates/analytics/app/hooks/use-user-pref.ts +3 -3
  58. package/corpus/templates/analytics/app/i18n-data.ts +40 -0
  59. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +18 -10
  60. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +14 -0
  61. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +1 -0
  62. package/corpus/templates/analytics/app/routes/chart.tsx +3 -0
  63. package/corpus/templates/analytics/changelog/2026-07-30-analytics-dashboards-now-expose-native-funnel-heatmap-callou.md +6 -0
  64. package/corpus/templates/analytics/changelog/2026-07-30-analytics-now-ships-reusable-native-v2-dashboard-manifests-w.md +6 -0
  65. package/corpus/templates/analytics/server/lib/dashboard-report-render.ts +81 -1
  66. package/corpus/templates/analytics/server/lib/native-v2-dashboards.ts +457 -0
  67. package/corpus/templates/analytics/server/lib/report-chart-svg.ts +73 -2
  68. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
  69. package/corpus/templates/analytics/shared/dashboard-funnel.ts +83 -0
  70. package/corpus/templates/content/changelog/2026-07-30-long-running-delegated-requests-now-continue-reliably.md +6 -0
  71. package/corpus/templates/content/netlify.toml +3 -0
  72. package/corpus/templates/content/server/plugins/agent-chat.ts +1 -0
  73. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +2 -1
  74. package/corpus/templates/slides/actions/patch-deck.ts +25 -1
  75. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +17 -4
  76. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +9 -1
  77. package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -1
  78. package/corpus/templates/slides/app/context/DeckContext.tsx +11 -3
  79. package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +39 -11
  80. package/corpus/templates/slides/app/pages/Index.tsx +2 -2
  81. package/corpus/templates/slides/changelog/2026-07-30-slide-previews-keep-their-aspect-ratio-isolate-embedded-styl.md +6 -0
  82. package/corpus/templates/slides/changelog/2026-07-30-slides-no-longer-lose-decks-while-organization-access-is-loa.md +6 -0
  83. package/corpus/toolkit/CHANGELOG.md +8 -0
  84. package/corpus/toolkit/package.json +1 -1
  85. package/corpus/toolkit/src/composer/runtime-adapters.tsx +16 -11
  86. package/dist/a2a/activity.d.ts +22 -4
  87. package/dist/a2a/activity.d.ts.map +1 -1
  88. package/dist/a2a/activity.js +97 -11
  89. package/dist/a2a/activity.js.map +1 -1
  90. package/dist/a2a/index.d.ts +1 -1
  91. package/dist/a2a/index.d.ts.map +1 -1
  92. package/dist/a2a/index.js +1 -1
  93. package/dist/a2a/index.js.map +1 -1
  94. package/dist/agent/durable-background.d.ts +13 -8
  95. package/dist/agent/durable-background.d.ts.map +1 -1
  96. package/dist/agent/durable-background.js +36 -28
  97. package/dist/agent/durable-background.js.map +1 -1
  98. package/dist/agent/harness/background.d.ts.map +1 -1
  99. package/dist/agent/harness/background.js +12 -2
  100. package/dist/agent/harness/background.js.map +1 -1
  101. package/dist/agent/production-agent.d.ts.map +1 -1
  102. package/dist/agent/production-agent.js +18 -15
  103. package/dist/agent/production-agent.js.map +1 -1
  104. package/dist/agent/run-store.d.ts +2 -0
  105. package/dist/agent/run-store.d.ts.map +1 -1
  106. package/dist/agent/run-store.js +44 -3
  107. package/dist/agent/run-store.js.map +1 -1
  108. package/dist/application-state/handlers.d.ts +15 -0
  109. package/dist/application-state/handlers.d.ts.map +1 -1
  110. package/dist/application-state/handlers.js +42 -2
  111. package/dist/application-state/handlers.js.map +1 -1
  112. package/dist/application-state/index.d.ts +2 -2
  113. package/dist/application-state/index.d.ts.map +1 -1
  114. package/dist/application-state/index.js +2 -2
  115. package/dist/application-state/index.js.map +1 -1
  116. package/dist/application-state/store.d.ts +11 -0
  117. package/dist/application-state/store.d.ts.map +1 -1
  118. package/dist/application-state/store.js +23 -10
  119. package/dist/application-state/store.js.map +1 -1
  120. package/dist/audit/redact.d.ts +25 -2
  121. package/dist/audit/redact.d.ts.map +1 -1
  122. package/dist/audit/redact.js +85 -21
  123. package/dist/audit/redact.js.map +1 -1
  124. package/dist/client/AgentPanel.d.ts.map +1 -1
  125. package/dist/client/AgentPanel.js +2 -7
  126. package/dist/client/AgentPanel.js.map +1 -1
  127. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  128. package/dist/client/MultiTabAssistantChat.js +1 -1
  129. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  130. package/dist/client/RunStuckBanner.d.ts +11 -1
  131. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  132. package/dist/client/RunStuckBanner.js +20 -7
  133. package/dist/client/RunStuckBanner.js.map +1 -1
  134. package/dist/client/agent-chat.d.ts.map +1 -1
  135. package/dist/client/agent-chat.js +4 -6
  136. package/dist/client/agent-chat.js.map +1 -1
  137. package/dist/client/application-state.d.ts +11 -0
  138. package/dist/client/application-state.d.ts.map +1 -1
  139. package/dist/client/application-state.js +87 -9
  140. package/dist/client/application-state.js.map +1 -1
  141. package/dist/client/dynamic-suggestions.d.ts.map +1 -1
  142. package/dist/client/dynamic-suggestions.js +4 -13
  143. package/dist/client/dynamic-suggestions.js.map +1 -1
  144. package/dist/client/guided-questions.d.ts.map +1 -1
  145. package/dist/client/guided-questions.js +6 -22
  146. package/dist/client/guided-questions.js.map +1 -1
  147. package/dist/collab/awareness.d.ts +2 -2
  148. package/dist/collab/awareness.d.ts.map +1 -1
  149. package/dist/db/client.d.ts +57 -10
  150. package/dist/db/client.d.ts.map +1 -1
  151. package/dist/db/client.js +122 -31
  152. package/dist/db/client.js.map +1 -1
  153. package/dist/db/create-get-db.d.ts.map +1 -1
  154. package/dist/db/create-get-db.js +31 -7
  155. package/dist/db/create-get-db.js.map +1 -1
  156. package/dist/db/runtime-diagnostics.d.ts +1 -0
  157. package/dist/db/runtime-diagnostics.d.ts.map +1 -1
  158. package/dist/db/runtime-diagnostics.js +31 -6
  159. package/dist/db/runtime-diagnostics.js.map +1 -1
  160. package/dist/deploy/build.d.ts +5 -20
  161. package/dist/deploy/build.d.ts.map +1 -1
  162. package/dist/deploy/build.js +18 -30
  163. package/dist/deploy/build.js.map +1 -1
  164. package/dist/feature-flags/store.d.ts.map +1 -1
  165. package/dist/feature-flags/store.js +10 -5
  166. package/dist/feature-flags/store.js.map +1 -1
  167. package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
  168. package/dist/integrations/a2a-continuations-store.js +12 -0
  169. package/dist/integrations/a2a-continuations-store.js.map +1 -1
  170. package/dist/integrations/config-store.d.ts +1 -0
  171. package/dist/integrations/config-store.d.ts.map +1 -1
  172. package/dist/integrations/config-store.js +11 -0
  173. package/dist/integrations/config-store.js.map +1 -1
  174. package/dist/integrations/google-docs-poller.d.ts.map +1 -1
  175. package/dist/integrations/google-docs-poller.js +19 -2
  176. package/dist/integrations/google-docs-poller.js.map +1 -1
  177. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  178. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  179. package/dist/mcp-client/routes.d.ts.map +1 -1
  180. package/dist/mcp-client/routes.js +30 -2
  181. package/dist/mcp-client/routes.js.map +1 -1
  182. package/dist/notifications/routes.d.ts +6 -6
  183. package/dist/observability/routes.d.ts +3 -3
  184. package/dist/onboarding/plugin.d.ts.map +1 -1
  185. package/dist/onboarding/plugin.js +12 -8
  186. package/dist/onboarding/plugin.js.map +1 -1
  187. package/dist/org/accept-pending.d.ts.map +1 -1
  188. package/dist/org/accept-pending.js +2 -0
  189. package/dist/org/accept-pending.js.map +1 -1
  190. package/dist/org/auto-join-domain.d.ts.map +1 -1
  191. package/dist/org/auto-join-domain.js +2 -0
  192. package/dist/org/auto-join-domain.js.map +1 -1
  193. package/dist/org/context.d.ts.map +1 -1
  194. package/dist/org/context.js +29 -9
  195. package/dist/org/context.js.map +1 -1
  196. package/dist/org/handlers.d.ts.map +1 -1
  197. package/dist/org/handlers.js +5 -0
  198. package/dist/org/handlers.js.map +1 -1
  199. package/dist/org/request-org-cache.d.ts +16 -0
  200. package/dist/org/request-org-cache.d.ts.map +1 -0
  201. package/dist/org/request-org-cache.js +62 -0
  202. package/dist/org/request-org-cache.js.map +1 -0
  203. package/dist/progress/routes.d.ts +1 -1
  204. package/dist/resources/handlers.d.ts +1 -1
  205. package/dist/resources/store.d.ts.map +1 -1
  206. package/dist/resources/store.js +11 -9
  207. package/dist/resources/store.js.map +1 -1
  208. package/dist/scripts/runner.d.ts.map +1 -1
  209. package/dist/scripts/runner.js +3 -3
  210. package/dist/scripts/runner.js.map +1 -1
  211. package/dist/secrets/storage.d.ts.map +1 -1
  212. package/dist/secrets/storage.js +82 -5
  213. package/dist/secrets/storage.js.map +1 -1
  214. package/dist/server/action-change.d.ts +20 -0
  215. package/dist/server/action-change.d.ts.map +1 -1
  216. package/dist/server/action-change.js +29 -0
  217. package/dist/server/action-change.js.map +1 -1
  218. package/dist/server/action-routes.d.ts.map +1 -1
  219. package/dist/server/action-routes.js +7 -8
  220. package/dist/server/action-routes.js.map +1 -1
  221. package/dist/server/agent-teams.d.ts.map +1 -1
  222. package/dist/server/agent-teams.js +10 -2
  223. package/dist/server/agent-teams.js.map +1 -1
  224. package/dist/server/better-auth-instance.d.ts.map +1 -1
  225. package/dist/server/better-auth-instance.js +31 -14
  226. package/dist/server/better-auth-instance.js.map +1 -1
  227. package/dist/server/core-routes-plugin.d.ts +1 -0
  228. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  229. package/dist/server/core-routes-plugin.js +18 -4
  230. package/dist/server/core-routes-plugin.js.map +1 -1
  231. package/dist/server/credential-provider.d.ts +16 -0
  232. package/dist/server/credential-provider.d.ts.map +1 -1
  233. package/dist/server/credential-provider.js +49 -13
  234. package/dist/server/credential-provider.js.map +1 -1
  235. package/dist/server/http-response-telemetry.d.ts.map +1 -1
  236. package/dist/server/http-response-telemetry.js +1 -0
  237. package/dist/server/http-response-telemetry.js.map +1 -1
  238. package/dist/server/poll.d.ts +6 -0
  239. package/dist/server/poll.d.ts.map +1 -1
  240. package/dist/server/poll.js +84 -47
  241. package/dist/server/poll.js.map +1 -1
  242. package/dist/server/transcribe-voice.d.ts +1 -1
  243. package/dist/server/voice-providers-status.d.ts.map +1 -1
  244. package/dist/server/voice-providers-status.js +12 -3
  245. package/dist/server/voice-providers-status.js.map +1 -1
  246. package/docs/content/durable-background-runs.mdx +15 -6
  247. package/docs/design/durable-agent-runs.md +15 -11
  248. package/package.json +2 -2
  249. package/src/a2a/activity.ts +127 -10
  250. package/src/a2a/index.ts +3 -0
  251. package/src/agent/durable-background.ts +38 -28
  252. package/src/agent/harness/background.ts +12 -2
  253. package/src/agent/production-agent.ts +18 -15
  254. package/src/agent/run-store.ts +45 -3
  255. package/src/application-state/handlers.ts +45 -0
  256. package/src/application-state/index.ts +3 -0
  257. package/src/application-state/store.ts +28 -11
  258. package/src/audit/redact.ts +104 -18
  259. package/src/client/AgentPanel.tsx +2 -7
  260. package/src/client/MultiTabAssistantChat.tsx +3 -0
  261. package/src/client/RunStuckBanner.tsx +29 -6
  262. package/src/client/agent-chat.ts +4 -9
  263. package/src/client/application-state.ts +115 -12
  264. package/src/client/dynamic-suggestions.ts +4 -12
  265. package/src/client/guided-questions.tsx +12 -23
  266. package/src/db/client.ts +148 -28
  267. package/src/db/create-get-db.ts +39 -6
  268. package/src/db/runtime-diagnostics.ts +41 -6
  269. package/src/deploy/build.ts +26 -33
  270. package/src/feature-flags/store.ts +10 -5
  271. package/src/integrations/a2a-continuations-store.ts +11 -0
  272. package/src/integrations/config-store.ts +13 -0
  273. package/src/integrations/google-docs-poller.ts +26 -2
  274. package/src/mcp-client/routes.ts +33 -2
  275. package/src/onboarding/plugin.ts +31 -25
  276. package/src/org/accept-pending.ts +2 -0
  277. package/src/org/auto-join-domain.ts +2 -0
  278. package/src/org/context.ts +35 -10
  279. package/src/org/handlers.ts +6 -0
  280. package/src/org/request-org-cache.ts +71 -0
  281. package/src/resources/store.ts +10 -9
  282. package/src/scripts/runner.ts +6 -3
  283. package/src/secrets/storage.ts +97 -5
  284. package/src/server/action-change.ts +33 -0
  285. package/src/server/action-routes.ts +11 -9
  286. package/src/server/agent-teams.ts +10 -2
  287. package/src/server/better-auth-instance.ts +39 -12
  288. package/src/server/core-routes-plugin.ts +19 -2
  289. package/src/server/credential-provider.ts +57 -14
  290. package/src/server/http-response-telemetry.ts +1 -0
  291. package/src/server/poll.ts +95 -57
  292. package/src/server/voice-providers-status.ts +19 -4
@@ -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
- isDurableBackgroundFlagEnabled,
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
- * Reads the same env flag the runtime gate uses
2580
- * (`AGENT_CHAT_DURABLE_BACKGROUND`).
2581
- *
2582
- * DEFAULT-OFF (opt-in). It IS the runtime gate's flag parse
2583
- * (`isDurableBackgroundFlagEnabled`), so the two can no longer drift:
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 isDurableBackgroundFlagEnabled();
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 backgroundWarmPath = isDurableBackgroundEmitRequired()
2653
- ? JSON.stringify(AGENT_BACKGROUND_FUNCTION_URL_PATH)
2654
- : "null";
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
- emitSingleTemplateNetlifyKeepWarmFunction(cwd);
4152
-
4153
- // Durable background agent runs (default-OFF / opt-in; enable with a truthy
4154
- // AGENT_CHAT_DURABLE_BACKGROUND). Additive ONLY: emits a SECOND Netlify
4155
- // function whose name ends in `-background` re-exporting the same handler
4156
- // bundle, so the chat `_process-run` POST lands on Netlify's async (15-min)
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
- const stored = scope.orgId?.trim()
93
- ? ((await getOrgSetting(scope.orgId, settingKey(key))) ??
94
- (await getSetting(settingKey(key))))
95
- : await getSetting(settingKey(key));
96
- return normalizeFeatureFlagRules(stored);
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 { getIntegrationConfig, saveIntegrationConfig } from "./config-store.js";
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) return;
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
@@ -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 () => clearInterval(timer);
359
+ return () => {
360
+ clearInterval(timer);
361
+ getSettingsEmitter().off("settings", markDirty);
362
+ };
332
363
  }
333
364
 
334
365
  async function resolveContextForRequest(event: H3Event): Promise<{
@@ -81,30 +81,34 @@ async function serializeSteps(
81
81
  options: { preview?: boolean } = {},
82
82
  ): Promise<OnboardingStepStatus[]> {
83
83
  const steps = listOnboardingSteps();
84
- const out: OnboardingStepStatus[] = [];
85
- for (const step of steps) {
86
- let complete = false;
87
- if (!options.preview) {
88
- try {
89
- complete = (await step.isComplete(context)) === true;
90
- } catch {
91
- complete = false;
92
- }
93
- if (!complete) {
94
- complete = await hasOverride(context.sessionId, step.id);
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
- out.push({
98
- id: step.id,
99
- title: step.title,
100
- description: step.description,
101
- order: step.order,
102
- required: step.required ?? false,
103
- complete,
104
- methods: step.methods,
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 appStateGet(context.sessionId, DISMISSED_KEY);
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
@@ -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 loadActiveOrgSettingForEvent(event, email);
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 rows = await queryOrgMembers({
414
- sql: `SELECT org_id FROM org_members WHERE LOWER(email) = ?
415
- ${MEMBERSHIP_FALLBACK_ORDER_BY}`,
416
- args: [email.toLowerCase()],
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
- if (!rows?.length) return null;
419
- const ids = rows.map((r: any) => String(r.org_id));
420
- const activeOrgSetting = (await getUserSetting(
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
- )) as ActiveOrgSetting;
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 loadActiveOrgSettingForEvent(event, email);
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
 
@@ -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
+ }
@@ -1494,18 +1494,19 @@ export async function resourceEffectiveContext(
1494
1494
  ): Promise<EffectiveResourceContext> {
1495
1495
  await ensureTable();
1496
1496
 
1497
- const workspace = await resourceGetByPath(WORKSPACE_OWNER, path, {
1498
- ...options,
1499
- userEmail,
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
- : await resourceGetByPath(organizationOwner, path);
1506
- const legacyShared = await resourceGetByPath(SHARED_OWNER, path);
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"