@agent-native/core 0.131.5 → 0.131.6

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 (328) hide show
  1. package/corpus/README.md +3 -3
  2. package/corpus/core/CHANGELOG.md +63 -0
  3. package/corpus/core/docs/content/agent-native-toolkit.mdx +13 -6
  4. package/corpus/core/docs/content/cloneable-saas.mdx +5 -5
  5. package/corpus/core/docs/content/creating-templates.mdx +9 -0
  6. package/corpus/core/docs/content/deployment.mdx +14 -10
  7. package/corpus/core/docs/content/doctor.mdx +8 -1
  8. package/corpus/core/docs/content/drop-in-agent.mdx +1 -1
  9. package/corpus/core/docs/content/extensions.mdx +18 -18
  10. package/corpus/core/docs/content/faq.mdx +4 -4
  11. package/corpus/core/docs/content/key-concepts.mdx +30 -21
  12. package/corpus/core/docs/content/template-brain-developers.mdx +4 -1
  13. package/corpus/core/docs/content/template-mail-developers.mdx +4 -1
  14. package/corpus/core/docs/content/what-is-agent-native.mdx +6 -6
  15. package/corpus/core/package.json +1 -1
  16. package/corpus/core/src/a2a/client.ts +449 -66
  17. package/corpus/core/src/a2a/correlation.ts +31 -0
  18. package/corpus/core/src/a2a/types.ts +4 -0
  19. package/corpus/core/src/action.ts +3 -0
  20. package/corpus/core/src/agent/engine/registry.ts +45 -5
  21. package/corpus/core/src/agent/production-agent.ts +151 -18
  22. package/corpus/core/src/agent/run-loop-with-resume.ts +190 -3
  23. package/corpus/core/src/agent/run-manager.ts +117 -41
  24. package/corpus/core/src/agent/thread-debug-history.ts +86 -0
  25. package/corpus/core/src/agent/types.ts +3 -1
  26. package/corpus/core/src/cli/create.ts +11 -1
  27. package/corpus/core/src/cli/templates-meta.ts +2 -2
  28. package/corpus/core/src/client/agent-chat-adapter.ts +91 -13
  29. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +11 -9
  30. package/corpus/core/src/client/sse-event-processor.ts +17 -3
  31. package/corpus/core/src/db/client.ts +57 -17
  32. package/corpus/core/src/integrations/adapters/slack.ts +8 -3
  33. package/corpus/core/src/localization/default-messages.ts +44 -0
  34. package/corpus/core/src/observability/traces.ts +78 -5
  35. package/corpus/core/src/scripts/call-agent.ts +290 -9
  36. package/corpus/core/src/scripts/describe-workspace-apps.ts +2 -2
  37. package/corpus/core/src/secrets/crypto.ts +126 -34
  38. package/corpus/core/src/secrets/storage.ts +61 -25
  39. package/corpus/core/src/server/agent-capabilities.ts +1 -1
  40. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +111 -7
  41. package/corpus/core/src/server/agent-chat/context-tools.ts +2 -0
  42. package/corpus/core/src/server/agent-chat/plugin-options.ts +34 -1
  43. package/corpus/core/src/server/agent-chat/prompt-resources.ts +1 -1
  44. package/corpus/core/src/server/agent-chat-plugin.ts +91 -59
  45. package/corpus/core/src/templates/default/.agents/skills/secrets/SKILL.md +14 -7
  46. package/corpus/core/src/templates/headless/.agents/skills/secrets/SKILL.md +14 -7
  47. package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +18 -7
  48. package/corpus/core/src/templates/workspace-core/.agents/skills/composable-mini-apps/SKILL.md +15 -14
  49. package/corpus/core/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +14 -7
  50. package/corpus/core/src/vite/client.ts +1 -0
  51. package/corpus/templates/analytics/.agents/skills/dashboard-ops/SKILL.md +5 -4
  52. package/corpus/templates/analytics/.agents/skills/gong/SKILL.md +6 -4
  53. package/corpus/templates/analytics/AGENTS.md +6 -4
  54. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +4 -3
  55. package/corpus/templates/analytics/changelog/2026-07-30-scheduled-dashboard-emails-complete-every-panel-without-pool.md +6 -0
  56. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +28 -1
  57. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +18 -11
  58. package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +4 -4
  59. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +9 -5
  60. package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +6 -2
  61. package/corpus/templates/analytics/server/lib/dashboard-panel-source-resolver.ts +11 -4
  62. package/corpus/templates/analytics/server/lib/dashboard-report-render.ts +49 -31
  63. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +16 -8
  64. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +9 -3
  65. package/corpus/templates/analytics/server/lib/production-serverless-runtime.ts +11 -0
  66. package/corpus/templates/analytics/server/plugins/agent-chat.ts +3 -2
  67. package/corpus/templates/analytics/server/plugins/analytics-alert-jobs.ts +2 -1
  68. package/corpus/templates/analytics/server/plugins/dashboard-report-jobs.ts +2 -1
  69. package/corpus/templates/analytics/server/plugins/uptime-monitor-jobs.ts +1 -12
  70. package/corpus/templates/brain/.agents/skills/brain/RUNBOOK.md +54 -6
  71. package/corpus/templates/brain/.agents/skills/brain/SKILL.md +20 -9
  72. package/corpus/templates/brain/.agents/skills/ingestion-and-connectors/SKILL.md +33 -1
  73. package/corpus/templates/brain/AGENTS.md +3 -1
  74. package/corpus/templates/brain/README.md +4 -1
  75. package/corpus/templates/brain/actions/_schemas.ts +30 -8
  76. package/corpus/templates/brain/actions/ask-brain.ts +124 -14
  77. package/corpus/templates/brain/actions/create-source.ts +18 -2
  78. package/corpus/templates/brain/actions/enqueue-captures-distillation.ts +8 -123
  79. package/corpus/templates/brain/actions/enqueue-distillation.ts +6 -127
  80. package/corpus/templates/brain/actions/import-capture.ts +17 -5
  81. package/corpus/templates/brain/actions/import-transcript.ts +17 -5
  82. package/corpus/templates/brain/actions/update-source.ts +25 -5
  83. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +1 -1
  84. package/corpus/templates/brain/changelog/2026-07-30-blessed-resources-can-feed-cited-company-answers.md +6 -0
  85. package/corpus/templates/brain/changelog/2026-07-30-the-left-sidebar-no-longer-shows-a-blank-organization-placeh.md +6 -0
  86. package/corpus/templates/brain/server/lib/brain.ts +49 -19
  87. package/corpus/templates/brain/server/lib/distillation-queue.ts +147 -0
  88. package/corpus/templates/brain/server/lib/source-policy.ts +264 -0
  89. package/corpus/templates/brain/server/plugins/agent-chat.ts +3 -2
  90. package/corpus/templates/brain/server/routes/api/_agent-native/brain/ingest.post.ts +116 -19
  91. package/corpus/templates/clips/app/components/meetings/google-oauth-identity-notice.tsx +51 -0
  92. package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
  93. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +3 -1
  94. package/corpus/templates/clips/changelog/2026-07-30-google-calendar-warning.md +6 -0
  95. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +72 -8
  96. package/corpus/templates/content/changelog/2026-07-30-the-slash-command-hint-now-stays-on-only-the-active-empty-li.md +6 -0
  97. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +506 -11
  98. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +148 -17
  99. package/corpus/templates/design/app/components/design/canvas-interactions/design-canvas-interactions.ts +357 -0
  100. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +7 -466
  101. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +15 -311
  102. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +19 -997
  103. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +8 -1216
  104. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +31 -38
  105. package/corpus/templates/design/package.json +0 -1
  106. package/corpus/templates/dispatch/AGENTS.md +8 -0
  107. package/corpus/templates/dispatch/DEVELOPING.md +29 -0
  108. package/corpus/templates/dispatch/actions/view-screen.ts +34 -2
  109. package/corpus/templates/dispatch/app/i18n-data.ts +457 -0
  110. package/corpus/templates/dispatch/changelog/2026-07-30-delegated-dispatch-work-continues-reliably-in-the-background.md +6 -0
  111. package/corpus/templates/dispatch/changelog/2026-07-30-find-failed-runs-across-apps.md +6 -0
  112. package/corpus/templates/dispatch/netlify.toml +3 -0
  113. package/corpus/templates/slides/.agents/skills/analytics-data-for-decks/SKILL.md +8 -12
  114. package/corpus/templates/slides/AGENTS.md +4 -4
  115. package/corpus/templates/slides/actions/duplicate-deck.ts +19 -4
  116. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +2 -10
  117. package/corpus/templates/slides/app/components/deck/MermaidRenderer.tsx +12 -1
  118. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +30 -13
  119. package/corpus/templates/slides/app/components/editor/DeckEditorSkeleton.tsx +35 -0
  120. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +63 -56
  121. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +21 -76
  122. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1110 -292
  123. package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +53 -0
  124. package/corpus/templates/slides/app/components/editor/SlideStyleInspector.tsx +235 -18
  125. package/corpus/templates/slides/app/components/editor/SpeakerNotesPanel.tsx +4 -1
  126. package/corpus/templates/slides/app/components/editor/canvas-interactions/index.ts +11 -0
  127. package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +129 -0
  128. package/corpus/templates/slides/app/components/editor/selection-overlay-measurement.ts +70 -0
  129. package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +519 -65
  130. package/corpus/templates/slides/app/components/visual-editor/CanvasCommentPins.tsx +36 -426
  131. package/corpus/templates/slides/app/components/visual-editor/DrawOverlay.tsx +19 -458
  132. package/corpus/templates/slides/app/context/DeckContext.tsx +179 -72
  133. package/corpus/templates/slides/app/global.css +105 -0
  134. package/corpus/templates/slides/app/i18n/en-US.ts +8 -0
  135. package/corpus/templates/slides/app/lib/deck-editor-loading.ts +26 -0
  136. package/corpus/templates/slides/app/lib/mermaid-blocks.ts +27 -0
  137. package/corpus/templates/slides/app/pages/DeckEditor.tsx +71 -38
  138. package/corpus/templates/slides/app/pages/Index.tsx +98 -41
  139. package/corpus/templates/slides/changelog/2026-07-30-deck-loading-no-longer-flashes-an-unavailable-message.md +6 -0
  140. package/corpus/templates/slides/changelog/2026-07-30-duplicating-a-deck-from-the-deck-list-now-opens-the-copy-imm.md +6 -0
  141. package/corpus/templates/slides/changelog/2026-07-30-layout-overflow-warning-is-readable-and-dismissible.md +6 -0
  142. package/corpus/templates/slides/changelog/2026-07-30-shared-canvas-annotations.md +6 -0
  143. package/corpus/templates/slides/changelog/2026-07-30-slide-objects-can-be-moved-as-a-group-copied-and-pasted-with.md +6 -0
  144. package/corpus/templates/slides/changelog/2026-07-30-slide-text-editing-and-object-controls-now-match-google-slides.md +6 -0
  145. package/corpus/templates/slides/changelog/2026-07-30-style-panel-now-scrolls-with-speaker-notes-open-shows-the-sl.md +6 -0
  146. package/corpus/templates/slides/changelog/2026-07-30-top-toolbar-controls-now-use-consistent-sizing-with-undo-and.md +6 -0
  147. package/corpus/templates/slides/netlify.toml +3 -1
  148. package/corpus/templates/slides/package.json +0 -1
  149. package/corpus/templates/slides/server/lib/chat-attachments.ts +18 -0
  150. package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -0
  151. package/corpus/templates/slides/vite.config.ts +0 -1
  152. package/corpus/toolkit/CHANGELOG.md +13 -0
  153. package/corpus/toolkit/README.md +19 -0
  154. package/corpus/toolkit/agent-native.eject.json +38 -0
  155. package/corpus/toolkit/package.json +29 -1
  156. package/corpus/toolkit/src/canvas-annotations/CanvasCommentPins.tsx +861 -0
  157. package/corpus/toolkit/src/canvas-annotations/DrawOverlay.tsx +1233 -0
  158. package/corpus/toolkit/src/canvas-annotations/index.ts +15 -0
  159. package/corpus/toolkit/src/canvas-annotations/types.ts +14 -0
  160. package/corpus/toolkit/src/canvas-interactions/canvas-interactions.ts +933 -0
  161. package/corpus/toolkit/src/canvas-interactions/index.ts +67 -0
  162. package/corpus/toolkit/src/design-tweaks/scrub-input-utils.ts +311 -0
  163. package/corpus/toolkit/src/design-tweaks/scrub-input.tsx +491 -0
  164. package/corpus/toolkit/src/design-tweaks/visual-style-controls.tsx +26 -171
  165. package/corpus/toolkit/src/index.ts +1 -0
  166. package/dist/a2a/client.d.ts +23 -0
  167. package/dist/a2a/client.d.ts.map +1 -1
  168. package/dist/a2a/client.js +309 -44
  169. package/dist/a2a/client.js.map +1 -1
  170. package/dist/a2a/correlation.d.ts +1 -0
  171. package/dist/a2a/correlation.d.ts.map +1 -1
  172. package/dist/a2a/correlation.js +27 -0
  173. package/dist/a2a/correlation.js.map +1 -1
  174. package/dist/a2a/types.d.ts +4 -0
  175. package/dist/a2a/types.d.ts.map +1 -1
  176. package/dist/a2a/types.js.map +1 -1
  177. package/dist/action.d.ts +3 -0
  178. package/dist/action.d.ts.map +1 -1
  179. package/dist/action.js.map +1 -1
  180. package/dist/agent/engine/registry.d.ts.map +1 -1
  181. package/dist/agent/engine/registry.js +34 -7
  182. package/dist/agent/engine/registry.js.map +1 -1
  183. package/dist/agent/production-agent.d.ts +40 -1
  184. package/dist/agent/production-agent.d.ts.map +1 -1
  185. package/dist/agent/production-agent.js +113 -15
  186. package/dist/agent/production-agent.js.map +1 -1
  187. package/dist/agent/run-loop-with-resume.d.ts +21 -0
  188. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  189. package/dist/agent/run-loop-with-resume.js +163 -4
  190. package/dist/agent/run-loop-with-resume.js.map +1 -1
  191. package/dist/agent/run-manager.d.ts +16 -9
  192. package/dist/agent/run-manager.d.ts.map +1 -1
  193. package/dist/agent/run-manager.js +93 -42
  194. package/dist/agent/run-manager.js.map +1 -1
  195. package/dist/agent/thread-debug-history.d.ts +10 -0
  196. package/dist/agent/thread-debug-history.d.ts.map +1 -0
  197. package/dist/agent/thread-debug-history.js +68 -0
  198. package/dist/agent/thread-debug-history.js.map +1 -0
  199. package/dist/agent/types.d.ts +3 -1
  200. package/dist/agent/types.d.ts.map +1 -1
  201. package/dist/agent/types.js.map +1 -1
  202. package/dist/cli/create.d.ts.map +1 -1
  203. package/dist/cli/create.js +6 -1
  204. package/dist/cli/create.js.map +1 -1
  205. package/dist/cli/templates-meta.js +2 -2
  206. package/dist/cli/templates-meta.js.map +1 -1
  207. package/dist/client/agent-chat-adapter.d.ts +1 -1
  208. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  209. package/dist/client/agent-chat-adapter.js +76 -13
  210. package/dist/client/agent-chat-adapter.js.map +1 -1
  211. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  212. package/dist/client/resources/McpIntegrationDialog.js +1 -1
  213. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  214. package/dist/client/sse-event-processor.d.ts.map +1 -1
  215. package/dist/client/sse-event-processor.js +12 -3
  216. package/dist/client/sse-event-processor.js.map +1 -1
  217. package/dist/collab/routes.d.ts +1 -1
  218. package/dist/collab/struct-routes.d.ts +1 -1
  219. package/dist/db/client.d.ts.map +1 -1
  220. package/dist/db/client.js +48 -17
  221. package/dist/db/client.js.map +1 -1
  222. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  223. package/dist/integrations/adapters/slack.js +7 -3
  224. package/dist/integrations/adapters/slack.js.map +1 -1
  225. package/dist/localization/default-messages.d.ts +43 -0
  226. package/dist/localization/default-messages.d.ts.map +1 -1
  227. package/dist/localization/default-messages.js +43 -0
  228. package/dist/localization/default-messages.js.map +1 -1
  229. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  230. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  231. package/dist/notifications/routes.d.ts +6 -6
  232. package/dist/observability/traces.d.ts +3 -1
  233. package/dist/observability/traces.d.ts.map +1 -1
  234. package/dist/observability/traces.js +73 -4
  235. package/dist/observability/traces.js.map +1 -1
  236. package/dist/progress/routes.d.ts +1 -1
  237. package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
  238. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  239. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  240. package/dist/scripts/call-agent.d.ts.map +1 -1
  241. package/dist/scripts/call-agent.js +233 -10
  242. package/dist/scripts/call-agent.js.map +1 -1
  243. package/dist/scripts/describe-workspace-apps.js +2 -2
  244. package/dist/scripts/describe-workspace-apps.js.map +1 -1
  245. package/dist/secrets/crypto.d.ts +31 -21
  246. package/dist/secrets/crypto.d.ts.map +1 -1
  247. package/dist/secrets/crypto.js +108 -33
  248. package/dist/secrets/crypto.js.map +1 -1
  249. package/dist/secrets/storage.d.ts +3 -5
  250. package/dist/secrets/storage.d.ts.map +1 -1
  251. package/dist/secrets/storage.js +40 -25
  252. package/dist/secrets/storage.js.map +1 -1
  253. package/dist/server/agent-capabilities.js +1 -1
  254. package/dist/server/agent-capabilities.js.map +1 -1
  255. package/dist/server/agent-chat/action-filters-a2a.d.ts +17 -3
  256. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  257. package/dist/server/agent-chat/action-filters-a2a.js +92 -3
  258. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  259. package/dist/server/agent-chat/context-tools.d.ts.map +1 -1
  260. package/dist/server/agent-chat/context-tools.js +2 -0
  261. package/dist/server/agent-chat/context-tools.js.map +1 -1
  262. package/dist/server/agent-chat/plugin-options.d.ts +23 -0
  263. package/dist/server/agent-chat/plugin-options.d.ts.map +1 -1
  264. package/dist/server/agent-chat/plugin-options.js +4 -1
  265. package/dist/server/agent-chat/plugin-options.js.map +1 -1
  266. package/dist/server/agent-chat/prompt-resources.js +1 -1
  267. package/dist/server/agent-chat/prompt-resources.js.map +1 -1
  268. package/dist/server/agent-chat-plugin.d.ts +2 -1
  269. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  270. package/dist/server/agent-chat-plugin.js +79 -56
  271. package/dist/server/agent-chat-plugin.js.map +1 -1
  272. package/dist/server/realtime-token.d.ts +1 -1
  273. package/dist/templates/default/.agents/skills/secrets/SKILL.md +14 -7
  274. package/dist/templates/headless/.agents/skills/secrets/SKILL.md +14 -7
  275. package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +18 -7
  276. package/dist/templates/workspace-core/.agents/skills/composable-mini-apps/SKILL.md +15 -14
  277. package/dist/templates/workspace-core/.agents/skills/secrets/SKILL.md +14 -7
  278. package/dist/vite/client.d.ts.map +1 -1
  279. package/dist/vite/client.js +1 -0
  280. package/dist/vite/client.js.map +1 -1
  281. package/docs/content/agent-native-toolkit.mdx +13 -6
  282. package/docs/content/cloneable-saas.mdx +5 -5
  283. package/docs/content/creating-templates.mdx +9 -0
  284. package/docs/content/deployment.mdx +14 -10
  285. package/docs/content/doctor.mdx +8 -1
  286. package/docs/content/drop-in-agent.mdx +1 -1
  287. package/docs/content/extensions.mdx +18 -18
  288. package/docs/content/faq.mdx +4 -4
  289. package/docs/content/key-concepts.mdx +30 -21
  290. package/docs/content/template-brain-developers.mdx +4 -1
  291. package/docs/content/template-mail-developers.mdx +4 -1
  292. package/docs/content/what-is-agent-native.mdx +6 -6
  293. package/package.json +2 -2
  294. package/src/a2a/client.ts +449 -66
  295. package/src/a2a/correlation.ts +31 -0
  296. package/src/a2a/types.ts +4 -0
  297. package/src/action.ts +3 -0
  298. package/src/agent/engine/registry.ts +45 -5
  299. package/src/agent/production-agent.ts +151 -18
  300. package/src/agent/run-loop-with-resume.ts +190 -3
  301. package/src/agent/run-manager.ts +117 -41
  302. package/src/agent/thread-debug-history.ts +86 -0
  303. package/src/agent/types.ts +3 -1
  304. package/src/cli/create.ts +11 -1
  305. package/src/cli/templates-meta.ts +2 -2
  306. package/src/client/agent-chat-adapter.ts +91 -13
  307. package/src/client/resources/McpIntegrationDialog.tsx +11 -9
  308. package/src/client/sse-event-processor.ts +17 -3
  309. package/src/db/client.ts +57 -17
  310. package/src/integrations/adapters/slack.ts +8 -3
  311. package/src/localization/default-messages.ts +44 -0
  312. package/src/observability/traces.ts +78 -5
  313. package/src/scripts/call-agent.ts +290 -9
  314. package/src/scripts/describe-workspace-apps.ts +2 -2
  315. package/src/secrets/crypto.ts +126 -34
  316. package/src/secrets/storage.ts +61 -25
  317. package/src/server/agent-capabilities.ts +1 -1
  318. package/src/server/agent-chat/action-filters-a2a.ts +111 -7
  319. package/src/server/agent-chat/context-tools.ts +2 -0
  320. package/src/server/agent-chat/plugin-options.ts +34 -1
  321. package/src/server/agent-chat/prompt-resources.ts +1 -1
  322. package/src/server/agent-chat-plugin.ts +91 -59
  323. package/src/templates/default/.agents/skills/secrets/SKILL.md +14 -7
  324. package/src/templates/headless/.agents/skills/secrets/SKILL.md +14 -7
  325. package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +18 -7
  326. package/src/templates/workspace-core/.agents/skills/composable-mini-apps/SKILL.md +15 -14
  327. package/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +14 -7
  328. package/src/vite/client.ts +1 -0
@@ -45,8 +45,9 @@ change the numbers.
45
45
  - A sibling app asking over A2A sends a question or shaped input, never SQL. It
46
46
  lacks this app's schema, data dictionary, dashboards, and dialect knowledge,
47
47
  so raw-query actions (`sql`, `code`, `script`, `expression`) are not
48
- sibling-invocable. For recurring numbers, add a shaped read action with
49
- semantic parameters and let this app own the query.
48
+ sibling-invocable. Prefer natural-language delegation so this app retains its
49
+ instructions, source selection, and tools. Shaped reads are stable contracts,
50
+ not workarounds for delegation; this app still owns the query.
50
51
  - Data integrity first. Never invent numbers, dimensions, filters, or source
51
52
  semantics; only present values you actually retrieved, and state uncertainty.
52
53
  - Every analytical answer carries audit context: source(s), time window,
@@ -70,8 +71,9 @@ change the numbers.
70
71
  only URLs, ids, or handles.
71
72
  - Never hardcode API keys, tokens, webhook URLs, secrets, private Builder data,
72
73
  or customer data. Use secrets/OAuth and obvious placeholders in examples.
73
- - External MCP callers default to `ask_app` for multi-step work; fallback reads
74
- are bounded and scoped, and writes stay `ask_app`-only.
74
+ - External MCP callers default to `ask_app` for interpretation, source choice,
75
+ analysis, or multi-step work. Direct reads require exact, complete input;
76
+ writes stay `ask_app`-only.
75
77
  - Dashboard email reports and analytics alert rules are SQL-backed,
76
78
  self-describing action surfaces — don't hand-wire routes around them. Reports
77
79
  cap at five recipients. See `dashboard-ops`.
@@ -33,9 +33,10 @@ export default defineAction({
33
33
  ),
34
34
  }),
35
35
  readOnly: true,
36
- // No raw HTTP route: a GET endpoint would put caller SQL in query strings
37
- // and access logs. External MCP callers reach this through the explicit
38
- // ANALYTICS_CONNECTOR_CATALOG entry, which dispatches run() in-process.
36
+ // No raw HTTP or connector-catalog route: SQL would land in query strings,
37
+ // logs, or a caller that lacks this app's schema and data dictionary. This
38
+ // remains an internal Analytics-agent tool; sibling agents ask Analytics a
39
+ // natural-language question and Analytics forms the query.
39
40
  http: false,
40
41
  publicAgent: { expose: true, readOnly: true, requiresAuth: true },
41
42
  run: async (args) => {
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-30
4
+ ---
5
+
6
+ Scheduled dashboard emails complete every panel without pooled query timeout leaks.
@@ -81,6 +81,7 @@ globalThis.__AGENT_NATIVE_DASHBOARD_REPORT_SCHEDULED_RUNTIME__ = true;
81
81
 
82
82
  const CRON_TOKEN = ${JSON.stringify(token)};
83
83
  const ROUTE_PATH = ${JSON.stringify(ROUTE_PATH)};
84
+ const WORKER_PATH = "/.netlify/functions/${WORKER_NAME}";
84
85
  let cachedHandler;
85
86
 
86
87
  function timingSafeEquals(a, b) {
@@ -92,6 +93,27 @@ function timingSafeEquals(a, b) {
92
93
  return diff === 0;
93
94
  }
94
95
 
96
+ async function triggerNextSweep(request) {
97
+ const url = new URL(request.url);
98
+ url.pathname = WORKER_PATH;
99
+ url.search = "";
100
+ const response = await fetch(url.toString(), {
101
+ method: "POST",
102
+ headers: {
103
+ "content-type": "application/json",
104
+ "x-agent-native-dashboard-report-cron": CRON_TOKEN,
105
+ },
106
+ body: JSON.stringify({ scheduled: true }),
107
+ });
108
+ if (!response.ok && response.status !== 202) {
109
+ console.error(
110
+ "[dashboard-report-cron] Follow-up sweep trigger failed:",
111
+ response.status,
112
+ await response.text().catch(() => ""),
113
+ );
114
+ }
115
+ }
116
+
95
117
  export default async function handler(request, context) {
96
118
  const token = request.headers.get("x-agent-native-dashboard-report-cron") || "";
97
119
  if (!timingSafeEquals(token, CRON_TOKEN)) {
@@ -109,7 +131,12 @@ export default async function handler(request, context) {
109
131
  body: JSON.stringify({ scheduled: true }),
110
132
  });
111
133
 
112
- return await cachedHandler(rewritten, context);
134
+ const response = await cachedHandler(rewritten, context);
135
+ const result = await response.clone().json().catch(() => null);
136
+ if (response.ok && result?.remaining > 0) {
137
+ await triggerNextSweep(request);
138
+ }
139
+ return response;
113
140
  }
114
141
 
115
142
  export const config = {
@@ -8,15 +8,21 @@ import {
8
8
  recordDashboardReportCaptureOutcome,
9
9
  } from "../lib/dashboard-report-subscriptions";
10
10
 
11
+ declare global {
12
+ var __AGENT_NATIVE_DASHBOARD_REPORT_SCHEDULED_RUNTIME__: boolean | undefined;
13
+ }
14
+
11
15
  let running = false;
12
16
  const DEFAULT_MAX_REPORTS_PER_SWEEP = 5;
13
17
  const SERVERLESS_REPORT_DELIVERY_BUDGET_MS = 220_000;
14
- /**
15
- * Reports render from SQL in seconds now, so one sweep can drain the whole
16
- * batch. When capture still drove a headless browser this was forced to 1 and
17
- * every subscription after the first missed its same-day retry window.
18
- */
19
- const SERVERLESS_MAX_REPORTS_PER_SWEEP = 5;
18
+ const SERVERLESS_MAX_REPORTS_PER_SWEEP = 1;
19
+
20
+ function serverlessDashboardReportRuntime(): boolean {
21
+ return (
22
+ process.env.NETLIFY === "true" ||
23
+ globalThis.__AGENT_NATIVE_DASHBOARD_REPORT_SCHEDULED_RUNTIME__ === true
24
+ );
25
+ }
20
26
 
21
27
  async function persistDashboardReportResult(
22
28
  ...args: Parameters<typeof markDashboardReportResult>
@@ -52,7 +58,9 @@ async function persistDashboardReportCaptureOutcome(
52
58
  }
53
59
 
54
60
  function maxReportsPerSweep(): number {
55
- if (process.env.NETLIFY === "true") return SERVERLESS_MAX_REPORTS_PER_SWEEP;
61
+ if (serverlessDashboardReportRuntime()) {
62
+ return SERVERLESS_MAX_REPORTS_PER_SWEEP;
63
+ }
56
64
  const raw = process.env.DASHBOARD_REPORT_SWEEP_LIMIT?.trim();
57
65
  if (!raw) return DEFAULT_MAX_REPORTS_PER_SWEEP;
58
66
  const parsed = Number.parseInt(raw, 10);
@@ -72,10 +80,6 @@ export async function runDashboardReportsOnce(): Promise<{
72
80
  }> {
73
81
  if (running) return { processed: 0, failed: 0, remaining: 0 };
74
82
  running = true;
75
- const deliveryDeadlineAt =
76
- process.env.NETLIFY === "true"
77
- ? Date.now() + SERVERLESS_REPORT_DELIVERY_BUDGET_MS
78
- : undefined;
79
83
  let processed = 0;
80
84
  let failed = 0;
81
85
  let remaining = 0;
@@ -86,6 +90,9 @@ export async function runDashboardReportsOnce(): Promise<{
86
90
  remaining = batch.length >= sweepLimit ? 1 : 0;
87
91
  for (const sub of batch) {
88
92
  processed++;
93
+ const deliveryDeadlineAt = serverlessDashboardReportRuntime()
94
+ ? Date.now() + SERVERLESS_REPORT_DELIVERY_BUDGET_MS
95
+ : undefined;
89
96
  const retryAt = dashboardReportRetryAt(sub);
90
97
  try {
91
98
  const result = await runWithRequestContext(
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * The deliberately small authenticated MCP surface for Analytics.
3
3
  *
4
- * Keep this list read-only and bounded. It includes incident evidence plus
5
- * the narrow Gong deep-dive/synthesis/evidence operations used by sibling
6
- * apps. Do not add raw replay blobs or dashboard/data mutation actions here.
4
+ * Keep this list read-only and bounded. It includes optional semantic read
5
+ * contracts for callers that already know the exact operation and complete
6
+ * input. Natural-language ask_app/A2A remains the default. Do not add raw SQL,
7
+ * replay blobs, or dashboard/data mutation actions here.
7
8
  */
8
9
  export const ANALYTICS_CONNECTOR_CATALOG = [
9
10
  "account-deep-dive",
@@ -14,7 +15,6 @@ export const ANALYTICS_CONNECTOR_CATALOG = [
14
15
  "list-session-recordings",
15
16
  "get-session-replay-summary",
16
17
  "get-session-replay-timeline",
17
- "query-agent-native-analytics",
18
18
  "list-error-issues",
19
19
  "get-error-issue",
20
20
  ] as const;
@@ -9,14 +9,17 @@ import {
9
9
  export const LEGACY_NEW_VS_RECURRING_USERS_SQL = `WITH all_users AS (SELECT NULLIF(user_key, '') AS user_key, event_date, user_id FROM analytics_events WHERE event_name = 'session status' AND signed_in = 'true' AND NULLIF(user_key, '') IS NOT NULL AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), 'unknown')) <> 'docs' AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io'))), first_seen AS (SELECT user_key, MIN(event_date) AS first_date FROM all_users GROUP BY user_key), daily AS (SELECT a.event_date AS date, CASE WHEN a.event_date = f.first_date THEN 'New' ELSE 'Recurring' END AS user_type, COUNT(DISTINCT a.user_key) AS users FROM all_users a JOIN first_seen f ON f.user_key = a.user_key WHERE ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))) GROUP BY 1, 2) SELECT date, user_type, users FROM daily ORDER BY date, CASE WHEN user_type = 'Recurring' THEN 0 ELSE 1 END`;
10
10
  const LEGACY_NEW_VS_RECURRING_USERS_DESCRIPTION =
11
11
  "Daily signed-in visitors split by first-ever session (New) vs return visit (Recurring), stacked with Recurring on the bottom and New on top. Docs excluded. A user is New only on their all-time first active day.";
12
- const NEW_VS_RECURRING_USERS_SQL = `WITH first_seen AS (SELECT NULLIF(user_key, '') AS user_key, MIN(event_date) AS first_date FROM analytics_events WHERE event_name = 'session status' AND signed_in = 'true' AND NULLIF(user_key, '') IS NOT NULL AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), 'unknown')) <> 'docs' AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io')) AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') GROUP BY 1), activity AS (SELECT NULLIF(user_key, '') AS user_key, event_date FROM analytics_events WHERE event_name = 'session status' AND signed_in = 'true' AND NULLIF(user_key, '') IS NOT NULL AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), 'unknown')) <> 'docs' AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io')) AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))), daily AS (SELECT a.event_date AS date, CASE WHEN a.event_date = f.first_date THEN 'New' ELSE 'Recurring' END AS user_type, COUNT(DISTINCT a.user_key) AS users FROM activity a JOIN first_seen f ON f.user_key = a.user_key GROUP BY 1, 2) SELECT date, user_type, users FROM daily ORDER BY date, CASE WHEN user_type = 'Recurring' THEN 0 ELSE 1 END`;
12
+ const BOUNDED_NEW_VS_RECURRING_USERS_SQL = `WITH first_seen AS (SELECT NULLIF(user_key, '') AS user_key, MIN(event_date) AS first_date FROM analytics_events WHERE event_name = 'session status' AND signed_in = 'true' AND NULLIF(user_key, '') IS NOT NULL AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), 'unknown')) <> 'docs' AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io')) AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') GROUP BY 1), activity AS (SELECT NULLIF(user_key, '') AS user_key, event_date FROM analytics_events WHERE event_name = 'session status' AND signed_in = 'true' AND NULLIF(user_key, '') IS NOT NULL AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), 'unknown')) <> 'docs' AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io')) AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))), daily AS (SELECT a.event_date AS date, CASE WHEN a.event_date = f.first_date THEN 'New' ELSE 'Recurring' END AS user_type, COUNT(DISTINCT a.user_key) AS users FROM activity a JOIN first_seen f ON f.user_key = a.user_key GROUP BY 1, 2) SELECT date, user_type, users FROM daily ORDER BY date, CASE WHEN user_type = 'Recurring' THEN 0 ELSE 1 END`;
13
13
  const MARKETING_SITE_TEMPLATE_FILTER =
14
14
  "lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), 'unknown')) <> 'www'";
15
- const NEW_VS_RECURRING_USERS_WITHOUT_MARKETING_SITE_SQL =
16
- NEW_VS_RECURRING_USERS_SQL.replace(
15
+ export const DEPLOYED_NEW_VS_RECURRING_USERS_SQL =
16
+ BOUNDED_NEW_VS_RECURRING_USERS_SQL.replace(
17
17
  " <> 'docs' AND ",
18
18
  ` <> 'docs' AND ${MARKETING_SITE_TEMPLATE_FILTER} AND `,
19
19
  );
20
+ // Keep cohort classification in one raw-table scan. Separate first-seen and
21
+ // activity scans double the random heap reads on the growing event table.
22
+ const NEW_VS_RECURRING_USERS_SQL = `WITH activity AS (SELECT NULLIF(user_key, '') AS user_key, event_date, MIN(event_date) OVER (PARTITION BY NULLIF(user_key, '')) AS first_date FROM analytics_events WHERE event_name = 'session status' AND signed_in = 'true' AND NULLIF(user_key, '') IS NOT NULL AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), 'unknown')) <> 'docs' AND ${MARKETING_SITE_TEMPLATE_FILTER} AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io')) AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')), daily AS (SELECT event_date AS date, CASE WHEN event_date = first_date THEN 'New' ELSE 'Recurring' END AS user_type, COUNT(DISTINCT user_key) AS users FROM activity WHERE ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))) GROUP BY 1, 2) SELECT date, user_type, users FROM daily ORDER BY date, CASE WHEN user_type = 'Recurring' THEN 0 ELSE 1 END`;
20
23
  const NEW_VS_RECURRING_USERS_DESCRIPTION =
21
24
  "Daily signed-in visitors split by first active day observed in the previous 365 days (New) vs return visit (Recurring), stacked with Recurring on the bottom and New on top. Docs and marketing-site traffic are excluded.";
22
25
 
@@ -34,9 +37,10 @@ const CANONICAL_CUSTOM_PANEL_REPLACEMENTS: readonly ExactFirstPartyPanelReplacem
34
37
  id: "new-vs-recurring-users",
35
38
  legacySql: [
36
39
  LEGACY_NEW_VS_RECURRING_USERS_SQL,
37
- NEW_VS_RECURRING_USERS_SQL,
40
+ BOUNDED_NEW_VS_RECURRING_USERS_SQL,
41
+ DEPLOYED_NEW_VS_RECURRING_USERS_SQL,
38
42
  ],
39
- sql: NEW_VS_RECURRING_USERS_WITHOUT_MARKETING_SITE_SQL,
43
+ sql: NEW_VS_RECURRING_USERS_SQL,
40
44
  legacyDescription: LEGACY_NEW_VS_RECURRING_USERS_DESCRIPTION,
41
45
  description: NEW_VS_RECURRING_USERS_DESCRIPTION,
42
46
  },
@@ -400,8 +400,9 @@ export async function runDashboardPanelQuery(args: {
400
400
  source: DashboardPanelSource;
401
401
  query: string;
402
402
  ctx: CredentialContext;
403
+ timeoutMs?: number;
403
404
  }): Promise<DashboardPanelQueryResult | MissingKeyResponse> {
404
- const { source, query, ctx } = args;
405
+ const { source, query, ctx, timeoutMs } = args;
405
406
 
406
407
  if (source === "bigquery") {
407
408
  const missing = await missingCredential(
@@ -452,7 +453,10 @@ export async function runDashboardPanelQuery(args: {
452
453
  userEmail: ctx.userEmail,
453
454
  orgId: ctx.orgId ?? null,
454
455
  },
455
- { cache: true },
456
+ {
457
+ cache: true,
458
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
459
+ },
456
460
  );
457
461
  }
458
462
 
@@ -19,17 +19,24 @@ type AnalyticsPanelSourceResolver = PanelSourceResolver<
19
19
  CredentialContext
20
20
  >;
21
21
 
22
+ type AnalyticsPanelSourceRequest = PanelSourceRequest<DashboardPanelSource> & {
23
+ timeoutMs?: number;
24
+ };
25
+
22
26
  function createResolver(
23
27
  source: DashboardPanelSource,
24
28
  ): AnalyticsPanelSourceResolver {
25
29
  return {
26
30
  source,
27
- resolve: async (request, context) =>
28
- (await runDashboardPanelQuery({
31
+ resolve: async (request, context) => {
32
+ const timeoutMs = (request as AnalyticsPanelSourceRequest).timeoutMs;
33
+ return (await runDashboardPanelQuery({
29
34
  source: request.source,
30
35
  query: request.query,
31
36
  ctx: context,
32
- })) as DashboardPanelQueryResult | MissingKeyResponse,
37
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
38
+ })) as DashboardPanelQueryResult | MissingKeyResponse;
39
+ },
33
40
  };
34
41
  }
35
42
 
@@ -42,7 +49,7 @@ const registry = createPanelSourceResolverRegistry<
42
49
  >({ resolvers: analyticsPanelSourceResolvers });
43
50
 
44
51
  export async function resolveAnalyticsPanelSource(
45
- request: PanelSourceRequest<DashboardPanelSource>,
52
+ request: AnalyticsPanelSourceRequest,
46
53
  context: CredentialContext,
47
54
  ): Promise<PanelSourceResult | MissingKeyResponse> {
48
55
  return registry.resolve(request, context) as Promise<
@@ -14,7 +14,10 @@ import type {
14
14
  TableColumnConfig,
15
15
  } from "../../app/pages/adhoc/sql-dashboard/types";
16
16
  import { DASHBOARD_REPORT_ACTION_TIMEOUT_MS } from "../../shared/dashboard-report-timeouts.js";
17
- import { MAX_CONCURRENT_SQL_QUERIES } from "../../shared/sql-query-limits";
17
+ import {
18
+ MAX_CONCURRENT_FIRST_PARTY_SQL_QUERIES,
19
+ MAX_CONCURRENT_SQL_QUERIES,
20
+ } from "../../shared/sql-query-limits";
18
21
  import {
19
22
  normalizeDashboardPanelQuery,
20
23
  type DashboardPanelSource,
@@ -186,7 +189,7 @@ async function fetchOnePanel(
186
189
 
187
190
  try {
188
191
  const result = await withTimeout(
189
- resolveAnalyticsPanelSource({ source, query }, ctx),
192
+ resolveAnalyticsPanelSource({ source, query, timeoutMs }, ctx),
190
193
  timeoutMs,
191
194
  `Panel query timed out after ${Math.round(timeoutMs / 1000)}s`,
192
195
  );
@@ -276,42 +279,57 @@ export async function fetchReportPanelData(args: {
276
279
  args.perPanelTimeoutMs ?? DEFAULT_PANEL_TIMEOUT_MS,
277
280
  );
278
281
 
279
- let next = 0;
280
- const worker = async (): Promise<void> => {
281
- while (next < queue.length) {
282
- const panel = queue[next++];
283
- const remaining = args.deadlineAt
284
- ? args.deadlineAt - Date.now()
285
- : Infinity;
286
- if (remaining <= 0) {
287
- results.set(panel.id, {
288
- status: "query-failed",
289
- message:
290
- "The report delivery deadline passed before this panel could run",
291
- });
292
- continue;
282
+ const runQueue = async (
283
+ panelQueue: SqlPanel[],
284
+ concurrency: number,
285
+ ): Promise<void> => {
286
+ let next = 0;
287
+ const worker = async (): Promise<void> => {
288
+ while (next < panelQueue.length) {
289
+ const panel = panelQueue[next++];
290
+ const remaining = args.deadlineAt
291
+ ? args.deadlineAt - Date.now()
292
+ : Infinity;
293
+ if (remaining <= 0) {
294
+ results.set(panel.id, {
295
+ status: "query-failed",
296
+ message:
297
+ "The report delivery deadline passed before this panel could run",
298
+ });
299
+ continue;
300
+ }
301
+ results.set(
302
+ panel.id,
303
+ await fetchOnePanel(
304
+ panel,
305
+ vars,
306
+ ctx,
307
+ Math.min(perPanelTimeoutMs, remaining),
308
+ ),
309
+ );
293
310
  }
294
- results.set(
295
- panel.id,
296
- await fetchOnePanel(
297
- panel,
298
- vars,
299
- ctx,
300
- Math.min(perPanelTimeoutMs, remaining),
301
- ),
302
- );
303
- }
311
+ };
312
+
313
+ await Promise.all(
314
+ Array.from({ length: Math.min(concurrency, panelQueue.length) }, () =>
315
+ worker(),
316
+ ),
317
+ );
304
318
  };
305
319
 
306
320
  await runWithRequestContext(
307
321
  { userEmail: ownerEmail, ...(args.orgId ? { orgId: args.orgId } : {}) },
308
322
  async () => {
309
- await Promise.all(
310
- Array.from(
311
- { length: Math.min(MAX_CONCURRENT_SQL_QUERIES, queue.length) },
312
- () => worker(),
323
+ await Promise.all([
324
+ runQueue(
325
+ queue.filter((panel) => panel.source === "first-party"),
326
+ MAX_CONCURRENT_FIRST_PARTY_SQL_QUERIES,
313
327
  ),
314
- );
328
+ runQueue(
329
+ queue.filter((panel) => panel.source !== "first-party"),
330
+ MAX_CONCURRENT_SQL_QUERIES,
331
+ ),
332
+ ]);
315
333
  },
316
334
  );
317
335
 
@@ -618,19 +618,27 @@ export function validateFirstPartyAnalyticsSql(sql: string): void {
618
618
  }
619
619
  }
620
620
 
621
- function scopeClause(scope: AnalyticsScope): {
621
+ function scopedTableSource(
622
+ tableName: string,
623
+ scope: AnalyticsScope,
624
+ today: string,
625
+ ): {
622
626
  sql: string;
623
627
  args: Array<string | null>;
624
628
  } {
629
+ const freshness = freshnessClause(tableName);
625
630
  if (scope.orgId) {
626
631
  return {
627
- sql: "(org_id = ? OR (org_id IS NULL AND owner_email = ?))",
628
- args: [scope.orgId, scope.userEmail],
632
+ // Keep the org and personal fallback as separate branches so Postgres can
633
+ // use each branch's composite tenant/date indexes instead of scanning one
634
+ // broad org index for an OR predicate.
635
+ sql: `(SELECT * FROM ${tableName} WHERE org_id = ? AND ${freshness} UNION ALL SELECT * FROM ${tableName} WHERE org_id IS NULL AND owner_email = ? AND ${freshness})`,
636
+ args: [scope.orgId, today, scope.userEmail, today],
629
637
  };
630
638
  }
631
639
  return {
632
- sql: "(org_id IS NULL AND owner_email = ?)",
633
- args: [scope.userEmail],
640
+ sql: `(SELECT * FROM ${tableName} WHERE org_id IS NULL AND owner_email = ? AND ${freshness})`,
641
+ args: [scope.userEmail, today],
634
642
  };
635
643
  }
636
644
 
@@ -661,9 +669,9 @@ export function scopedAnalyticsSql(
661
669
  !RESERVED_ALIAS_WORDS.has(normalizedAlias)
662
670
  ? aliasPart
663
671
  : ` AS ${normalizedTable}`;
664
- const scopeDef = scopeClause(scope);
665
- args.push(...scopeDef.args, today);
666
- return `${keyword} (SELECT * FROM ${normalizedTable} WHERE ${scopeDef.sql} AND ${freshnessClause(normalizedTable)})${usableAlias}`;
672
+ const scopedSource = scopedTableSource(normalizedTable, scope, today);
673
+ args.push(...scopedSource.args);
674
+ return `${keyword} ${scopedSource.sql}${usableAlias}`;
667
675
  },
668
676
  );
669
677
  return { sql: rewritten, args };
@@ -298,11 +298,12 @@ export const PRE_MARKETING_SITE_ONE_DAY_RETENTION_BY_TEMPLATE_SQL =
298
298
  `${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen`,
299
299
  `${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER}), ranked_first_seen`,
300
300
  );
301
- const ONE_DAY_RETENTION_BY_TEMPLATE_SQL =
301
+ export const MATERIALIZED_ONE_DAY_RETENTION_BY_TEMPLATE_SQL =
302
302
  PRE_MARKETING_SITE_ONE_DAY_RETENTION_BY_TEMPLATE_SQL.replace(
303
303
  KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER,
304
304
  `${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER}`,
305
305
  );
306
+ const ONE_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} GROUP BY 1, 2, 3), observed AS (SELECT user_key, event_date, FIRST_VALUE(template) OVER (PARTITION BY user_key ORDER BY event_date, template) AS starting_template, MIN(event_date) OVER (PARTITION BY user_key ORDER BY event_date, template) AS cohort_date FROM base), cohorts AS (SELECT user_key, starting_template AS template, cohort_date, MAX(CASE WHEN event_date > cohort_date AND event_date <= to_char(cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') THEN 1 ELSE 0 END) AS retained FROM observed WHERE cohort_date <= ${daysAgoSql(7)} AND ${dashboardTimeRangeFilter("cohort_date")} GROUP BY user_key, starting_template, cohort_date) SELECT template, SUM(retained) AS retained_users, COUNT(*) AS cohort_users, SUM(retained)::float / NULLIF(COUNT(*), 0) AS rate FROM cohorts GROUP BY template HAVING COUNT(*) >= ${PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE} ORDER BY rate DESC, cohort_users DESC, template`;
306
307
  export const PRE_MARKETING_SITE_SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL =
307
308
  LEGACY_SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL.replace(
308
309
  `${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen`,
@@ -314,17 +315,19 @@ const SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL =
314
315
  `${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER}`,
315
316
  );
316
317
  export const PRE_MARKETING_SITE_RECURRING_USERS_BY_TEMPLATE_SQL = `WITH first_seen AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, MIN(${EVENT_DATE_SQL}) AS first_date FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} GROUP BY 1), activity AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} AND ${DASHBOARD_TIME_RANGE_FILTER}) SELECT a.event_date AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM activity a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' GROUP BY 1, 2 ORDER BY date, template`;
317
- const RECURRING_USERS_BY_TEMPLATE_SQL =
318
+ export const DOUBLE_SCAN_RECURRING_USERS_BY_TEMPLATE_SQL =
318
319
  PRE_MARKETING_SITE_RECURRING_USERS_BY_TEMPLATE_SQL.replace(
319
320
  PRODUCT_ACTIVITY_TEMPLATE_FILTER,
320
321
  `${PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER}`,
321
322
  );
323
+ const RECURRING_USERS_BY_TEMPLATE_SQL = `WITH activity AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, ${TEMPLATE_EXPR} AS template, MIN(${EVENT_DATE_SQL}) OVER (PARTITION BY ${SIGNED_IN_ACTIVITY_KEY_SQL}) AS first_date FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER}) SELECT event_date AS date, template, COUNT(DISTINCT user_key) AS users FROM activity WHERE event_date <> first_date AND template <> 'unknown' AND ${DASHBOARD_TIME_RANGE_FILTER} GROUP BY 1, 2 ORDER BY date, template`;
322
324
  export const PRE_MARKETING_SITE_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL = `WITH first_seen AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, MIN(${EVENT_DATE_SQL}) AS first_date FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} GROUP BY 1), activity AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} AND ${DASHBOARD_TIME_RANGE_FILTER}) SELECT to_char(date_trunc('week', a.event_date::date), 'YYYY-MM-DD') AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM activity a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' GROUP BY 1, 2 ORDER BY date, template`;
323
- const RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL =
325
+ export const DOUBLE_SCAN_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL =
324
326
  PRE_MARKETING_SITE_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL.replace(
325
327
  PRODUCT_ACTIVITY_TEMPLATE_FILTER,
326
328
  `${PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER}`,
327
329
  );
330
+ const RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL = `WITH activity AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, ${TEMPLATE_EXPR} AS template, MIN(${EVENT_DATE_SQL}) OVER (PARTITION BY ${SIGNED_IN_ACTIVITY_KEY_SQL}) AS first_date FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER}) SELECT to_char(date_trunc('week', event_date::date), 'YYYY-MM-DD') AS date, template, COUNT(DISTINCT user_key) AS users FROM activity WHERE event_date <> first_date AND template <> 'unknown' AND ${DASHBOARD_TIME_RANGE_FILTER} GROUP BY 1, 2 ORDER BY date, template`;
328
331
  const LEGACY_RECURRING_USERS_DESCRIPTION =
329
332
  "Daily signed-in visitors who are NOT on their all-time first active day (Recurring only), stacked by inferred template/app used that day. Docs traffic and unknown template are excluded.";
330
333
  const LEGACY_RECURRING_USERS_WEEKLY_DESCRIPTION =
@@ -382,6 +385,7 @@ export function repairFirstPartyObservedRetentionPanels(
382
385
  LEGACY_ONE_DAY_RETENTION_BY_TEMPLATE_SQL,
383
386
  LEGACY_V0_ONE_DAY_RETENTION_BY_TEMPLATE_SQL,
384
387
  PRE_MARKETING_SITE_ONE_DAY_RETENTION_BY_TEMPLATE_SQL,
388
+ MATERIALIZED_ONE_DAY_RETENTION_BY_TEMPLATE_SQL,
385
389
  ],
386
390
  sql: ONE_DAY_RETENTION_BY_TEMPLATE_SQL,
387
391
  legacyDescription: LEGACY_ONE_DAY_RETENTION_BY_TEMPLATE_DESCRIPTION,
@@ -409,6 +413,7 @@ export function repairFirstPartyObservedRetentionPanels(
409
413
  INTERMEDIATE_RECURRING_USERS_BY_TEMPLATE_SQL,
410
414
  DEPLOYED_RECURRING_USERS_BY_TEMPLATE_SQL,
411
415
  PRE_MARKETING_SITE_RECURRING_USERS_BY_TEMPLATE_SQL,
416
+ DOUBLE_SCAN_RECURRING_USERS_BY_TEMPLATE_SQL,
412
417
  ],
413
418
  sql: RECURRING_USERS_BY_TEMPLATE_SQL,
414
419
  legacyDescription: LEGACY_RECURRING_USERS_DESCRIPTION,
@@ -422,6 +427,7 @@ export function repairFirstPartyObservedRetentionPanels(
422
427
  LEGACY_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL,
423
428
  INTERMEDIATE_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL,
424
429
  PRE_MARKETING_SITE_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL,
430
+ DOUBLE_SCAN_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL,
425
431
  ],
426
432
  sql: RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL,
427
433
  legacyDescription: LEGACY_RECURRING_USERS_WEEKLY_DESCRIPTION,
@@ -0,0 +1,11 @@
1
+ export function isProductionServerlessRuntime(): boolean {
2
+ if (process.env.NODE_ENV !== "production") return false;
3
+ return (
4
+ process.env.NETLIFY === "true" ||
5
+ Boolean(process.env.NETLIFY_FUNCTION_NAME) ||
6
+ Boolean(process.env.AWS_LAMBDA_FUNCTION_NAME) ||
7
+ Boolean(process.env.LAMBDA_TASK_ROOT) ||
8
+ process.env.AWS_EXECUTION_ENV?.startsWith("AWS_Lambda") === true ||
9
+ process.env.VERCEL === "1"
10
+ );
11
+ }
@@ -848,8 +848,9 @@ export default createAgentChatPlugin({
848
848
  connectorCatalog: [...ANALYTICS_CONNECTOR_CATALOG],
849
849
  externalAgents: {
850
850
  // Keep the direct MCP surface deliberately curated. External agents
851
- // should use ask_app for multi-step investigation; cataloged actions are
852
- // bounded fallback reads for callers that already know what they need.
851
+ // should use ask_app by default; cataloged actions are optional stable
852
+ // semantic reads for callers with an exact, fully known contract. They are
853
+ // never a fallback for slow or failed delegation.
853
854
  authenticatedReads: "off",
854
855
  writes: "ask_app_only",
855
856
  },
@@ -1,4 +1,5 @@
1
1
  import { runAnalyticsAlertsOnce } from "../jobs/analytics-alerts";
2
+ import { isProductionServerlessRuntime } from "../lib/production-serverless-runtime";
2
3
 
3
4
  const DEFAULT_INTERVAL_MS = 60_000;
4
5
  let skippingLogged = false;
@@ -9,7 +10,7 @@ declare global {
9
10
 
10
11
  function platformSchedulerOwnsAlerts(): boolean {
11
12
  return (
12
- process.env.NETLIFY === "true" ||
13
+ isProductionServerlessRuntime() ||
13
14
  globalThis.__AGENT_NATIVE_ANALYTICS_ALERT_SCHEDULED_RUNTIME__ === true
14
15
  );
15
16
  }
@@ -1,4 +1,5 @@
1
1
  import { runDashboardReportsOnce } from "../jobs/dashboard-report";
2
+ import { isProductionServerlessRuntime } from "../lib/production-serverless-runtime";
2
3
 
3
4
  const INTERVAL_MS = 60_000;
4
5
  let skippingLogged = false;
@@ -9,7 +10,7 @@ declare global {
9
10
 
10
11
  function platformSchedulerOwnsReports(): boolean {
11
12
  return (
12
- process.env.NETLIFY === "true" ||
13
+ isProductionServerlessRuntime() ||
13
14
  globalThis.__AGENT_NATIVE_DASHBOARD_REPORT_SCHEDULED_RUNTIME__ === true
14
15
  );
15
16
  }
@@ -8,6 +8,7 @@
8
8
  * the actions/UI have their schema even when in-process sweeps are disabled.
9
9
  */
10
10
  import { runDueMonitorsOnce } from "../jobs/uptime-monitors";
11
+ import { isProductionServerlessRuntime } from "../lib/production-serverless-runtime";
11
12
 
12
13
  const DEFAULT_INTERVAL_MS = 30_000;
13
14
  let skippingLogged = false;
@@ -16,18 +17,6 @@ declare global {
16
17
  var __AGENT_NATIVE_UPTIME_MONITOR_SCHEDULED_RUNTIME__: boolean | undefined;
17
18
  }
18
19
 
19
- function isProductionServerlessRuntime(): boolean {
20
- if (process.env.NODE_ENV !== "production") return false;
21
- return (
22
- process.env.NETLIFY === "true" ||
23
- Boolean(process.env.NETLIFY_FUNCTION_NAME) ||
24
- Boolean(process.env.AWS_LAMBDA_FUNCTION_NAME) ||
25
- Boolean(process.env.LAMBDA_TASK_ROOT) ||
26
- process.env.AWS_EXECUTION_ENV?.startsWith("AWS_Lambda") === true ||
27
- process.env.VERCEL === "1"
28
- );
29
- }
30
-
31
20
  function platformSchedulerOwnsMonitors(): boolean {
32
21
  return (
33
22
  isProductionServerlessRuntime() ||