@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
@@ -442,6 +442,12 @@ export class AppSyncState {
442
442
  private lastExtensionsUpdatedAt: string | number | undefined;
443
443
  private lastExtensionMarkerTs = 0;
444
444
  private lastActionMarkerTs = 0;
445
+ /**
446
+ * Set by the seed when a durable action marker predates this process, so the
447
+ * first `doCheckExternalDbChanges` reads the marker rows even though the
448
+ * MAX(updated_at) probe cannot distinguish that row from the seeded max.
449
+ */
450
+ private replayActionMarkerOnce = false;
445
451
 
446
452
  /**
447
453
  * Tracks the latest updated_at seen on the `__screen_refresh__` key.
@@ -1355,6 +1361,7 @@ export class AppSyncState {
1355
1361
  // performed by a separate action process. Do not baseline past an existing
1356
1362
  // marker on cold start, or the first poll after the action will miss it.
1357
1363
  this.lastActionMarkerTs = 0;
1364
+ this.replayActionMarkerOnce = actionMarkerTs > 0;
1358
1365
  this.lastScreenRefreshTs = refreshTs;
1359
1366
  this.lastScreenRefreshTsBySession.clear();
1360
1367
  for (const row of refreshResult.rows) {
@@ -1416,28 +1423,54 @@ export class AppSyncState {
1416
1423
  // concurrently to shave stacked latency; results are still processed in
1417
1424
  // the original sequential order, and conditional follow-up queries stay
1418
1425
  // sequential within their branch.
1426
+ //
1427
+ // `lastAppStateTs` is maintained as MAX(updated_at) over the whole
1428
+ // `application_state` table (seeded in `seedVersionFromDb`, advanced by
1429
+ // the scan below over every key, not just the ones that emit events).
1430
+ // The application-state row scan and screen/extension marker reads only
1431
+ // report rows strictly newer than a watermark that is itself <=
1432
+ // `lastAppStateTs`, so an unadvanced MAX proves those reads are no-ops.
1433
+ // The action marker keeps its own max probe because its watermark must
1434
+ // remain independent under cross-process clock skew.
1419
1435
  const [
1420
- appResult,
1436
+ appStateMaxTs,
1421
1437
  actionMarkerTs,
1422
- refreshResult,
1423
- extensionMarkerTs,
1424
1438
  settingsTs,
1425
1439
  extensionsMaxUpdatedAt,
1426
1440
  ] = await Promise.all([
1427
- db.execute({
1428
- sql: "SELECT session_id, key, updated_at FROM application_state WHERE updated_at > ? ORDER BY updated_at ASC",
1429
- args: [this.lastAppStateTs],
1430
- }),
1441
+ readMaxUpdatedAt(db, "application_state"),
1431
1442
  readActionMarkerMaxUpdatedAt(db),
1432
- db.execute({
1433
- sql: "SELECT session_id, updated_at, value FROM application_state WHERE key = ?",
1434
- args: [SCREEN_REFRESH_KEY],
1435
- }),
1436
- readExtensionMarkerMaxUpdatedAt(db),
1437
1443
  readMaxUpdatedAt(db, "settings"),
1438
1444
  readMaxUpdatedAtRaw(db, "tools"),
1439
1445
  ]);
1440
1446
 
1447
+ // The seed deliberately rewinds `lastActionMarkerTs` to 0 so a marker
1448
+ // written before this process booted still reaches the first poll. That
1449
+ // replay is invisible to the MAX probe (the row is already counted in the
1450
+ // seeded max), so it gets one forced full read.
1451
+ const replayActionMarker = this.replayActionMarkerOnce;
1452
+ this.replayActionMarkerOnce = false;
1453
+ const appStateChanged =
1454
+ appStateMaxTs > this.lastAppStateTs || replayActionMarker;
1455
+
1456
+ const [appResult, refreshResult, extensionMarkerTs] = appStateChanged
1457
+ ? await Promise.all([
1458
+ db.execute({
1459
+ sql: "SELECT session_id, key, updated_at FROM application_state WHERE updated_at > ? ORDER BY updated_at ASC",
1460
+ args: [this.lastAppStateTs],
1461
+ }),
1462
+ db.execute({
1463
+ sql: "SELECT session_id, updated_at, value FROM application_state WHERE key = ?",
1464
+ args: [SCREEN_REFRESH_KEY],
1465
+ }),
1466
+ readExtensionMarkerMaxUpdatedAt(db),
1467
+ ])
1468
+ : ([
1469
+ { rows: [] as Record<string, unknown>[] },
1470
+ null,
1471
+ this.lastExtensionMarkerTs,
1472
+ ] as const);
1473
+
1441
1474
  // Check application_state for external writes. Preserve the changed key so
1442
1475
  // clients can invalidate one-shot command queries (`navigate`, `__set_url__`)
1443
1476
  // only when those command rows actually change; noisy keys such as
@@ -1497,54 +1530,59 @@ export class AppSyncState {
1497
1530
  // tool writes to application_state under a well-known key; when its
1498
1531
  // updated_at bumps, emit a distinct event so the client invalidates
1499
1532
  // all queries (not just the ones matching its default queryKey prefix).
1500
- const refreshTs = refreshResult.rows.reduce(
1501
- (max, row) => Math.max(max, timestampValue(row.updated_at)),
1502
- 0,
1503
- );
1504
- if (!this.screenRefreshInitialized) {
1505
- this.lastScreenRefreshTs = refreshTs;
1506
- for (const row of refreshResult.rows) {
1507
- if (typeof row.session_id === "string") {
1508
- this.lastScreenRefreshTsBySession.set(
1509
- row.session_id,
1510
- timestampValue(row.updated_at),
1511
- );
1512
- }
1513
- }
1514
- this.screenRefreshInitialized = true;
1515
- } else if (refreshTs > this.lastScreenRefreshTs) {
1516
- // Emit a per-user event only for the session(s) whose row actually
1517
- // advanced, scoped with `owner` so canSeeChangeForUser delivers it only
1518
- // to that user — not every authenticated poller.
1519
- for (const row of refreshResult.rows) {
1520
- const owner =
1521
- typeof row.session_id === "string" ? row.session_id : undefined;
1522
- if (!owner) continue;
1523
- const rowTs = timestampValue(row.updated_at);
1524
- if (rowTs <= (this.lastScreenRefreshTsBySession.get(owner) ?? 0)) {
1525
- continue;
1533
+ // `refreshResult` is null exactly when the MAX probe proved no
1534
+ // application_state row moved, which is also exactly when this block
1535
+ // could not emit anything.
1536
+ if (refreshResult) {
1537
+ const refreshTs = refreshResult.rows.reduce(
1538
+ (max, row) => Math.max(max, timestampValue(row.updated_at)),
1539
+ 0,
1540
+ );
1541
+ if (!this.screenRefreshInitialized) {
1542
+ this.lastScreenRefreshTs = refreshTs;
1543
+ for (const row of refreshResult.rows) {
1544
+ if (typeof row.session_id === "string") {
1545
+ this.lastScreenRefreshTsBySession.set(
1546
+ row.session_id,
1547
+ timestampValue(row.updated_at),
1548
+ );
1549
+ }
1526
1550
  }
1527
- let scope: string | undefined;
1528
- try {
1529
- const raw = row.value;
1530
- if (typeof raw === "string") {
1531
- const parsed = JSON.parse(raw);
1532
- if (typeof parsed?.scope === "string") scope = parsed.scope;
1551
+ this.screenRefreshInitialized = true;
1552
+ } else if (refreshTs > this.lastScreenRefreshTs) {
1553
+ // Emit a per-user event only for the session(s) whose row actually
1554
+ // advanced, scoped with `owner` so canSeeChangeForUser delivers it only
1555
+ // to that user — not every authenticated poller.
1556
+ for (const row of refreshResult.rows) {
1557
+ const owner =
1558
+ typeof row.session_id === "string" ? row.session_id : undefined;
1559
+ if (!owner) continue;
1560
+ const rowTs = timestampValue(row.updated_at);
1561
+ if (rowTs <= (this.lastScreenRefreshTsBySession.get(owner) ?? 0)) {
1562
+ continue;
1533
1563
  }
1534
- } catch {}
1535
- this.recordChange(
1536
- {
1537
- source: "screen-refresh",
1538
- type: "change",
1539
- key: SCREEN_REFRESH_KEY,
1540
- owner,
1541
- ...(scope ? { scope } : {}),
1542
- },
1543
- { dedupeKey: `screen-refresh|${rowTs}` },
1544
- );
1545
- this.lastScreenRefreshTsBySession.set(owner, rowTs);
1564
+ let scope: string | undefined;
1565
+ try {
1566
+ const raw = row.value;
1567
+ if (typeof raw === "string") {
1568
+ const parsed = JSON.parse(raw);
1569
+ if (typeof parsed?.scope === "string") scope = parsed.scope;
1570
+ }
1571
+ } catch {}
1572
+ this.recordChange(
1573
+ {
1574
+ source: "screen-refresh",
1575
+ type: "change",
1576
+ key: SCREEN_REFRESH_KEY,
1577
+ owner,
1578
+ ...(scope ? { scope } : {}),
1579
+ },
1580
+ { dedupeKey: `screen-refresh|${rowTs}` },
1581
+ );
1582
+ this.lastScreenRefreshTsBySession.set(owner, rowTs);
1583
+ }
1584
+ this.lastScreenRefreshTs = refreshTs;
1546
1585
  }
1547
- this.lastScreenRefreshTs = refreshTs;
1548
1586
  }
1549
1587
 
1550
1588
  // Extension mutations write a durable marker row so delete and hide/unhide
@@ -22,6 +22,7 @@ import {
22
22
  import { getOrgContext } from "../org/context.js";
23
23
  import { getSession } from "./auth.js";
24
24
  import {
25
+ prefetchSecrets,
25
26
  resolveHasCompleteBuilderConnection,
26
27
  resolveSecret,
27
28
  } from "./credential-provider.js";
@@ -74,10 +75,14 @@ export function createVoiceProvidersStatusHandler() {
74
75
  async function hasKey(key: string): Promise<boolean> {
75
76
  try {
76
77
  if (key === "GOOGLE_APPLICATION_CREDENTIALS") {
77
- const resolved = await resolveGoogleRealtimeCredentials({
78
- userEmail: session?.email,
79
- orgId: orgCtx?.orgId ?? undefined,
80
- });
78
+ // Same identity, passed explicitly — the context is here only so this
79
+ // scope sweep shares the prefetched per-request memo below.
80
+ const resolved = await withRequestContext(() =>
81
+ resolveGoogleRealtimeCredentials({
82
+ userEmail: session?.email,
83
+ orgId: orgCtx?.orgId ?? undefined,
84
+ }),
85
+ );
81
86
  return typeof resolved === "string" && resolved.length > 0;
82
87
  }
83
88
  const resolved = await withRequestContext(() => resolveSecret(key));
@@ -87,6 +92,16 @@ export function createVoiceProvidersStatusHandler() {
87
92
  }
88
93
  }
89
94
 
95
+ // One read per scope for every key below, instead of one per key per scope.
96
+ await withRequestContext(() =>
97
+ prefetchSecrets([
98
+ "GEMINI_API_KEY",
99
+ "OPENAI_API_KEY",
100
+ "GROQ_API_KEY",
101
+ "GOOGLE_APPLICATION_CREDENTIALS",
102
+ ]),
103
+ );
104
+
90
105
  let builder = false;
91
106
  try {
92
107
  builder =
@@ -156,6 +156,43 @@ flattens into the baseline and reads as "no data."
156
156
  - Scheduled email reports render the same two scales, so a dual-axis panel is
157
157
  safe to put on a subscribed dashboard.
158
158
 
159
+ ## Reusable Native Dashboard Patterns
160
+
161
+ The recent extension-backed dashboards in Builder Analytics cluster into a few
162
+ repeatable compositions. Prefer these native panels, with a real SQL or Data
163
+ Program result behind each one, when creating a replacement or a new dashboard:
164
+
165
+ | Pattern | Native composition |
166
+ | --- | --- |
167
+ | Customer ROI / value realization | `metric` KPI cards, `line` or `area` trends, `table` detail, and `callout` or `section` panels for the business narrative |
168
+ | Account engagement / outreach | `metric` coverage and adoption cards, a daily `line` trend, `heatmap` or `table` segmentation, and `callout` alerts |
169
+ | GTM pipeline / cross-sell | `funnel` for ordered stages, `metric` totals, `bar` or `line` trends, and a `table` for account-level follow-up |
170
+ | Win/loss analysis | `section` groups with `metric`, `table`, `bar`, `callout`, and trend panels; use a Data Program for provider joins and evidence rows |
171
+
172
+ Funnel panels use `config.xKey` for the stage label and `config.yKey` for a
173
+ non-negative count or value. The renderer preserves the SQL row order, shows
174
+ each stage's share of the first stage, and shows the change from the previous
175
+ stage. Keep the intended stage order in SQL with `ORDER BY`.
176
+
177
+ When a dashboard is being migrated from an extension, create a new v2 copy,
178
+ bind its panels to the real provider schema or Data Programs, and compare it
179
+ with the original before retiring the extension-backed version. Do not invent
180
+ customer-specific SQL, provider joins, cached rows, or extension ids in a
181
+ catalog template. Existing dashboards remain readable while the native
182
+ replacement is validated. Bespoke interaction flows, arbitrary layouts, and
183
+ visualizations outside these contracts may remain Custom Blocks.
184
+
185
+ The source tree ships four provider-free v2 manifests in
186
+ `server/lib/native-v2-dashboards.ts`: Customer ROI, Account Engagement,
187
+ Cross-sell, and Win / Loss. They intentionally contain no customer names,
188
+ provider ids, SQL, cached rows, or guessed joins. After deployment, an
189
+ organization owner or admin provisions them with `ensure-native-v2-dashboards`
190
+ by supplying one real Data Program per binding key. The action validates the
191
+ stored program output contract, shares the programs with the organization,
192
+ creates deterministic `native-*-v2-*` dashboard copies, and preserves the
193
+ extension-backed originals and any existing v2 edits. Do not add these to the
194
+ root demo bootstrap or silently auto-bind them to guessed provider schemas.
195
+
159
196
  ## When To Use An Extension Instead
160
197
 
161
198
  Native Analytics dashboards are JSON configs rendered by the built-in dashboard
@@ -474,6 +511,7 @@ type PanelPatch = {
474
511
  | "table"
475
512
  | "pie"
476
513
  | "section"
514
+ | "funnel"
477
515
  | "heatmap"
478
516
  | "callout"
479
517
  | "extension";
@@ -49,7 +49,7 @@ type PanelPatch = {
49
49
  title?: string;
50
50
  sql?: string;
51
51
  source?: "bigquery" | "ga4" | "amplitude" | "first-party" | "demo" | "prometheus" | "program";
52
- chartType?: "line" | "area" | "bar" | "metric" | "table" | "pie" | "section" | "heatmap" | "callout" | "extension";
52
+ chartType?: "line" | "area" | "bar" | "metric" | "table" | "pie" | "funnel" | "section" | "heatmap" | "callout" | "extension";
53
53
  width?: number;
54
54
  columns?: number;
55
55
  tab?: string;
@@ -0,0 +1,299 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import { defineAction, embedApp } from "@agent-native/core";
4
+ import { getDataProgram } from "@agent-native/core/data-programs";
5
+ import {
6
+ buildDeepLink,
7
+ getRequestOrgId,
8
+ getRequestUserEmail,
9
+ } from "@agent-native/core/server";
10
+ import { resolveAccess } from "@agent-native/core/sharing";
11
+ import setResourceVisibilityAction from "@agent-native/core/sharing/actions/set-resource-visibility";
12
+ import { z } from "zod";
13
+
14
+ import { getDashboard, upsertDashboard } from "../server/lib/dashboards-store";
15
+ import { requireAnalyticsAdminContext } from "../server/lib/db-admin-connections";
16
+ import {
17
+ compileNativeV2Dashboard,
18
+ getNativeV2DashboardManifest,
19
+ nativeV2DashboardManifests,
20
+ NATIVE_V2_DASHBOARD_IDS,
21
+ type NativeV2Binding,
22
+ type NativeV2BindingKey,
23
+ type NativeV2DashboardId,
24
+ } from "../server/lib/native-v2-dashboards";
25
+
26
+ const ANALYTICS_APP_ID = "analytics";
27
+
28
+ const bindingSchema = z.object({
29
+ programId: z.string().min(1),
30
+ params: z.record(z.string(), z.unknown()).optional(),
31
+ });
32
+
33
+ function orgDashboardId(
34
+ orgId: string,
35
+ templateId: NativeV2DashboardId,
36
+ ): string {
37
+ const suffix = createHash("sha256").update(orgId).digest("hex").slice(0, 10);
38
+ return `native-${templateId}-${suffix}`;
39
+ }
40
+
41
+ function parseOutputColumns(value: string | null): Set<string> {
42
+ if (!value) return new Set();
43
+ try {
44
+ const parsed = JSON.parse(value);
45
+ if (!Array.isArray(parsed)) return new Set();
46
+ return new Set(
47
+ parsed.flatMap((column) =>
48
+ column && typeof column === "object" && typeof column.name === "string"
49
+ ? [column.name]
50
+ : [],
51
+ ),
52
+ );
53
+ } catch {
54
+ return new Set();
55
+ }
56
+ }
57
+
58
+ function templateIdFromDashboard(
59
+ config: Record<string, unknown>,
60
+ ): string | null {
61
+ const catalog = config.catalog;
62
+ if (!catalog || typeof catalog !== "object" || Array.isArray(catalog)) {
63
+ return null;
64
+ }
65
+ const templateId = (catalog as Record<string, unknown>).templateId;
66
+ return typeof templateId === "string" ? templateId : null;
67
+ }
68
+
69
+ async function validateAndShareBindings(
70
+ bindings: Record<string, NativeV2Binding>,
71
+ requiredKeys: NativeV2BindingKey[],
72
+ requiredColumnsByBinding: Partial<Record<NativeV2BindingKey, string[]>>,
73
+ admin: { userEmail: string; orgId: string },
74
+ ): Promise<void> {
75
+ const missing = requiredKeys.filter((key) => !bindings[key]?.programId);
76
+ if (missing.length > 0) {
77
+ throw new Error(`Missing required v2 bindings: ${missing.join(", ")}`);
78
+ }
79
+
80
+ const uniqueProgramIds = new Set(
81
+ requiredKeys.map((key) => bindings[key]?.programId).filter(Boolean),
82
+ );
83
+ const programsToShare: string[] = [];
84
+ for (const programId of uniqueProgramIds) {
85
+ const program = await getDataProgram(programId, ANALYTICS_APP_ID);
86
+ if (!program) {
87
+ throw new Error(`Data program not found: ${programId}`);
88
+ }
89
+ if (program.orgId && program.orgId !== admin.orgId) {
90
+ throw new Error(
91
+ `Data program belongs to another organization: ${programId}`,
92
+ );
93
+ }
94
+ if (program.archivedAt) {
95
+ throw new Error(`Data program is archived: ${programId}`);
96
+ }
97
+ const access = await resolveAccess("data_program", programId, {
98
+ userEmail: admin.userEmail,
99
+ orgId: admin.orgId,
100
+ });
101
+ if (!access) {
102
+ throw new Error(`No access to data program: ${programId}`);
103
+ }
104
+ const outputColumns = parseOutputColumns(program.outputColumns);
105
+ const missingColumns = [
106
+ ...new Set(
107
+ requiredKeys.flatMap((key) =>
108
+ bindings[key]?.programId === programId
109
+ ? (requiredColumnsByBinding[key] ?? [])
110
+ : [],
111
+ ),
112
+ ),
113
+ ].filter((column) => !outputColumns.has(column));
114
+ if (missingColumns.length > 0) {
115
+ throw new Error(
116
+ `Data program ${programId} is missing required output columns: ${missingColumns.join(", ")}. Re-save it after a successful dry-run.`,
117
+ );
118
+ }
119
+
120
+ if (program.visibility !== "org" || program.orgId !== admin.orgId) {
121
+ programsToShare.push(programId);
122
+ }
123
+ }
124
+
125
+ for (const programId of programsToShare) {
126
+ await setResourceVisibilityAction.run({
127
+ resourceType: "data_program",
128
+ resourceId: programId,
129
+ visibility: "org",
130
+ });
131
+ }
132
+ }
133
+
134
+ export interface EnsureNativeV2DashboardsResult {
135
+ orgId: string;
136
+ dashboards: Array<{
137
+ templateId: NativeV2DashboardId;
138
+ dashboardId: string;
139
+ status: "created" | "updated" | "preserved";
140
+ requiredBindings: NativeV2BindingKey[];
141
+ }>;
142
+ }
143
+
144
+ export async function ensureNativeV2Dashboards(
145
+ args: {
146
+ templateIds?: NativeV2DashboardId[];
147
+ bindings: Record<string, NativeV2Binding>;
148
+ overwrite: boolean;
149
+ },
150
+ ctx?: { userEmail?: string; orgId?: string | null },
151
+ ): Promise<EnsureNativeV2DashboardsResult> {
152
+ const admin = await requireAnalyticsAdminContext(ctx);
153
+ const templateIds = args.templateIds?.length
154
+ ? args.templateIds
155
+ : [...NATIVE_V2_DASHBOARD_IDS];
156
+ const manifests = templateIds.map(getNativeV2DashboardManifest);
157
+ const allRequiredBindings = [
158
+ ...new Set(manifests.flatMap((manifest) => manifest.requiredBindings)),
159
+ ];
160
+ const requiredColumnsByBinding: Partial<
161
+ Record<NativeV2BindingKey, string[]>
162
+ > = Object.fromEntries(
163
+ manifests.flatMap((manifest) =>
164
+ manifest.panels
165
+ .filter(
166
+ (panel): panel is typeof panel & { bindingKey: NativeV2BindingKey } =>
167
+ Boolean(panel.bindingKey),
168
+ )
169
+ .map((panel) => [panel.bindingKey, panel.requiredColumns ?? []]),
170
+ ),
171
+ );
172
+
173
+ await validateAndShareBindings(
174
+ args.bindings,
175
+ allRequiredBindings,
176
+ requiredColumnsByBinding,
177
+ admin,
178
+ );
179
+
180
+ const installedAt = new Date().toISOString();
181
+ const dashboards: EnsureNativeV2DashboardsResult["dashboards"] = [];
182
+ for (const manifest of manifests) {
183
+ const dashboardId = orgDashboardId(admin.orgId, manifest.id);
184
+ const existing = await getDashboard(dashboardId, {
185
+ email: admin.userEmail,
186
+ orgId: admin.orgId,
187
+ });
188
+ if (existing && templateIdFromDashboard(existing.config) !== manifest.id) {
189
+ throw new Error(
190
+ `Refusing to overwrite unrelated dashboard at deterministic id ${dashboardId}.`,
191
+ );
192
+ }
193
+
194
+ if (existing && !args.overwrite) {
195
+ dashboards.push({
196
+ templateId: manifest.id,
197
+ dashboardId,
198
+ status: "preserved",
199
+ requiredBindings: manifest.requiredBindings,
200
+ });
201
+ continue;
202
+ }
203
+
204
+ const config = compileNativeV2Dashboard(
205
+ manifest,
206
+ args.bindings,
207
+ installedAt,
208
+ );
209
+ await upsertDashboard(
210
+ dashboardId,
211
+ "sql",
212
+ config as unknown as Record<string, unknown>,
213
+ {
214
+ email: admin.userEmail,
215
+ orgId: admin.orgId,
216
+ },
217
+ );
218
+ if (!existing) {
219
+ await setResourceVisibilityAction.run({
220
+ resourceType: "dashboard",
221
+ resourceId: dashboardId,
222
+ visibility: "org",
223
+ });
224
+ }
225
+ dashboards.push({
226
+ templateId: manifest.id,
227
+ dashboardId,
228
+ status: existing ? "updated" : "created",
229
+ requiredBindings: manifest.requiredBindings,
230
+ });
231
+ }
232
+
233
+ return { orgId: admin.orgId, dashboards };
234
+ }
235
+
236
+ export default defineAction({
237
+ description:
238
+ "Provision the four reusable native Analytics v2 dashboard patterns for the active organization from explicit Data Program bindings. " +
239
+ "This is an admin-only post-deploy action: it validates every program exists and exposes the manifest's required output columns, shares the bound programs with the organization, creates deterministic native-* dashboard copies, and never changes the legacy extension dashboards. " +
240
+ "Re-running preserves existing v2 dashboard edits unless overwrite=true.",
241
+ schema: z.object({
242
+ templateIds: z
243
+ .array(z.enum(NATIVE_V2_DASHBOARD_IDS))
244
+ .min(1)
245
+ .optional()
246
+ .describe("Optional subset of native v2 templates to provision."),
247
+ bindings: z
248
+ .record(z.string(), bindingSchema)
249
+ .describe(
250
+ 'Binding key to stored Analytics Data Program, e.g. {"roi.kpi": {"programId": "dp_..."}}.',
251
+ ),
252
+ overwrite: z
253
+ .boolean()
254
+ .optional()
255
+ .default(false)
256
+ .describe("Replace an existing v2 copy only when explicitly true."),
257
+ }),
258
+ mcpApp: {
259
+ compactCatalog: true,
260
+ resource: embedApp({
261
+ title: "Native Analytics v2 dashboards",
262
+ description: "Open the provisioned native v2 dashboard in Analytics.",
263
+ iframeTitle: "Agent-Native Analytics",
264
+ openLabel: "Open native v2 dashboard",
265
+ height: 680,
266
+ }),
267
+ },
268
+ run: async (args, ctx) => {
269
+ const userEmail = ctx?.userEmail ?? getRequestUserEmail();
270
+ const orgId = ctx?.orgId ?? getRequestOrgId() ?? null;
271
+ return ensureNativeV2Dashboards(
272
+ {
273
+ templateIds: args.templateIds,
274
+ bindings: args.bindings,
275
+ overwrite: args.overwrite,
276
+ },
277
+ { userEmail, orgId },
278
+ );
279
+ },
280
+ link: ({ result }) => {
281
+ const dashboardId =
282
+ result && typeof result === "object"
283
+ ? (result as EnsureNativeV2DashboardsResult).dashboards?.[0]
284
+ ?.dashboardId
285
+ : null;
286
+ if (!dashboardId) return null;
287
+ return {
288
+ url: buildDeepLink({
289
+ app: "analytics",
290
+ view: "adhoc",
291
+ params: { dashboardId },
292
+ }),
293
+ label: "Open native v2 dashboard",
294
+ view: "adhoc",
295
+ };
296
+ },
297
+ });
298
+
299
+ export { nativeV2DashboardManifests };
@@ -0,0 +1,34 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { z } from "zod";
3
+
4
+ import {
5
+ nativeV2DashboardManifests,
6
+ NATIVE_V2_DASHBOARD_VERSION,
7
+ } from "../server/lib/native-v2-dashboards";
8
+
9
+ export default defineAction({
10
+ description:
11
+ "List the source-controlled native Analytics v2 dashboard manifests, including required Data Program binding keys and output columns. These provider-free manifests are compiled into org dashboards by ensure-native-v2-dashboards.",
12
+ schema: z.object({}),
13
+ http: { method: "GET" },
14
+ readOnly: true,
15
+ publicAgent: { expose: true, readOnly: true, requiresAuth: true },
16
+ run: async () => ({
17
+ version: NATIVE_V2_DASHBOARD_VERSION,
18
+ templates: nativeV2DashboardManifests.map((manifest) => ({
19
+ id: manifest.id,
20
+ name: manifest.name,
21
+ description: manifest.description,
22
+ category: manifest.category,
23
+ tags: manifest.tags,
24
+ requiredBindings: manifest.requiredBindings,
25
+ panels: manifest.panels.map((panel) => ({
26
+ id: panel.id,
27
+ title: panel.title,
28
+ chartType: panel.chartType,
29
+ bindingKey: panel.bindingKey ?? null,
30
+ requiredColumns: panel.requiredColumns ?? [],
31
+ })),
32
+ })),
33
+ }),
34
+ });