@agent-native/core 0.146.3 → 0.146.4

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 (206) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +13 -10
  3. package/corpus/templates/analytics/.agents/skills/self-modifying-code/SKILL.md +7 -3
  4. package/corpus/templates/analytics/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  5. package/corpus/templates/analytics/actions/compose-dashboard.ts +53 -6
  6. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +8 -2
  7. package/corpus/templates/analytics/actions/migrate-first-party-analytics-to-bigquery.ts +3 -1
  8. package/corpus/templates/analytics/actions/mutate-dashboard.ts +48 -9
  9. package/corpus/templates/analytics/actions/update-dashboard.ts +80 -6
  10. package/corpus/templates/analytics/changelog/2026-08-07-dashboard-creation-and-edits-now-save-faster-with-reliable-s.md +6 -0
  11. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +19 -19
  12. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +21 -2
  13. package/corpus/templates/analytics/server/lib/bigquery.ts +59 -24
  14. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +44 -8
  15. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +3 -5
  16. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +33 -69
  17. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +90 -24
  18. package/corpus/templates/analytics/server/lib/real-data-actions.ts +1 -1
  19. package/corpus/templates/analytics/server/plugins/agent-chat.ts +17 -2
  20. package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +2 -0
  21. package/corpus/templates/assets/.agents/skills/self-modifying-code/SKILL.md +7 -3
  22. package/corpus/templates/assets/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  23. package/corpus/templates/assets/netlify.toml +1 -1
  24. package/corpus/templates/assets/package.json +1 -0
  25. package/corpus/templates/assets/scripts/migrate-production.ts +24 -0
  26. package/corpus/templates/assets/server/plugins/db.ts +1 -1
  27. package/corpus/templates/assets/tsconfig.json +1 -0
  28. package/corpus/templates/brain/.agents/skills/self-modifying-code/SKILL.md +7 -3
  29. package/corpus/templates/brain/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  30. package/corpus/templates/brain/netlify.toml +1 -1
  31. package/corpus/templates/brain/package.json +2 -0
  32. package/corpus/templates/brain/scripts/migrate-production.ts +24 -0
  33. package/corpus/templates/brain/server/plugins/db.ts +1 -1
  34. package/corpus/templates/brain/tsconfig.json +1 -0
  35. package/corpus/templates/calendar/.agents/skills/self-modifying-code/SKILL.md +7 -3
  36. package/corpus/templates/calendar/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  37. package/corpus/templates/calendar/netlify.toml +1 -1
  38. package/corpus/templates/calendar/package.json +1 -0
  39. package/corpus/templates/calendar/scripts/migrate-production.ts +24 -0
  40. package/corpus/templates/calendar/server/plugins/db.ts +1 -1
  41. package/corpus/templates/calendar/tsconfig.json +1 -0
  42. package/corpus/templates/chat/.agents/skills/self-modifying-code/SKILL.md +7 -3
  43. package/corpus/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  44. package/corpus/templates/chat/netlify.toml +1 -1
  45. package/corpus/templates/chat/package.json +1 -0
  46. package/corpus/templates/chat/scripts/migrate-production.ts +31 -0
  47. package/corpus/templates/chat/tsconfig.json +1 -0
  48. package/corpus/templates/clips/.agents/skills/self-modifying-code/SKILL.md +7 -3
  49. package/corpus/templates/clips/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  50. package/corpus/templates/clips/netlify.toml +1 -1
  51. package/corpus/templates/clips/package.json +1 -0
  52. package/corpus/templates/clips/scripts/migrate-production.ts +24 -0
  53. package/corpus/templates/clips/server/plugins/db.ts +1 -1
  54. package/corpus/templates/clips/tsconfig.json +1 -0
  55. package/corpus/templates/content/.agents/skills/self-modifying-code/SKILL.md +7 -3
  56. package/corpus/templates/content/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  57. package/corpus/templates/content/netlify.toml +1 -1
  58. package/corpus/templates/content/package.json +2 -0
  59. package/corpus/templates/content/scripts/migrate-production.ts +28 -0
  60. package/corpus/templates/content/server/plugins/db.ts +2 -2
  61. package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  62. package/corpus/templates/crm/netlify.toml +1 -1
  63. package/corpus/templates/crm/package.json +2 -0
  64. package/corpus/templates/crm/scripts/migrate-production.ts +24 -0
  65. package/corpus/templates/crm/server/plugins/db.ts +1 -1
  66. package/corpus/templates/crm/tsconfig.json +1 -0
  67. package/corpus/templates/design/.agents/skills/self-modifying-code/SKILL.md +7 -3
  68. package/corpus/templates/design/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  69. package/corpus/templates/design/netlify.toml +1 -1
  70. package/corpus/templates/design/package.json +1 -0
  71. package/corpus/templates/design/scripts/migrate-production.ts +24 -0
  72. package/corpus/templates/design/server/plugins/db.ts +1 -1
  73. package/corpus/templates/design/tsconfig.json +1 -0
  74. package/corpus/templates/dispatch/.agents/skills/self-modifying-code/SKILL.md +7 -3
  75. package/corpus/templates/dispatch/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  76. package/corpus/templates/dispatch/netlify.toml +1 -1
  77. package/corpus/templates/dispatch/package.json +1 -0
  78. package/corpus/templates/dispatch/scripts/migrate-production.ts +23 -0
  79. package/corpus/templates/dispatch/tsconfig.json +1 -0
  80. package/corpus/templates/factory/.agents/skills/self-modifying-code/SKILL.md +7 -3
  81. package/corpus/templates/factory/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  82. package/corpus/templates/factory/netlify.toml +1 -1
  83. package/corpus/templates/factory/package.json +1 -0
  84. package/corpus/templates/factory/scripts/migrate-production.ts +31 -0
  85. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +2 -0
  86. package/corpus/templates/factory/tsconfig.json +1 -0
  87. package/corpus/templates/forms/.agents/skills/self-modifying-code/SKILL.md +7 -3
  88. package/corpus/templates/forms/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  89. package/corpus/templates/forms/netlify.toml +1 -1
  90. package/corpus/templates/forms/package.json +2 -0
  91. package/corpus/templates/forms/scripts/migrate-production.ts +24 -0
  92. package/corpus/templates/forms/server/plugins/db.ts +1 -1
  93. package/corpus/templates/forms/tsconfig.json +1 -0
  94. package/corpus/templates/macros/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  95. package/corpus/templates/macros/netlify.toml +1 -1
  96. package/corpus/templates/macros/package.json +1 -0
  97. package/corpus/templates/macros/scripts/migrate-production.ts +24 -0
  98. package/corpus/templates/macros/server/plugins/db.ts +1 -1
  99. package/corpus/templates/macros/tsconfig.json +1 -0
  100. package/corpus/templates/mail/.agents/skills/self-modifying-code/SKILL.md +7 -3
  101. package/corpus/templates/mail/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  102. package/corpus/templates/mail/netlify.toml +1 -1
  103. package/corpus/templates/mail/package.json +1 -0
  104. package/corpus/templates/mail/scripts/migrate-production.ts +24 -0
  105. package/corpus/templates/mail/server/plugins/db.ts +1 -1
  106. package/corpus/templates/mail/tsconfig.json +1 -0
  107. package/corpus/templates/plan/.agents/skills/self-modifying-code/SKILL.md +7 -3
  108. package/corpus/templates/plan/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  109. package/corpus/templates/plan/netlify.toml +1 -1
  110. package/corpus/templates/plan/package.json +1 -0
  111. package/corpus/templates/plan/scripts/migrate-production.ts +24 -0
  112. package/corpus/templates/plan/server/plugins/00-mcp.ts +3 -2
  113. package/corpus/templates/plan/server/plugins/db.ts +1 -1
  114. package/corpus/templates/plan/tsconfig.json +1 -0
  115. package/corpus/templates/slides/.agents/skills/self-modifying-code/SKILL.md +7 -3
  116. package/corpus/templates/slides/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  117. package/corpus/templates/slides/netlify.toml +1 -1
  118. package/corpus/templates/slides/package.json +1 -0
  119. package/corpus/templates/slides/scripts/migrate-production.ts +24 -0
  120. package/corpus/templates/slides/server/plugins/db.ts +1 -1
  121. package/corpus/templates/slides/tsconfig.json +1 -0
  122. package/corpus/templates/tasks/.agents/skills/self-modifying-code/SKILL.md +7 -3
  123. package/corpus/templates/tasks/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  124. package/corpus/templates/tasks/netlify.toml +1 -1
  125. package/corpus/templates/tasks/package.json +1 -0
  126. package/corpus/templates/tasks/scripts/migrate-production.ts +24 -0
  127. package/corpus/templates/tasks/server/plugins/db.ts +1 -1
  128. package/corpus/templates/tasks/tsconfig.json +1 -0
  129. package/dist/action.d.ts +2 -0
  130. package/dist/agent/production-agent.d.ts +1 -0
  131. package/dist/agent/production-agent.js +2 -0
  132. package/dist/agent/thread-data-builder.d.ts +4 -0
  133. package/dist/agent/thread-data-builder.js +44 -0
  134. package/dist/automations/service.d.ts +1 -0
  135. package/dist/automations/service.js +21 -2
  136. package/dist/cli/create.js +20 -2
  137. package/dist/client/agent-page/AgentJobsTab.js +3 -1
  138. package/dist/client/agent-page/AutomationDetailsDialog.js +9 -2
  139. package/dist/collab/awareness.d.ts +2 -2
  140. package/dist/collab/struct-routes.d.ts +1 -1
  141. package/dist/db/migrations.js +7 -13
  142. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  143. package/dist/jobs/actions/list-automation-runs.d.ts +1 -0
  144. package/dist/jobs/actions/list-automation-runs.js +9 -2
  145. package/dist/jobs/actions/list-recurring-jobs.js +3 -1
  146. package/dist/jobs/actions/manage-recurring-job.js +1 -1
  147. package/dist/jobs/actions/run-automation-now.js +1 -0
  148. package/dist/jobs/background-automation-runner.js +2 -0
  149. package/dist/jobs/frontmatter.d.ts +2 -0
  150. package/dist/jobs/frontmatter.js +4 -0
  151. package/dist/jobs/run-history.d.ts +4 -0
  152. package/dist/jobs/run-history.js +28 -8
  153. package/dist/jobs/run-now.d.ts +4 -1
  154. package/dist/jobs/run-now.js +4 -3
  155. package/dist/jobs/scheduler.js +18 -0
  156. package/dist/jobs/tools.d.ts +3 -2
  157. package/dist/jobs/tools.js +26 -12
  158. package/dist/localization/default-messages.d.ts +1 -0
  159. package/dist/localization/default-messages.js +1 -0
  160. package/dist/notifications/routes.d.ts +2 -2
  161. package/dist/observability/routes.d.ts +3 -3
  162. package/dist/progress/routes.d.ts +1 -1
  163. package/dist/resources/handlers.d.ts +1 -1
  164. package/dist/server/action-routes.d.ts +2 -0
  165. package/dist/server/action-routes.js +1 -0
  166. package/dist/server/agent-chat-plugin.js +3 -2
  167. package/dist/server/builder-browser.js +24 -8
  168. package/dist/server/transcribe-voice.d.ts +1 -1
  169. package/dist/templates/chat/.agents/skills/self-modifying-code/SKILL.md +7 -3
  170. package/dist/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  171. package/dist/templates/chat/netlify.toml +1 -1
  172. package/dist/templates/chat/package.json +1 -0
  173. package/dist/templates/chat/scripts/migrate-production.ts +31 -0
  174. package/dist/templates/chat/tsconfig.json +1 -0
  175. package/dist/templates/default/.agents/skills/self-modifying-code/SKILL.md +7 -3
  176. package/dist/templates/default/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  177. package/dist/templates/default/package.json +1 -0
  178. package/dist/templates/default/scripts/migrate-production.ts +31 -0
  179. package/dist/templates/default/tsconfig.json +1 -0
  180. package/dist/templates/headless/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  181. package/dist/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +7 -3
  182. package/dist/templates/workspace-core/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  183. package/dist/templates/workspace-root/AGENTS.md +2 -1
  184. package/dist/triggers/actions/list-automations.js +1 -1
  185. package/dist/triggers/actions/manage-automation.js +5 -1
  186. package/dist/triggers/actions.d.ts +1 -1
  187. package/dist/triggers/actions.js +21 -15
  188. package/dist/triggers/dispatcher.js +17 -2
  189. package/dist/triggers/routes.d.ts +1 -0
  190. package/dist/triggers/routes.js +1 -0
  191. package/package.json +1 -1
  192. package/src/templates/chat/.agents/skills/self-modifying-code/SKILL.md +7 -3
  193. package/src/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  194. package/src/templates/chat/netlify.toml +1 -1
  195. package/src/templates/chat/package.json +1 -0
  196. package/src/templates/chat/scripts/migrate-production.ts +31 -0
  197. package/src/templates/chat/tsconfig.json +1 -0
  198. package/src/templates/default/.agents/skills/self-modifying-code/SKILL.md +7 -3
  199. package/src/templates/default/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  200. package/src/templates/default/package.json +1 -0
  201. package/src/templates/default/scripts/migrate-production.ts +31 -0
  202. package/src/templates/default/tsconfig.json +1 -0
  203. package/src/templates/headless/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  204. package/src/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +7 -3
  205. package/src/templates/workspace-core/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  206. package/src/templates/workspace-root/AGENTS.md +2 -1
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 7904
34
+ - template files: 7921
@@ -464,9 +464,10 @@ Filters auto-apply on change — there is no Apply button. Each filter change wr
464
464
  ## Modifying A Dashboard
465
465
 
466
466
  For existing dashboard edits, default to `mutate-dashboard`. It gives the
467
- agent a small typed script API without exposing arbitrary JavaScript execution.
468
- The main action payload is a string, so it avoids native-array serialization
469
- traps in tool calls while still giving the agent a code-like editing surface.
467
+ agent a typed mutation API without exposing arbitrary JavaScript execution.
468
+ Pass structured `operations` for normal agent calls; keep the legacy `code`
469
+ form for short layout/config edits only. Large SQL payloads belong in the
470
+ server-side first-party metric catalog, not in a prompt argument.
470
471
  The server parses only documented `dashboard.*` method calls, applies the
471
472
  resulting operations in memory, validates the final dashboard config, writes
472
473
  SQL once, syncs collab, and returns compact proof.
@@ -670,8 +671,9 @@ After a mutation, navigate to the dashboard if the user is elsewhere. The app sy
670
671
 
671
672
  ### Reordering Panels
672
673
 
673
- For simple "move this chart/section" requests, prefer `mutate-dashboard` with a
674
- string script:
674
+ For simple "move this chart/section" requests, prefer `mutate-dashboard` with
675
+ structured operations. The short script form is also supported for one compact
676
+ move:
675
677
 
676
678
  ```json
677
679
  {
@@ -694,9 +696,9 @@ When the user asks to change existing panels:
694
696
 
695
697
  1. Read the current dashboard with `get-sql-dashboard` compact mode unless full
696
698
  SQL/config is required.
697
- 2. Call `mutate-dashboard` once with every change in one script. Use
698
- `panel(...)`, `panels([...])`, or `panelsMatching({...})` selectors by id or
699
- metadata; do not compute shifted array indexes.
699
+ 2. Call `mutate-dashboard` once with every change in `operations`. Use panel
700
+ ids, not shifted array indexes. The short `code` form is only for compact
701
+ layout/config edits; do not stream a large multi-panel SQL script.
700
702
  3. Verify the returned `panelCount`, `appliedOps`, `firstPanelIds`, and
701
703
  `summary`. If possible, read the affected panels back and confirm the exact
702
704
  fields changed.
@@ -742,8 +744,9 @@ For a **first-party analytics** dashboard, prefer `compose-dashboard` over hand-
742
744
  - **Never hand-author large first-party configs panel-by-panel.** Call `compose-dashboard` with the metric keys instead.
743
745
  - Unknown metric keys are skipped and reported in `unknownMetrics` (not fatal). Each panel's SQL is validated independently — valid panels save, invalid ones are reported in `invalidMetrics`.
744
746
  - By default (no `overwrite`), composing into an existing dashboard APPENDS the new panels and skips ids already present. `overwrite: true` replaces the whole config.
747
+ - Set `refreshExisting: true` when refreshing a catalog-backed dashboard. Matching metric panels are replaced in place, unrelated panels and layout order are preserved, and the server returns `refreshedExistingIds`.
745
748
  - Each metric accepts an optional per-metric `window` of `'30d' | '90d' | 'all'` (only affects windowed virality/time metrics) and `title` / `chartType` / `width` overrides. Request `'all'` only when the user asks for all-time coverage, and describe it as full available history.
746
- - Returns `{ dashboardId, panelCount, createdMetrics, unknownMetrics, invalidMetrics, skippedExistingIds }` — report `panelCount` as proof-of-done.
749
+ - Returns `{ saved, dashboardId, panelCount, createdMetrics, refreshedExistingIds, unknownMetrics, invalidMetrics, skippedExistingIds }` — report `saved: true`, `panelCount`, and any invalid/unknown metrics as proof-of-done.
747
750
 
748
751
  Available metric keys: `total-signups`, `signups-over-time`, `signups-by-template`, `sessions-by-app`, `sessions-over-time`, `replay-sessions`, `replay-chunks-over-time`, `recent-replay-sessions`, `signed-in-vs-anon`, `total-template-clicks`, `total-demo-clicks`, `total-cli-copies`, `template-interest-over-time`, `clicks-by-template`, `demo-clicks-by-template`, `cli-copies-by-template`, `cli-copies-over-time`, `pageviews-over-time`, `top-referrer-domains`, `referred-signups-30d`, `viral-signup-share-30d`, `clip-share-signups-30d`, `signups-by-referral-source`, `referred-signups-over-time`, `top-referrers`, `share-funnel-30d`, `viral-participation-rate-90d`, `viral-coefficient-90d`, `activated-referrers-90d`.
749
752
 
@@ -760,7 +763,7 @@ This is the dashboard-specific application of the framework-wide `reliable-mutat
760
763
  Hosted agent runs have a **~40s budget**. Many sequential `update-dashboard` calls (one per panel, plus schema-discovery calls) will blow that budget and leave the dashboard in a partial state — earlier inserts looked like they succeeded (✓), but nothing actually persisted. Avoid this:
761
764
 
762
765
  - **For a large first-party dashboard, use `compose-dashboard`** (see the section above): name the metrics, the server generates the panels in one call. Do not hand-author the big config.
763
- - **Batch ALL edits into ONE `mutate-dashboard` call.** One script can move,
766
+ - **Batch ALL edits into ONE `mutate-dashboard` call.** Structured operations can move,
764
767
  insert, remove, duplicate, and update many panels. Never loop dashboard edit
765
768
  actions panel-by-panel.
766
769
  - To bulk edit existing panels, use selectors:
@@ -36,8 +36,10 @@ Tier 4 includes **all** of the following — not only editing package source:
36
36
  - Files under `node_modules/@agent-native/*` (core, dispatch, scheduling, …)
37
37
  - `pnpm.overrides`, `overrides`, `resolutions`, or `patchedDependencies` that
38
38
  target any `@agent-native/*` package
39
- - Local package patches or invented "dispatch/core behavior" shims meant to
40
- paper over a version skew or failed upgrade
39
+ - `pnpm patch`, `pnpm patch-commit`, or local package patch artifacts for any
40
+ dependency
41
+ - Invented "dispatch/core behavior" shims meant to paper over a version skew
42
+ or failed upgrade
41
43
 
42
44
  This does not prohibit intentional app-owned UI customization. When public
43
45
  props and composition are insufficient, the `customizing-agent-native` skill
@@ -100,8 +102,10 @@ inline strings in components.
100
102
  - Don't confuse readable package source with app-owned code: use
101
103
  `customizing-agent-native` and `agent-native eject` for supported ownership
102
104
  transfer
105
+ - Don't use `pnpm patch` / `pnpm patch-commit`, commit `patches/` artifacts, or
106
+ add `pnpm.patchedDependencies` to "make the app run" after a version bump
103
107
  - Don't add `pnpm.overrides` / `patchedDependencies` / `resolutions` for
104
- `@agent-native/*` to "make the app run" after a version bump
108
+ `@agent-native/*` to paper over a broken upgrade
105
109
  - Don't invent local dispatch/core behavior overrides when upgrade fails —
106
110
  run `npx @agent-native/core@latest upgrade`, then fix app-level errors only
107
111
  - Don't modify `.agents/skills/` or `AGENTS.md` unless explicitly requested
@@ -15,8 +15,9 @@ metadata:
15
15
 
16
16
  When an older Agent Native app/branch needs to run on current packages, use
17
17
  `agent-native upgrade`. Never "fix" upgrade breakage with
18
- `pnpm.overrides`, `patchedDependencies`, `resolutions`, local patches, or
19
- edits under `node_modules/@agent-native/*` — especially not against
18
+ `pnpm patch`, `pnpm patch-commit`, `pnpm.patchedDependencies`,
19
+ `pnpm.overrides`, `patchedDependencies`, `resolutions`, local patch artifacts,
20
+ or edits under `node_modules/@agent-native/*` — especially not against
20
21
  `@agent-native/core` or `@agent-native/dispatch`.
21
22
 
22
23
  ## Why
@@ -109,6 +110,8 @@ install → refresh scaffold skills → verify, then fix **app** code only.
109
110
 
110
111
  - Don't add `pnpm.overrides`, `overrides`, `resolutions`, or
111
112
  `patchedDependencies` for any `@agent-native/*` package
113
+ - Don't run `pnpm patch` or `pnpm patch-commit`, or commit files under
114
+ `patches/`, for any dependency
112
115
  - Don't edit `node_modules/@agent-native/core` or
113
116
  `node_modules/@agent-native/dispatch`
114
117
  - Don't invent local "dispatch behavior" shims to paper over version skew
@@ -111,8 +111,8 @@ export default defineAction({
111
111
  "Build a large first-party analytics dashboard in ONE fast call: name the metrics you want and the SERVER generates the validated SQL + chart config for every panel. " +
112
112
  "Do NOT hand-author large `update-dashboard` configs panel-by-panel — producing/streaming a big multi-panel config inside the ~40s run budget fails and thrashes. " +
113
113
  "Each metric expands into a complete first-party panel from the shipped, already-validated metric catalog. Unknown metric keys are skipped and reported (not fatal); each panel's SQL is validated independently (valid panels are saved, invalid ones reported), and the dashboard is assembled and saved in a single atomic store write. " +
114
- "If the dashboard already exists and `overwrite` is false (default), the new panels are APPENDED (panels whose id is already present are skipped); with `overwrite: true` the config is replaced. " +
115
- "Returns { dashboardId, panelCount, createdMetrics, unknownMetrics, invalidMetrics, urlPath, deepLink, message } — use panelCount as proof-of-done. " +
114
+ "If the dashboard already exists and `overwrite` is false (default), the new panels are APPENDED (panels whose id is already present are skipped). Set `refreshExisting: true` to atomically replace matching catalog panels in place while preserving unrelated panels. With `overwrite: true` the config is replaced. " +
115
+ "Returns { dashboardId, panelCount, createdMetrics, refreshedExistingIds, unknownMetrics, invalidMetrics, urlPath, deepLink, message } — use panelCount as proof-of-done. " +
116
116
  `Available metric keys: ${METRIC_KEYS.join(", ")}. ` +
117
117
  "Each metric accepts an optional per-metric `window` of '30d' | '90d' | 'all' (only affects windowed virality/time metrics). Catalog panels are time-scoped by construction; custom first-party SQL added later must use `{{timeRange}}` or declare `config.timeScope`.",
118
118
  schema: z.object({
@@ -142,6 +142,12 @@ export default defineAction({
142
142
  .describe(
143
143
  "If true, replace the whole dashboard config. If false (default) and the dashboard exists, APPEND the new panels (skipping ids already present).",
144
144
  ),
145
+ refreshExisting: z
146
+ .boolean()
147
+ .optional()
148
+ .describe(
149
+ "When appending to an existing dashboard, replace panels whose ids match the requested catalog metrics in place, preserving unrelated panels and layout order. Use this to refresh a catalog-backed dashboard without sending SQL through the prompt.",
150
+ ),
145
151
  }),
146
152
  mcpApp: {
147
153
  compactCatalog: true,
@@ -238,6 +244,7 @@ export default defineAction({
238
244
  let finalConfig: Record<string, unknown>;
239
245
  let appendedCount = composedPanels.length;
240
246
  let skippedExistingIds: string[] = [];
247
+ let refreshedExistingIds: string[] = [];
241
248
 
242
249
  if (existing && !args.overwrite) {
243
250
  // Append: preserve existing panels + order, add only new panel ids.
@@ -260,18 +267,36 @@ export default defineAction({
260
267
  .map((p) => (typeof p?.id === "string" ? p.id : null))
261
268
  .filter((id): id is string => !!id),
262
269
  );
270
+ const composedById = new Map(
271
+ composedPanels.map((panel) => [panel.id, panel]),
272
+ );
273
+ const refreshed = new Set<string>();
274
+ const mergedExistingPanels = existingPanels.map((panel) => {
275
+ const id = typeof panel?.id === "string" ? panel.id : "";
276
+ const replacement = args.refreshExisting
277
+ ? composedById.get(id)
278
+ : undefined;
279
+ if (!replacement) return panel;
280
+ refreshed.add(id);
281
+ return replacement;
282
+ });
263
283
  const toAppend: ComposedPanel[] = [];
264
284
  const skipped: string[] = [];
285
+ const appendedIds = new Set<string>();
265
286
  for (const panel of composedPanels) {
287
+ if (refreshed.has(panel.id)) continue;
266
288
  if (existingIds.has(panel.id)) {
267
289
  skipped.push(panel.id);
268
290
  continue;
269
291
  }
292
+ if (appendedIds.has(panel.id)) continue;
270
293
  toAppend.push(panel);
294
+ appendedIds.add(panel.id);
271
295
  existingIds.add(panel.id);
272
296
  }
273
297
  appendedCount = toAppend.length;
274
- skippedExistingIds = skipped;
298
+ skippedExistingIds = Array.from(new Set(skipped));
299
+ refreshedExistingIds = Array.from(refreshed);
275
300
  const merged = withFilters({
276
301
  ...existingConfig,
277
302
  name:
@@ -279,7 +304,7 @@ export default defineAction({
279
304
  existingConfig.name.trim()
280
305
  ? existingConfig.name
281
306
  : dashboardName,
282
- panels: [...existingPanels, ...toAppend],
307
+ panels: [...mergedExistingPanels, ...toAppend],
283
308
  });
284
309
  return { kind: "sql" as const, body: merged };
285
310
  },
@@ -304,11 +329,30 @@ export default defineAction({
304
329
  ? (finalConfig.panels as unknown[]).length
305
330
  : 0;
306
331
 
307
- queueDashboardCollabSync(args.dashboardId, finalConfig, "agent");
332
+ const changed =
333
+ !existing ||
334
+ args.overwrite === true ||
335
+ appendedCount > 0 ||
336
+ refreshedExistingIds.length > 0;
337
+
338
+ if (changed) {
339
+ queueDashboardCollabSync(args.dashboardId, finalConfig, "agent");
340
+ }
308
341
 
309
342
  const parts: string[] = [];
310
343
  if (existing && !args.overwrite) {
311
- parts.push(`Appended ${appendedCount} panel(s) to "${args.dashboardId}"`);
344
+ if (!changed) {
345
+ parts.push(`No changes were needed for "${args.dashboardId}"`);
346
+ } else if (refreshedExistingIds.length > 0) {
347
+ parts.push(
348
+ `Refreshed ${refreshedExistingIds.length} existing panel(s)`,
349
+ );
350
+ }
351
+ if (changed) {
352
+ parts.push(
353
+ `Appended ${appendedCount} panel(s) to "${args.dashboardId}"`,
354
+ );
355
+ }
312
356
  if (skippedExistingIds.length > 0) {
313
357
  parts.push(`${skippedExistingIds.length} already present`);
314
358
  }
@@ -330,11 +374,14 @@ export default defineAction({
330
374
  parts.push(`Dashboard now has ${panelCount} panel(s).`);
331
375
 
332
376
  return {
377
+ saved: true,
378
+ changed,
333
379
  id: args.dashboardId,
334
380
  dashboardId: args.dashboardId,
335
381
  name: dashboardName,
336
382
  panelCount,
337
383
  createdMetrics,
384
+ refreshedExistingIds,
338
385
  unknownMetrics,
339
386
  invalidMetrics,
340
387
  skippedExistingIds,
@@ -136,6 +136,10 @@ export const DASHBOARD_MUTATION_EXAMPLES = [
136
136
  'dashboard.insertPanel({"id":"personal-widget-slot","title":"Personal Widget Slot","chartType":"extension","width":3,"config":{"extensionSlotId":"analytics.dashboard.<dashboard-id>.panel.personal-widget-slot"}}).atBottom();',
137
137
  ] as const;
138
138
 
139
+ /** Keep the legacy script surface bounded before it reaches the parser. */
140
+ export const MAX_DASHBOARD_MUTATION_CODE_LENGTH = 12_000;
141
+ export const MAX_DASHBOARD_MUTATION_OPERATIONS = 100;
142
+
139
143
  export type DashboardMutationOperation =
140
144
  | {
141
145
  op: "movePanels";
@@ -1494,8 +1498,10 @@ export function parseDashboardMutationScript(
1494
1498
  config: Record<string, unknown>,
1495
1499
  code: string,
1496
1500
  ): DashboardMutationOperation[] {
1497
- if (code.length > 12_000) {
1498
- throw new Error("mutation script is too large; keep it under 12000 chars");
1501
+ if (code.length > MAX_DASHBOARD_MUTATION_CODE_LENGTH) {
1502
+ throw new Error(
1503
+ `mutation script is too large; keep it under ${MAX_DASHBOARD_MUTATION_CODE_LENGTH} chars or use structured operations/compose-dashboard`,
1504
+ );
1499
1505
  }
1500
1506
  if (hasTemplateLiteralSyntax(code)) {
1501
1507
  throw new Error("mutation script does not support template literals");
@@ -44,7 +44,9 @@ const migrationSchema = z.object({
44
44
  .min(1)
45
45
  .max(750)
46
46
  .optional()
47
- .describe("Optional per-page limit used when mode is prepare."),
47
+ .describe(
48
+ "Optional per-page limit used when mode is prepare; for an existing job, it can only increase the bounded batch size.",
49
+ ),
48
50
  confirm: z.boolean().optional(),
49
51
  });
50
52
 
@@ -19,6 +19,8 @@ import {
19
19
  applyDashboardMutationOperations,
20
20
  DASHBOARD_MUTATION_API_TYPES,
21
21
  DASHBOARD_MUTATION_EXAMPLES,
22
+ MAX_DASHBOARD_MUTATION_CODE_LENGTH,
23
+ MAX_DASHBOARD_MUTATION_OPERATIONS,
22
24
  parseDashboardMutationScript,
23
25
  type DashboardMutationOperation,
24
26
  type DashboardMutationResult,
@@ -104,6 +106,11 @@ function parseJsonArrayString(
104
106
  if (!Array.isArray(parsed)) {
105
107
  throw new Error(`${fieldName} must be a JSON array`);
106
108
  }
109
+ if (parsed.length > MAX_DASHBOARD_MUTATION_OPERATIONS) {
110
+ throw new Error(
111
+ `${fieldName} has ${parsed.length} operations; keep it at or below ${MAX_DASHBOARD_MUTATION_OPERATIONS}`,
112
+ );
113
+ }
107
114
  return parsed.map((op, index) => {
108
115
  try {
109
116
  return mutationOperationSchema.parse(op) as DashboardMutationOperation;
@@ -115,7 +122,7 @@ function parseJsonArrayString(
115
122
 
116
123
  const operationsInputSchema = z
117
124
  .union([
118
- z.array(mutationOperationSchema),
125
+ z.array(mutationOperationSchema).max(MAX_DASHBOARD_MUTATION_OPERATIONS),
119
126
  z.string().transform((value) => {
120
127
  const trimmed = value.trim();
121
128
  return trimmed ? parseJsonArrayString(trimmed, "operations") : undefined;
@@ -134,7 +141,7 @@ function nonEmptyOperations(
134
141
  }
135
142
 
136
143
  const apiHelp =
137
- "Constrained TypeScript-like dashboard mutation script. The server parses only calls on `dashboard`; it does not execute arbitrary JavaScript. " +
144
+ "Short, constrained TypeScript-like dashboard mutation script for compatibility. Prefer structured `operations` for agent calls; use this only for small layout/config edits. The server parses only calls on `dashboard`; it does not execute arbitrary JavaScript. " +
138
145
  "No variables, imports, loops, functions, templates, network, filesystem, or DB access. Arguments must be JSON-compatible literals, so quote object keys. " +
139
146
  "Subjects: dashboard.set, dashboard.setFilterDefault, dashboard.panel, dashboard.panels, dashboard.panelsMatching, dashboard.section, dashboard.insertPanel. " +
140
147
  'For a simple default-filter change, use `dashboard.setFilterDefault("emailFilter","exclude_builder");`; it verifies the filter and option value without resending every filter or revalidating unchanged panel SQL. ' +
@@ -148,7 +155,26 @@ const agentInputSchema = z.object({
148
155
  .string()
149
156
  .min(1)
150
157
  .describe("Dashboard id, e.g. 'agent-native-templates-first-party'."),
151
- code: z.string().min(1).describe(apiHelp),
158
+ operations: z
159
+ .array(mutationOperationSchema)
160
+ .max(MAX_DASHBOARD_MUTATION_OPERATIONS)
161
+ .optional()
162
+ .describe(
163
+ "Preferred agent input: structured dashboard edits applied atomically in one save. Use panel ids, not array indexes. For first-party metric refreshes, use compose-dashboard with metric keys instead of embedding SQL here.",
164
+ ),
165
+ code: z
166
+ .string()
167
+ .max(MAX_DASHBOARD_MUTATION_CODE_LENGTH)
168
+ .optional()
169
+ .describe(apiHelp),
170
+ dryRun: z
171
+ .boolean()
172
+ .optional()
173
+ .describe("Validate the mutation without saving it."),
174
+ returnConfig: z
175
+ .boolean()
176
+ .optional()
177
+ .describe("Include the full resulting config only when it is needed."),
152
178
  });
153
179
 
154
180
  function resolveScope() {
@@ -212,10 +238,13 @@ function sqlValidationScope(
212
238
  async function validateMutationSql(
213
239
  config: Record<string, unknown>,
214
240
  operations: DashboardMutationOperation[],
241
+ signal?: AbortSignal,
215
242
  ): Promise<string | null> {
216
243
  const scope = sqlValidationScope(operations);
217
244
  if (scope === null) return null;
218
- return validatePanelSql(config, scope === "all" ? undefined : scope);
245
+ return validatePanelSql(config, scope === "all" ? undefined : scope, {
246
+ signal,
247
+ });
219
248
  }
220
249
 
221
250
  function movedPanelIdsFrom(operations: DashboardMutationOperation[]): string[] {
@@ -239,11 +268,11 @@ function helpResult() {
239
268
 
240
269
  export default defineAction({
241
270
  description:
242
- "Apply general SQL dashboard edits through a small typed mutation API in ONE atomic save. " +
271
+ "Apply general SQL dashboard edits through a small typed mutation API in ONE atomic save. Prefer structured `operations` for agent calls; use the short `code` form only for compact layout/config edits. " +
243
272
  "Prefer this for dashboard layout and panel edits: move panels by id, edit titles/SQL/width/config, remove panels, duplicate panels, insert panels, or patch dashboard fields. " +
244
273
  "For user placement requests like 'second row' or 'next to return rates', use row-aware placement such as `dashboard.insertPanel(...).nextTo(\"retention-over-time\")` or `.atRow(2)`, then verify rendered rows from `get-sql-dashboard.layout.groups`. " +
245
274
  "This is code-shaped but not arbitrary code execution: the server parses the allowed dashboard methods, validates the resulting config with the same invariants as update-dashboard, saves once, syncs collab, and returns compact proof. First-party SQL must be explicitly time-bound as described in the API help; server validation rejects unbound first-party SQL. " +
246
- "The main code argument is a string, so it avoids brittle JSON-pointer indexes and native-array serialization issues. " +
275
+ "Structured operations avoid brittle JSON-pointer indexes and native-array serialization issues. Do not put a large multi-panel SQL payload in `code`; use `compose-dashboard` for catalog metrics or structured operations for a bounded custom edit. " +
247
276
  `Common example: ${DASHBOARD_MUTATION_EXAMPLES[0]}`,
248
277
  schema: z.object({
249
278
  dashboardId: z
@@ -254,7 +283,11 @@ export default defineAction({
254
283
  .string()
255
284
  .optional()
256
285
  .describe("Legacy alias for dashboardId. Prefer dashboardId."),
257
- code: z.string().optional().describe(apiHelp),
286
+ code: z
287
+ .string()
288
+ .max(MAX_DASHBOARD_MUTATION_CODE_LENGTH)
289
+ .optional()
290
+ .describe(apiHelp),
258
291
  operations: operationsInputSchema.describe(
259
292
  "Structured equivalent of the typed script. Native callers should pass an array of mutation ops; shell/legacy callers may pass a JSON string. " +
260
293
  "Supported ops: movePanels, removePanels, updatePanel, updatePanelPath, insertPanel, duplicatePanel, setDashboard, setFilterDefault.",
@@ -288,7 +321,8 @@ export default defineAction({
288
321
  height: 680,
289
322
  }),
290
323
  },
291
- run: async (args) => {
324
+ timeoutMs: 35_000,
325
+ run: async (args, actionContext) => {
292
326
  const code = nonEmptyCode(args.code);
293
327
  const requestedOperations = nonEmptyOperations(args.operations);
294
328
  const wantsHelpOnly =
@@ -357,7 +391,11 @@ export default defineAction({
357
391
  root = computed.nextRoot;
358
392
  operations = computed.nextOperations;
359
393
  mutation = computed.nextMutation;
360
- const sqlError = await validateMutationSql(root, operations);
394
+ const sqlError = await validateMutationSql(
395
+ root,
396
+ operations,
397
+ actionContext?.signal,
398
+ );
361
399
  if (sqlError) throw new Error(sqlError);
362
400
  } else {
363
401
  const saved = await upsertDashboardWithRetry(
@@ -368,6 +406,7 @@ export default defineAction({
368
406
  const sqlError = await validateMutationSql(
369
407
  computed.nextRoot,
370
408
  computed.nextOperations,
409
+ actionContext?.signal,
371
410
  );
372
411
  if (sqlError) throw new Error(sqlError);
373
412
  root = computed.nextRoot;
@@ -16,6 +16,7 @@ import {
16
16
  } from "../server/lib/dashboards-store";
17
17
  import { parseDemoDescriptor } from "../server/lib/demo-source";
18
18
  import { validateFirstPartyAnalyticsSql } from "../server/lib/first-party-analytics.js";
19
+ import { DASHBOARD_SQL_VALIDATION_TIMEOUT_MS } from "../shared/dashboard-report-timeouts.js";
19
20
  import {
20
21
  applyPanelOrder,
21
22
  compactDashboardResult,
@@ -388,15 +389,30 @@ export function validateDashboardConfig(
388
389
  return null;
389
390
  }
390
391
 
392
+ const MAX_CONCURRENT_SQL_VALIDATIONS = 8;
393
+
394
+ export interface ValidatePanelSqlOptions {
395
+ signal?: AbortSignal;
396
+ }
397
+
391
398
  /** Validate every query panel, or only the supplied ids for a targeted edit. */
392
399
  export async function validatePanelSql(
393
400
  config: Record<string, unknown>,
394
401
  panelIds?: ReadonlySet<string>,
402
+ options: ValidatePanelSqlOptions = {},
395
403
  ): Promise<string | null> {
396
404
  const panels = config.panels;
397
405
  if (!Array.isArray(panels)) return null;
398
406
  const vars = buildDryRunVars(config);
407
+ const bigQueryPanels: Array<{
408
+ index: number;
409
+ panel: Record<string, unknown>;
410
+ sql: string;
411
+ }> = [];
399
412
  for (let i = 0; i < panels.length; i++) {
413
+ if (options.signal?.aborted) {
414
+ throw new Error("Dashboard SQL validation was cancelled");
415
+ }
400
416
  const p = panels[i] as Record<string, unknown>;
401
417
  if (panelIds && (typeof p.id !== "string" || !panelIds.has(p.id))) {
402
418
  continue;
@@ -458,14 +474,72 @@ export async function validatePanelSql(
458
474
  if (!raw.trim()) continue;
459
475
  const sql = interpolate(raw, vars);
460
476
  if (!sql.trim()) continue;
461
- let err: string | null;
462
- try {
463
- err = await dryRunQuery(sql);
464
- } catch (e: any) {
465
- err = e?.message ?? String(e);
477
+ bigQueryPanels.push({ index: i, panel: p, sql });
478
+ }
479
+
480
+ if (bigQueryPanels.length === 0) return null;
481
+
482
+ // A dashboard save is one logical operation. Validate the selected BigQuery
483
+ // panels as one bounded batch so a slow panel cannot multiply the per-query
484
+ // timeout by the number of panels in the mutation.
485
+ const validationController = new AbortController();
486
+ const abortFromCaller = () => validationController.abort();
487
+ options.signal?.addEventListener("abort", abortFromCaller, { once: true });
488
+ let timedOut = false;
489
+ const timeout = setTimeout(() => {
490
+ timedOut = true;
491
+ validationController.abort();
492
+ }, DASHBOARD_SQL_VALIDATION_TIMEOUT_MS);
493
+ const errors: Array<string | null> = Array(bigQueryPanels.length).fill(null);
494
+ let nextIndex = 0;
495
+
496
+ const worker = async () => {
497
+ while (!validationController.signal.aborted) {
498
+ const taskIndex = nextIndex++;
499
+ const task = bigQueryPanels[taskIndex];
500
+ if (!task) return;
501
+
502
+ let err: string | null;
503
+ try {
504
+ err = await dryRunQuery(task.sql, {
505
+ signal: validationController.signal,
506
+ });
507
+ } catch (e: any) {
508
+ err = e?.message ?? String(e);
509
+ }
510
+ errors[taskIndex] = err;
466
511
  }
512
+ };
513
+
514
+ try {
515
+ await Promise.all(
516
+ Array.from(
517
+ {
518
+ length: Math.min(
519
+ MAX_CONCURRENT_SQL_VALIDATIONS,
520
+ bigQueryPanels.length,
521
+ ),
522
+ },
523
+ () => worker(),
524
+ ),
525
+ );
526
+ } finally {
527
+ clearTimeout(timeout);
528
+ options.signal?.removeEventListener("abort", abortFromCaller);
529
+ }
530
+
531
+ if (options.signal?.aborted && !timedOut) {
532
+ throw new Error("Dashboard SQL validation was cancelled");
533
+ }
534
+ if (timedOut) {
535
+ return `Dashboard SQL validation timed out after ${Math.round(DASHBOARD_SQL_VALIDATION_TIMEOUT_MS / 1000)} seconds`;
536
+ }
537
+
538
+ for (let i = 0; i < bigQueryPanels.length; i++) {
539
+ const err = errors[i];
467
540
  if (err) {
468
- return `panel[${i}] "${p.title || p.id}" SQL is invalid: ${err}`;
541
+ const task = bigQueryPanels[i];
542
+ return `panel[${task.index}] "${task.panel.title || task.panel.id}" SQL is invalid: ${err}`;
469
543
  }
470
544
  }
471
545
  return null;
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-07
4
+ ---
5
+
6
+ Dashboard creation and edits now save faster with reliable structured mutations