@agent-native/core 0.99.0 → 0.99.2

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 (184) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +23 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +33 -38
  5. package/corpus/core/src/audit/config.ts +0 -1
  6. package/corpus/core/src/chat-threads/store.ts +57 -41
  7. package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
  8. package/corpus/core/src/client/AssistantChat.tsx +1 -2
  9. package/corpus/core/src/client/analytics.ts +13 -0
  10. package/corpus/core/src/client/chat/message-components.tsx +15 -3
  11. package/corpus/core/src/client/chat/tool-call-display.tsx +18 -1
  12. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +5 -9
  13. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +0 -133
  14. package/corpus/core/src/client/index.ts +7 -0
  15. package/corpus/core/src/client/settings/DemoModeSection.tsx +9 -65
  16. package/corpus/core/src/client/settings/SettingsPanel.tsx +1 -1
  17. package/corpus/core/src/client/use-db-sync.ts +3 -15
  18. package/corpus/core/src/client/use-demo-mode-status.ts +16 -30
  19. package/corpus/core/src/demo/browser-state.ts +47 -0
  20. package/corpus/core/src/demo/fetch-interceptor.ts +12 -59
  21. package/corpus/core/src/deploy/workspace-deploy.ts +2 -2
  22. package/corpus/core/src/server/action-discovery.ts +0 -1
  23. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +13 -0
  24. package/corpus/core/src/server/agent-teams.ts +17 -0
  25. package/corpus/core/src/server/core-routes-plugin.ts +0 -30
  26. package/corpus/core/src/server/google-oauth.ts +6 -2
  27. package/corpus/core/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  28. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +48 -7
  29. package/corpus/templates/analytics/AGENTS.md +4 -4
  30. package/corpus/templates/analytics/actions/compose-dashboard.ts +12 -1
  31. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +15 -4
  32. package/corpus/templates/analytics/actions/mutate-dashboard.ts +2 -1
  33. package/corpus/templates/analytics/actions/update-dashboard.ts +14 -0
  34. package/corpus/templates/analytics/app/assets/replay-cursor.svg +17 -0
  35. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +4 -3
  36. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +21 -27
  37. package/corpus/templates/analytics/app/global.css +17 -9
  38. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +32 -4
  39. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -1
  40. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +21 -0
  41. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +14 -0
  42. package/corpus/templates/analytics/app/pages/monitoring/errors/IssueDetail.tsx +1 -1
  43. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +77 -23
  44. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
  45. package/corpus/templates/analytics/changelog/2026-07-13-all-time-dashboard-filters-no-longer-send-invalid-dates-to-b.md +6 -0
  46. package/corpus/templates/analytics/changelog/2026-07-13-dashboard-charts-now-honor-the-selected-time-range-by-defaul.md +6 -0
  47. package/corpus/templates/analytics/changelog/2026-07-13-error-issue-titles-wrap-instead-of-overflowing.md +6 -0
  48. package/corpus/templates/analytics/changelog/2026-07-13-keep-real-authorized-identities-in-analytics-incident-lookup.md +6 -0
  49. package/corpus/templates/analytics/changelog/2026-07-13-saving-a-new-dashboard-view-no-longer-fails-with-a-server-er.md +6 -0
  50. package/corpus/templates/analytics/changelog/2026-07-13-session-replay-cursors-stay-hidden-until-their-first-recorde.md +6 -0
  51. package/corpus/templates/analytics/changelog/2026-07-13-session-replay-links-now-support-cmd-click-and-middle-click-.md +6 -0
  52. package/corpus/templates/analytics/changelog/2026-07-13-the-analytics-agents-tab-is-available-again-from-the-main-na.md +6 -0
  53. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +3 -3
  54. package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +4 -0
  55. package/corpus/templates/analytics/server/lib/dashboard-time-scope.ts +170 -0
  56. package/corpus/templates/analytics/server/lib/dashboards-store.ts +43 -4
  57. package/corpus/templates/analytics/server/lib/error-capture.ts +30 -67
  58. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +20 -5
  59. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +1 -1
  60. package/corpus/templates/analytics/server/lib/session-replay.ts +5 -111
  61. package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -0
  62. package/corpus/templates/assets/app/routes/library.tsx +3 -4
  63. package/corpus/templates/assets/changelog/2026-07-13-internal-library-links-open-in-new-tabs.md +6 -0
  64. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +43 -40
  65. package/corpus/templates/calendar/app/pages/ManageBookingPage.tsx +6 -8
  66. package/corpus/templates/calendar/changelog/2026-07-13-booking-links-and-previews-open-in-new-tabs.md +6 -0
  67. package/corpus/templates/clips/app/components/library/recording-card.tsx +27 -26
  68. package/corpus/templates/clips/app/components/library/space-card.tsx +4 -6
  69. package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +4 -7
  70. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +19 -9
  71. package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -3
  72. package/corpus/templates/clips/changelog/2026-07-13-recording-links-open-in-new-tabs.md +6 -0
  73. package/corpus/templates/content/app/components/editor/ContentReferencePreview.tsx +6 -8
  74. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +5 -3
  75. package/corpus/templates/content/app/components/editor/database/FormView.tsx +6 -9
  76. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +18 -16
  77. package/corpus/templates/content/changelog/2026-07-13-document-links-open-in-new-tabs.md +6 -0
  78. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +33 -16
  79. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -1
  80. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +4 -4
  81. package/corpus/templates/design/app/pages/DesignSystems.tsx +8 -10
  82. package/corpus/templates/design/app/pages/Index.tsx +6 -1
  83. package/corpus/templates/design/app/pages/Present.tsx +12 -6
  84. package/corpus/templates/design/changelog/2026-07-13-design-navigation-links-open-in-new-tabs.md +6 -0
  85. package/corpus/templates/design/changelog/2026-07-13-design-session-replays-include-preview-iframes.md +6 -0
  86. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -11
  87. package/corpus/templates/forms/app/pages/FormsListPage.tsx +6 -8
  88. package/corpus/templates/forms/changelog/2026-07-13-form-navigation-links-open-in-new-tabs.md +6 -0
  89. package/corpus/templates/mail/app/pages/DraftQueuePage.tsx +3 -8
  90. package/corpus/templates/mail/changelog/2026-07-13-settings-links-open-in-new-tabs.md +6 -0
  91. package/dist/agent/production-agent.d.ts +9 -0
  92. package/dist/agent/production-agent.d.ts.map +1 -1
  93. package/dist/agent/production-agent.js +25 -37
  94. package/dist/agent/production-agent.js.map +1 -1
  95. package/dist/audit/config.d.ts.map +1 -1
  96. package/dist/audit/config.js +0 -1
  97. package/dist/audit/config.js.map +1 -1
  98. package/dist/chat-threads/store.d.ts.map +1 -1
  99. package/dist/chat-threads/store.js +47 -33
  100. package/dist/chat-threads/store.js.map +1 -1
  101. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  102. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  103. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  104. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  105. package/dist/client/AssistantChat.d.ts.map +1 -1
  106. package/dist/client/AssistantChat.js +1 -1
  107. package/dist/client/AssistantChat.js.map +1 -1
  108. package/dist/client/analytics.d.ts.map +1 -1
  109. package/dist/client/analytics.js +11 -0
  110. package/dist/client/analytics.js.map +1 -1
  111. package/dist/client/chat/message-components.d.ts.map +1 -1
  112. package/dist/client/chat/message-components.js +9 -3
  113. package/dist/client/chat/message-components.js.map +1 -1
  114. package/dist/client/chat/tool-call-display.d.ts +3 -1
  115. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  116. package/dist/client/chat/tool-call-display.js +10 -2
  117. package/dist/client/chat/tool-call-display.js.map +1 -1
  118. package/dist/client/extensions/EmbeddedExtension.js +2 -6
  119. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  120. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  121. package/dist/client/extensions/ExtensionViewer.js +2 -59
  122. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  123. package/dist/client/index.d.ts +2 -0
  124. package/dist/client/index.d.ts.map +1 -1
  125. package/dist/client/index.js +2 -0
  126. package/dist/client/index.js.map +1 -1
  127. package/dist/client/settings/DemoModeSection.d.ts +1 -13
  128. package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
  129. package/dist/client/settings/DemoModeSection.js +5 -52
  130. package/dist/client/settings/DemoModeSection.js.map +1 -1
  131. package/dist/client/settings/SettingsPanel.js +1 -1
  132. package/dist/client/settings/SettingsPanel.js.map +1 -1
  133. package/dist/client/use-db-sync.d.ts.map +1 -1
  134. package/dist/client/use-db-sync.js +3 -7
  135. package/dist/client/use-db-sync.js.map +1 -1
  136. package/dist/client/use-demo-mode-status.d.ts +3 -4
  137. package/dist/client/use-demo-mode-status.d.ts.map +1 -1
  138. package/dist/client/use-demo-mode-status.js +8 -23
  139. package/dist/client/use-demo-mode-status.js.map +1 -1
  140. package/dist/collab/struct-routes.d.ts +1 -1
  141. package/dist/demo/browser-state.d.ts +10 -0
  142. package/dist/demo/browser-state.d.ts.map +1 -0
  143. package/dist/demo/browser-state.js +49 -0
  144. package/dist/demo/browser-state.js.map +1 -0
  145. package/dist/demo/fetch-interceptor.d.ts +2 -7
  146. package/dist/demo/fetch-interceptor.d.ts.map +1 -1
  147. package/dist/demo/fetch-interceptor.js +12 -56
  148. package/dist/demo/fetch-interceptor.js.map +1 -1
  149. package/dist/deploy/workspace-deploy.js +2 -2
  150. package/dist/deploy/workspace-deploy.js.map +1 -1
  151. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  152. package/dist/notifications/routes.d.ts +2 -2
  153. package/dist/observability/routes.d.ts +5 -5
  154. package/dist/resources/handlers.d.ts +3 -3
  155. package/dist/secrets/routes.d.ts +9 -9
  156. package/dist/server/action-discovery.d.ts.map +1 -1
  157. package/dist/server/action-discovery.js +0 -1
  158. package/dist/server/action-discovery.js.map +1 -1
  159. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  160. package/dist/server/agent-chat/action-filters-a2a.js +11 -1
  161. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  162. package/dist/server/agent-teams.d.ts.map +1 -1
  163. package/dist/server/agent-teams.js +15 -1
  164. package/dist/server/agent-teams.js.map +1 -1
  165. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  166. package/dist/server/core-routes-plugin.js +0 -27
  167. package/dist/server/core-routes-plugin.js.map +1 -1
  168. package/dist/server/google-oauth.d.ts.map +1 -1
  169. package/dist/server/google-oauth.js +6 -2
  170. package/dist/server/google-oauth.js.map +1 -1
  171. package/dist/server/transcribe-voice.d.ts +1 -1
  172. package/dist/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  173. package/package.json +2 -2
  174. package/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  175. package/corpus/core/src/demo/actions/toggle-demo-mode.ts +0 -23
  176. package/corpus/core/src/demo/config.ts +0 -57
  177. package/dist/demo/actions/toggle-demo-mode.d.ts +0 -8
  178. package/dist/demo/actions/toggle-demo-mode.d.ts.map +0 -1
  179. package/dist/demo/actions/toggle-demo-mode.js +0 -21
  180. package/dist/demo/actions/toggle-demo-mode.js.map +0 -1
  181. package/dist/demo/config.d.ts +0 -9
  182. package/dist/demo/config.d.ts.map +0 -1
  183. package/dist/demo/config.js +0 -58
  184. package/dist/demo/config.js.map +0 -1
@@ -0,0 +1,170 @@
1
+ import type { DashboardTimeScope } from "../../app/pages/adhoc/sql-dashboard/types";
2
+
3
+ export const DASHBOARD_TIME_SCOPES: readonly DashboardTimeScope[] = [
4
+ "dashboard",
5
+ "fixed-window",
6
+ "cohort-history",
7
+ "all-time",
8
+ ];
9
+
10
+ type DashboardFilterLike = {
11
+ id?: unknown;
12
+ key?: unknown;
13
+ type?: unknown;
14
+ default?: unknown;
15
+ };
16
+
17
+ type DashboardPanelLike = {
18
+ id?: unknown;
19
+ title?: unknown;
20
+ chartType?: unknown;
21
+ source?: unknown;
22
+ sql?: unknown;
23
+ config?: unknown;
24
+ };
25
+
26
+ type DashboardConfigLike = {
27
+ filters?: unknown;
28
+ };
29
+
30
+ const TEMPORAL_VARIABLE_RE = /\{\{(timeRange|[A-Za-z_]\w*(?:Start|End))\}\}/g;
31
+
32
+ /** Return the time variables a panel actually references. */
33
+ export function extractDashboardTimeVariables(sql: string): string[] {
34
+ const variables = new Set<string>();
35
+ for (const match of sql.matchAll(TEMPORAL_VARIABLE_RE)) {
36
+ variables.add(match[1]);
37
+ }
38
+ return [...variables];
39
+ }
40
+
41
+ function filterId(filter: DashboardFilterLike): string {
42
+ const value = filter.id ?? filter.key;
43
+ return typeof value === "string" ? value.trim() : "";
44
+ }
45
+
46
+ function filtersFrom(config: DashboardConfigLike): DashboardFilterLike[] {
47
+ return Array.isArray(config.filters)
48
+ ? (config.filters as DashboardFilterLike[])
49
+ : [];
50
+ }
51
+
52
+ function panelConfig(panel: DashboardPanelLike): Record<string, unknown> {
53
+ return panel.config && typeof panel.config === "object"
54
+ ? (panel.config as Record<string, unknown>)
55
+ : {};
56
+ }
57
+
58
+ function hasExplicitLowerBound(sql: string): boolean {
59
+ // This intentionally recognizes the bounded shapes used by the shipped
60
+ // first-party catalog. It is a compatibility escape hatch for fixed-window
61
+ // catalog metrics; new ad-hoc panels should use a dashboard placeholder.
62
+ return /\b(?:event_date|timestamp|started_at|ended_at|cohort_date|created_at|date)\b[\s\S]{0,100}?(?:>=|>)\s*[\s\S]{0,160}?(?:CURRENT_DATE|CURRENT_TIMESTAMP|NOW\s*\(|INTERVAL\s*['"]|DATE\s*['"]|TIMESTAMP\s*['"]|\b20\d{2}-\d{2}-\d{2}\b)/i.test(
63
+ sql,
64
+ );
65
+ }
66
+
67
+ function hasIntentionalHistoryDescription(
68
+ panel: DashboardPanelLike,
69
+ config: Record<string, unknown>,
70
+ ): boolean {
71
+ const values = [panel.title, config.description].filter(
72
+ (value): value is string => typeof value === "string",
73
+ );
74
+ return /all[- ]?time|lifetime|histor(?:y|ical)/i.test(values.join(" "));
75
+ }
76
+
77
+ function scopeValue(panel: DashboardPanelLike): DashboardTimeScope | undefined {
78
+ const value = panelConfig(panel).timeScope;
79
+ return typeof value === "string" &&
80
+ (DASHBOARD_TIME_SCOPES as readonly string[]).includes(value)
81
+ ? (value as DashboardTimeScope)
82
+ : undefined;
83
+ }
84
+
85
+ /**
86
+ * Validate the temporal contract for a first-party dashboard panel.
87
+ *
88
+ * Ordinary panels must bind to a dashboard filter. Intentional exceptions are
89
+ * explicit in `config.timeScope`, while legacy fixed-window catalog SQL is
90
+ * accepted when its lower bound is visible in the SQL itself.
91
+ */
92
+ export function validateFirstPartyDashboardTimeScope(
93
+ panel: DashboardPanelLike,
94
+ dashboard: DashboardConfigLike,
95
+ index: number,
96
+ ): string | null {
97
+ if (panel.source !== "first-party") return null;
98
+ if (panel.chartType === "section" || panel.chartType === "extension") {
99
+ return null;
100
+ }
101
+
102
+ const sql = typeof panel.sql === "string" ? panel.sql : "";
103
+ const variables = extractDashboardTimeVariables(sql);
104
+ const filters = filtersFrom(dashboard);
105
+ const label =
106
+ typeof panel.title === "string" && panel.title.trim()
107
+ ? `"${panel.title}"`
108
+ : `at index ${index}`;
109
+
110
+ for (const variable of variables) {
111
+ const expectedFilterId =
112
+ variable === "timeRange"
113
+ ? "timeRange"
114
+ : variable.replace(/(?:Start|End)$/, "");
115
+ const filter = filters.find(
116
+ (candidate) => filterId(candidate) === expectedFilterId,
117
+ );
118
+ if (!filter) {
119
+ return `panel[${index}] ${label} uses {{${variable}}} but config.filters has no matching "${expectedFilterId}" filter; bind first-party SQL to a declared dashboard time filter`;
120
+ }
121
+ if (variable === "timeRange") {
122
+ if (filter.type !== "select") {
123
+ return `panel[${index}] ${label} uses {{timeRange}}, but filter "timeRange" must have type "select"`;
124
+ }
125
+ } else if (filter.type !== "date-range") {
126
+ return `panel[${index}] ${label} uses {{${variable}}}, but filter "${expectedFilterId}" must have type "date-range"`;
127
+ }
128
+ if (typeof filter.default !== "string" || !filter.default.trim()) {
129
+ return `panel[${index}] ${label} uses a time filter without a non-empty default; choose a bounded default such as "90d" so missing filter state cannot become all-time`;
130
+ }
131
+ }
132
+
133
+ const configuredScope = panelConfig(panel).timeScope;
134
+ if (
135
+ configuredScope !== undefined &&
136
+ (typeof configuredScope !== "string" ||
137
+ !(DASHBOARD_TIME_SCOPES as readonly string[]).includes(configuredScope))
138
+ ) {
139
+ return `panel[${index}] ${label} config.timeScope must be one of ${DASHBOARD_TIME_SCOPES.join(", ")}`;
140
+ }
141
+
142
+ const scope = scopeValue(panel);
143
+ const hasDashboardBinding = variables.length > 0;
144
+ const hasLowerBound = hasExplicitLowerBound(sql);
145
+
146
+ if (scope === "dashboard" && !hasDashboardBinding) {
147
+ return `panel[${index}] ${label} declares timeScope "dashboard" but does not reference {{timeRange}} or a date-range variable`;
148
+ }
149
+ if (scope === "fixed-window" && hasDashboardBinding) {
150
+ return `panel[${index}] ${label} declares timeScope "fixed-window" but uses dashboard time variables; use timeScope "dashboard" or remove the placeholders`;
151
+ }
152
+ if (scope === "fixed-window" && !hasLowerBound) {
153
+ return `panel[${index}] ${label} declares timeScope "fixed-window" without a recognizable lower date bound`;
154
+ }
155
+ if (scope === "all-time" && hasDashboardBinding) {
156
+ return `panel[${index}] ${label} declares timeScope "all-time" but uses dashboard time variables; use timeScope "dashboard" for filter-bound output`;
157
+ }
158
+ if (
159
+ scope === "all-time" &&
160
+ !hasIntentionalHistoryDescription(panel, panelConfig(panel))
161
+ ) {
162
+ return `panel[${index}] ${label} declares timeScope "all-time"; add a description or title that says lifetime, historical, or all-time so the exception is explicit`;
163
+ }
164
+
165
+ if (!scope && !hasDashboardBinding && !hasLowerBound) {
166
+ return `panel[${index}] ${label} reads first-party analytics without a time bound; use {{timeRange}} with a non-empty default filter, or explicitly set config.timeScope to "cohort-history" or "all-time" for intentional history scans`;
167
+ }
168
+
169
+ return null;
170
+ }
@@ -1846,12 +1846,38 @@ export async function saveDashboardView(
1846
1846
  orgId: ctx.orgId ?? undefined,
1847
1847
  });
1848
1848
  const db = getDb() as any;
1849
- const id = view.id ?? nanoidFallback();
1849
+ let id = view.id ?? nanoidFallback();
1850
+ let existing = false;
1850
1851
  if (view.id) {
1852
+ const [existingRow] = await db
1853
+ .select({
1854
+ id: schema.dashboardViews.id,
1855
+ dashboardId: schema.dashboardViews.dashboardId,
1856
+ })
1857
+ .from(schema.dashboardViews)
1858
+ .where(eq(schema.dashboardViews.id, view.id))
1859
+ .limit(1);
1860
+ if (existingRow?.dashboardId === dashboardId) {
1861
+ existing = true;
1862
+ } else if (existingRow) {
1863
+ // View ids are generated from names in the browser but remain globally
1864
+ // primary-keyed for backwards compatibility. Avoid colliding with a
1865
+ // same-named view on another dashboard instead of updating that row or
1866
+ // returning a raw unique-constraint 500.
1867
+ id = nanoidFallback();
1868
+ }
1869
+ }
1870
+
1871
+ if (existing) {
1851
1872
  await db
1852
1873
  .update(schema.dashboardViews)
1853
1874
  .set({ name: view.name, filters: JSON.stringify(view.filters) })
1854
- .where(eq(schema.dashboardViews.id, id));
1875
+ .where(
1876
+ and(
1877
+ eq(schema.dashboardViews.id, id),
1878
+ eq(schema.dashboardViews.dashboardId, dashboardId),
1879
+ ),
1880
+ );
1855
1881
  } else {
1856
1882
  await db.insert(schema.dashboardViews).values({
1857
1883
  id,
@@ -1864,7 +1890,15 @@ export async function saveDashboardView(
1864
1890
  const [row] = await db
1865
1891
  .select()
1866
1892
  .from(schema.dashboardViews)
1867
- .where(eq(schema.dashboardViews.id, id));
1893
+ .where(
1894
+ and(
1895
+ eq(schema.dashboardViews.id, id),
1896
+ eq(schema.dashboardViews.dashboardId, dashboardId),
1897
+ ),
1898
+ );
1899
+ if (!row) {
1900
+ throw new Error("Dashboard view was not persisted");
1901
+ }
1868
1902
  const dash = await getDashboard(dashboardId, ctx);
1869
1903
  if (dash) {
1870
1904
  recordScopedChange(
@@ -1891,7 +1925,12 @@ export async function deleteDashboardView(
1891
1925
  const db = getDb() as any;
1892
1926
  await db
1893
1927
  .delete(schema.dashboardViews)
1894
- .where(eq(schema.dashboardViews.id, viewId));
1928
+ .where(
1929
+ and(
1930
+ eq(schema.dashboardViews.id, viewId),
1931
+ eq(schema.dashboardViews.dashboardId, dashboardId),
1932
+ ),
1933
+ );
1895
1934
  const dash = await getDashboard(dashboardId, ctx);
1896
1935
  if (dash) {
1897
1936
  recordScopedChange(
@@ -18,7 +18,6 @@ import { fileURLToPath } from "node:url";
18
18
  * reads go through `accessFilter` so an org-scoped key surfaces its issues to
19
19
  * the whole org exactly like session recordings.
20
20
  */
21
- import { appStateGet } from "@agent-native/core/application-state";
22
21
  import { notifyWithDelivery } from "@agent-native/core/notifications";
23
22
  import { recordChange } from "@agent-native/core/server";
24
23
  import { accessFilter } from "@agent-native/core/sharing";
@@ -547,6 +546,27 @@ export interface RawExceptionInput {
547
546
  breadcrumbs: unknown[];
548
547
  }
549
548
 
549
+ /**
550
+ * Browser request cancellation is expected during navigation and query
551
+ * invalidation. Keep the first-party issue store aligned with the client
552
+ * Sentry filter, while preserving other AbortError failures for triage.
553
+ */
554
+ export function isBenignBrowserAbortException(
555
+ input: Pick<RawExceptionInput, "type" | "message">,
556
+ ): boolean {
557
+ const exceptionType = input.type.trim().toLowerCase();
558
+ const exceptionValue = input.message.trim().toLowerCase();
559
+ return (
560
+ exceptionValue === "the user aborted a request." ||
561
+ exceptionValue === "signal is aborted without reason" ||
562
+ exceptionValue === "aborterror: the user aborted a request." ||
563
+ exceptionValue === "aborterror: signal is aborted without reason" ||
564
+ (exceptionType === "aborterror" &&
565
+ (exceptionValue.includes("the user aborted a request") ||
566
+ exceptionValue.includes("signal is aborted without reason")))
567
+ );
568
+ }
569
+
550
570
  function nowIso(): string {
551
571
  return new Date().toISOString();
552
572
  }
@@ -930,11 +950,9 @@ export async function ingestAnalyticsExceptionEvents(
930
950
  let ingested = 0;
931
951
  for (const item of events) {
932
952
  try {
933
- await ingestException(
934
- scope,
935
- extractExceptionInput(item.properties),
936
- item.derived,
937
- );
953
+ const raw = extractExceptionInput(item.properties);
954
+ if (isBenignBrowserAbortException(raw)) continue;
955
+ await ingestException(scope, raw, item.derived);
938
956
  ingested += 1;
939
957
  } catch (error) {
940
958
  console.warn("[error-capture] Failed to ingest exception event:", error);
@@ -1080,56 +1098,6 @@ export interface ListErrorIssuesFilters {
1080
1098
  limit?: number;
1081
1099
  }
1082
1100
 
1083
- export const ERROR_REPORTING_ANONYMOUS_EMAIL = "anonymous@builder.io";
1084
- const ERROR_REPORTING_EMAIL_PATTERN = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
1085
-
1086
- /**
1087
- * Error reports contain emails in identity fields, messages, stacks, URLs,
1088
- * tags, extra context, and breadcrumbs. Redact every string at this owning
1089
- * read seam so demo mode never depends on browser interception timing.
1090
- */
1091
- export function anonymizeErrorReportingEmails<T>(value: T): T {
1092
- if (typeof value === "string") {
1093
- return value.replace(
1094
- ERROR_REPORTING_EMAIL_PATTERN,
1095
- ERROR_REPORTING_ANONYMOUS_EMAIL,
1096
- ) as T;
1097
- }
1098
- if (Array.isArray(value)) {
1099
- return value.map((item) => anonymizeErrorReportingEmails(item)) as T;
1100
- }
1101
- if (value && typeof value === "object") {
1102
- if (value instanceof Date) return value;
1103
- const output: Record<string, unknown> = {};
1104
- for (const [key, entry] of Object.entries(value)) {
1105
- output[key] = anonymizeErrorReportingEmails(entry);
1106
- }
1107
- return output as T;
1108
- }
1109
- return value;
1110
- }
1111
-
1112
- async function isErrorDemoModeEnabled(
1113
- userEmail?: string | null,
1114
- ): Promise<boolean> {
1115
- if (process.env.DEMO_MODE === "true") return true;
1116
- if (!userEmail) return false;
1117
- try {
1118
- // Keep demo state tied to the caller whose issues are being read.
1119
- const state = await appStateGet(userEmail, "demo-mode");
1120
- return state?.enabled === true;
1121
- } catch {
1122
- return false;
1123
- }
1124
- }
1125
-
1126
- function anonymizeErrorReportingEmailsInDemoMode<T>(
1127
- value: T,
1128
- enabled: boolean,
1129
- ): T {
1130
- return enabled ? anonymizeErrorReportingEmails(value) : value;
1131
- }
1132
-
1133
1101
  export interface ErrorIssueSummary {
1134
1102
  id: string;
1135
1103
  fingerprint: string;
@@ -1202,7 +1170,6 @@ export async function listErrorIssues(
1202
1170
  scope: ErrorReadScope,
1203
1171
  filters: ListErrorIssuesFilters = {},
1204
1172
  ): Promise<ErrorIssueSummary[]> {
1205
- const demoModeEnabled = isErrorDemoModeEnabled(scope.userEmail);
1206
1173
  const db = getDb() as any;
1207
1174
  const limit = Math.min(
1208
1175
  MAX_ISSUE_LIMIT,
@@ -1293,7 +1260,7 @@ export async function listErrorIssues(
1293
1260
  template: row.template ?? null,
1294
1261
  sparkline: sparklines.get(row.id) ?? new Array(SPARKLINE_DAYS).fill(0),
1295
1262
  }));
1296
- return anonymizeErrorReportingEmailsInDemoMode(issues, await demoModeEnabled);
1263
+ return issues;
1297
1264
  }
1298
1265
 
1299
1266
  export interface ErrorEventDetail {
@@ -1385,7 +1352,6 @@ export async function getErrorIssue(
1385
1352
  issueId: string,
1386
1353
  options: { eventsLimit?: number } = {},
1387
1354
  ): Promise<ErrorIssueDetail> {
1388
- const demoModeEnabled = isErrorDemoModeEnabled(scope.userEmail);
1389
1355
  const db = getDb() as any;
1390
1356
  const [issueRow] = await db
1391
1357
  .select()
@@ -1511,14 +1477,11 @@ export async function getErrorIssue(
1511
1477
  sparkline: sparklines.get(issueId) ?? new Array(SPARKLINE_DAYS).fill(0),
1512
1478
  };
1513
1479
 
1514
- return anonymizeErrorReportingEmailsInDemoMode(
1515
- {
1516
- issue,
1517
- events,
1518
- sessions: Array.from(sessions.values()),
1519
- },
1520
- await demoModeEnabled,
1521
- );
1480
+ return {
1481
+ issue,
1482
+ events,
1483
+ sessions: Array.from(sessions.values()),
1484
+ };
1522
1485
  }
1523
1486
 
1524
1487
  export interface UpdateErrorIssueInput {
@@ -163,6 +163,7 @@ const DASHBOARD_WAU_BASE_RANGE_FILTER = dashboardLookbackTimeRangeFilter(
163
163
  );
164
164
  const DASHBOARD_EMAIL_FILTER =
165
165
  "('{{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'))";
166
+ const SESSION_STATUS_FILTER = `event_name = 'session status' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}`;
166
167
  const SIGNED_IN_ACTIVITY_KEY_SQL = USER_KEY_SQL;
167
168
  const SIGNED_IN_ACTIVITY_FILTER = `event_name = 'session status' AND signed_in = 'true' AND ${SIGNED_IN_ACTIVITY_KEY_SQL} IS NOT NULL`;
168
169
  const SIGNED_IN_PRODUCT_ACTIVITY_FILTER = `${SIGNED_IN_ACTIVITY_FILTER} AND ${PRODUCT_ACTIVITY_TEMPLATE_FILTER}`;
@@ -292,7 +293,7 @@ const ENTRIES: FirstPartyMetric[] = [
292
293
  width: 2,
293
294
  windowed: false,
294
295
  buildSql: fixed(
295
- "SELECT COALESCE(NULLIF(app, ''), 'unknown') AS app, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' GROUP BY COALESCE(NULLIF(app, ''), 'unknown') ORDER BY count DESC LIMIT 20",
296
+ `SELECT COALESCE(NULLIF(app, ''), 'unknown') AS app, COUNT(*) AS count FROM analytics_events WHERE ${SESSION_STATUS_FILTER} GROUP BY COALESCE(NULLIF(app, ''), 'unknown') ORDER BY count DESC LIMIT 20`,
296
297
  ),
297
298
  config: {
298
299
  xKey: "app",
@@ -310,7 +311,7 @@ const ENTRIES: FirstPartyMetric[] = [
310
311
  width: 1,
311
312
  windowed: false,
312
313
  buildSql: fixed(
313
- `SELECT ${EVENT_DATE_SQL} AS date, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' GROUP BY ${EVENT_DATE_SQL} ORDER BY date`,
314
+ `SELECT ${EVENT_DATE_SQL} AS date, COUNT(*) AS count FROM analytics_events WHERE ${SESSION_STATUS_FILTER} GROUP BY ${EVENT_DATE_SQL} ORDER BY date`,
314
315
  ),
315
316
  config: { xKey: "date", yKey: "count", color: "#10b981" },
316
317
  },
@@ -380,7 +381,7 @@ const ENTRIES: FirstPartyMetric[] = [
380
381
  width: 1,
381
382
  windowed: false,
382
383
  buildSql: fixed(
383
- "SELECT COALESCE(NULLIF(signed_in, ''), 'unknown') AS signed_in, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' GROUP BY COALESCE(NULLIF(signed_in, ''), 'unknown') ORDER BY signed_in",
384
+ `SELECT COALESCE(NULLIF(signed_in, ''), 'unknown') AS signed_in, COUNT(*) AS count FROM analytics_events WHERE ${SESSION_STATUS_FILTER} GROUP BY COALESCE(NULLIF(signed_in, ''), 'unknown') ORDER BY signed_in`,
384
385
  ),
385
386
  config: {
386
387
  xKey: "signed_in",
@@ -1007,14 +1008,28 @@ export function buildPanel(
1007
1008
  overrides.width <= 6
1008
1009
  ? overrides.width
1009
1010
  : metric.width;
1011
+ const title =
1012
+ overrides.title?.trim() ||
1013
+ (window === "all"
1014
+ ? metric.title.replace(/\(\d+d\)$/i, "(all-time)")
1015
+ : metric.title);
1016
+ const config = { ...metric.config };
1017
+ if (metric.windowed) {
1018
+ config.timeScope = window === "all" ? "all-time" : "fixed-window";
1019
+ if (window === "all") {
1020
+ const description =
1021
+ typeof config.description === "string" ? config.description.trim() : "";
1022
+ config.description = `${description ? `${description} ` : ""}This panel is explicitly configured for an all-time window.`;
1023
+ }
1024
+ }
1010
1025
  return {
1011
1026
  id: overrides.id?.trim() || metric.key,
1012
- title: overrides.title?.trim() || metric.title,
1027
+ title,
1013
1028
  chartType: overrides.chartType?.trim() || metric.chartType,
1014
1029
  source: "first-party",
1015
1030
  width,
1016
1031
  sql: metric.buildSql(window),
1017
1032
  // Clone so callers can't mutate the shared catalog config object.
1018
- config: { ...metric.config },
1033
+ config,
1019
1034
  };
1020
1035
  }
@@ -663,7 +663,7 @@ export function resolveSessionReplayAgentAccess(
663
663
  resourceId: recordingId,
664
664
  });
665
665
  // Replay grants are always minted by an authenticated viewer. Fail closed
666
- // for grants without that signed identity: demo-mode policy is viewer-scoped,
666
+ // for grants without that signed identity: access policy is viewer-scoped,
667
667
  // so guessing from the ambient request could expose identities to agents.
668
668
  if (!result.ok || !result.viewerEmail) return null;
669
669
  return { viewerEmail: result.viewerEmail };
@@ -2,7 +2,6 @@ import { Buffer } from "node:buffer";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import { gzipSync, gunzipSync } from "node:zlib";
4
4
 
5
- import { appStateGet } from "@agent-native/core/application-state";
6
5
  import {
7
6
  deletePrivateBlob,
8
7
  putPrivateBlob,
@@ -258,8 +257,6 @@ const DEFAULT_REPLAY_MAX_REQUESTS_PER_MINUTE = 120;
258
257
  const RETENTION_DELETE_BATCH_SIZE = 500;
259
258
  const REPLAY_PRIVATE_BLOB_REF_KIND = "agent-native.session-replay.private-blob";
260
259
  const REPLAY_PRIVATE_BLOB_REF_VERSION = 1;
261
- const SESSION_DEMO_EMAIL_PATTERN = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
262
- const SESSION_DEMO_ANONYMOUS_EMAIL = "anonymous@builder.io";
263
260
  let inlineReplayFallbackWarned = false;
264
261
 
265
262
  function replayError(message: string, statusCode: number): Error {
@@ -297,89 +294,6 @@ function replayEmail(value: unknown): string | null {
297
294
  return raw && raw.includes("@") ? raw : null;
298
295
  }
299
296
 
300
- function normalizedEmail(value: unknown): string | null {
301
- return replayEmail(value)?.toLowerCase() ?? null;
302
- }
303
-
304
- function isBuilderEmail(value: unknown): boolean {
305
- return normalizedEmail(value)?.endsWith("@builder.io") === true;
306
- }
307
-
308
- async function isSessionDemoModeEnabled(
309
- userEmail?: string | null,
310
- ): Promise<boolean> {
311
- if (process.env.DEMO_MODE === "true") return true;
312
- if (!userEmail) return false;
313
- try {
314
- // These reads also run from custom routes and actions, so use the
315
- // authenticated scope explicitly instead of ambient request state.
316
- const state = await appStateGet(userEmail, "demo-mode");
317
- return state?.enabled === true;
318
- } catch {
319
- return false;
320
- }
321
- }
322
-
323
- function demoBuilderSessionCondition() {
324
- return or(
325
- sql`lower(${schema.sessionRecordings.userId}) like ${"%@builder.io"}`,
326
- sql`lower(${schema.sessionRecordings.userKey}) like ${"%@builder.io"}`,
327
- );
328
- }
329
-
330
- class SessionDemoAnonymizer {
331
- aliasFor(value: unknown): string | null {
332
- const raw = replayString(value);
333
- if (!raw || raw.match(SESSION_DEMO_EMAIL_PATTERN)?.[0] !== raw) {
334
- return null;
335
- }
336
- return SESSION_DEMO_ANONYMOUS_EMAIL;
337
- }
338
-
339
- summarize(row: any, role?: SessionReplayAccessRole): SessionRecordingSummary {
340
- const summary = rowToSessionRecordingSummary(row, role);
341
- return {
342
- ...summary,
343
- userId: this.aliasFor(row.userId) ?? summary.userId,
344
- userKey: this.aliasFor(row.userKey) ?? summary.userKey,
345
- ownerEmail: this.aliasFor(summary.ownerEmail) ?? summary.ownerEmail,
346
- metadata: anonymizeEmailValue(summary.metadata, this) as Record<
347
- string,
348
- unknown
349
- >,
350
- };
351
- }
352
- }
353
-
354
- function anonymizeEmailValue(
355
- value: unknown,
356
- anonymizer: SessionDemoAnonymizer,
357
- ): unknown {
358
- const alias = anonymizer.aliasFor(value);
359
- if (alias) return alias;
360
- if (Array.isArray(value)) {
361
- return value.map((item) => anonymizeEmailValue(item, anonymizer));
362
- }
363
- if (typeof value === "string") {
364
- return value.replace(SESSION_DEMO_EMAIL_PATTERN, (email) => {
365
- return anonymizer.aliasFor(email) ?? email;
366
- });
367
- }
368
- if (value && typeof value === "object") {
369
- const out: Record<string, unknown> = {};
370
- for (const [key, item] of Object.entries(value)) {
371
- out[key] = anonymizeEmailValue(item, anonymizer);
372
- }
373
- return out;
374
- }
375
- return value;
376
- }
377
-
378
- function assertDemoVisibleSession(row: any): void {
379
- if (isBuilderEmail(row.userId) || isBuilderEmail(row.userKey)) return;
380
- throw replayError("Session recording not found", 404);
381
- }
382
-
383
297
  function replayInteger(value: unknown): number | null {
384
298
  if (typeof value === "number" && Number.isInteger(value)) return value;
385
299
  if (typeof value === "string" && /^-?\d+$/.test(value.trim())) {
@@ -1686,7 +1600,6 @@ export async function listSessionRecordings(
1686
1600
  filters: SessionReplayListFilters = {},
1687
1601
  ): Promise<SessionRecordingSummary[]> {
1688
1602
  const db = getDb() as any;
1689
- const demoMode = await isSessionDemoModeEnabled(scope.userEmail);
1690
1603
  const limit = Math.min(
1691
1604
  MAX_SESSION_RECORDINGS_LIMIT,
1692
1605
  Math.max(1, filters.limit ?? DEFAULT_SESSION_RECORDINGS_LIMIT),
@@ -1699,7 +1612,6 @@ export async function listSessionRecordings(
1699
1612
  replayVisibleIdentityCondition(),
1700
1613
  replayPlayableEventsCondition(),
1701
1614
  ];
1702
- if (demoMode) conditions.push(demoBuilderSessionCondition());
1703
1615
  if (filters.app)
1704
1616
  conditions.push(eq(schema.sessionRecordings.app, filters.app));
1705
1617
  if (filters.template) {
@@ -1753,22 +1665,13 @@ export async function listSessionRecordings(
1753
1665
  .where(and(...conditions))
1754
1666
  .orderBy(desc(schema.sessionRecordings.startedAt))
1755
1667
  .limit(limit);
1756
- if (!demoMode) {
1757
- return rows.map((row: any) => rowToSessionRecordingSummary(row));
1758
- }
1759
- const anonymizer = new SessionDemoAnonymizer();
1760
- return rows
1761
- .filter(
1762
- (row: any) => isBuilderEmail(row.userId) || isBuilderEmail(row.userKey),
1763
- )
1764
- .map((row: any) => anonymizer.summarize(row));
1668
+ return rows.map((row: any) => rowToSessionRecordingSummary(row));
1765
1669
  }
1766
1670
 
1767
1671
  export async function getSessionReplaySummary(
1768
1672
  recordingId: string,
1769
1673
  scope: SessionReplayScope,
1770
1674
  ): Promise<SessionRecordingSummary> {
1771
- const demoMode = await isSessionDemoModeEnabled(scope.userEmail);
1772
1675
  const access = await resolveAccess("session-recording", recordingId, {
1773
1676
  userEmail: scope.userEmail,
1774
1677
  orgId: scope.orgId ?? undefined,
@@ -1777,22 +1680,15 @@ export async function getSessionReplaySummary(
1777
1680
  if (!isVisibleSessionRecording(access.resource)) {
1778
1681
  throw replayError("Session recording not found", 404);
1779
1682
  }
1780
- if (!demoMode) {
1781
- return rowToSessionRecordingSummary(access.resource, access.role);
1782
- }
1783
- assertDemoVisibleSession(access.resource);
1784
- return new SessionDemoAnonymizer().summarize(access.resource, access.role);
1683
+ return rowToSessionRecordingSummary(access.resource, access.role);
1785
1684
  }
1786
1685
 
1787
1686
  export async function getSessionReplayTokenizedSummary(
1788
1687
  recordingId: string,
1789
1688
  viewerEmail: string,
1790
1689
  ): Promise<SessionRecordingSummary> {
1791
- // Tokenized reads often run without an authenticated request session. Use
1792
- // the viewer identity embedded in the signed, recording-scoped grant so a
1793
- // link minted while demo mode is enabled cannot reveal real identities when
1794
- // it is opened in a signed-out browser or by an external agent.
1795
- const demoMode = await isSessionDemoModeEnabled(viewerEmail);
1690
+ // Tokenized reads often run without an authenticated request session. The
1691
+ // viewer identity is still required by the signed, recording-scoped grant.
1796
1692
  const db = getDb() as any;
1797
1693
  // guard:allow-unscoped -- called only after verifySessionReplayAgentAccess(recordingId, token) verifies a signed, recording-scoped agent_access token.
1798
1694
  const [row] = await db
@@ -1803,9 +1699,7 @@ export async function getSessionReplayTokenizedSummary(
1803
1699
  if (!row || !isVisibleSessionRecording(row)) {
1804
1700
  throw replayError("Session recording not found", 404);
1805
1701
  }
1806
- if (!demoMode) return rowToSessionRecordingSummary(row, "viewer");
1807
- assertDemoVisibleSession(row);
1808
- return new SessionDemoAnonymizer().summarize(row, "viewer");
1702
+ return rowToSessionRecordingSummary(row, "viewer");
1809
1703
  }
1810
1704
 
1811
1705
  function parseInlineReplayEvents(inlineData: string): unknown[] {
@@ -239,6 +239,11 @@ export function realDataFinalGuard(
239
239
  // actual query. One corrective turn was enough for Sonnet but caused Luna
240
240
  // to hit the fallback before it reached the query.
241
241
  maxRetries: 2,
242
+ // The first request may use the compact starter catalog. A corrective
243
+ // retry must be able to reach the real source action directly; otherwise
244
+ // some model families spend the retry on tool-search narration and hit
245
+ // the canned fallback without ever running a query.
246
+ expandToolSurface: true,
242
247
  };
243
248
  }
244
249
 
@@ -281,6 +286,7 @@ export default createAgentChatPlugin({
281
286
  analyticsSourceGuidanceOpening() +
282
287
  "DASHBOARD CREATION RULE — You may create dashboards, analyses, SQL panels, or other resources only when the user explicitly asks you to (e.g. 'build me a dashboard for...', 'create a new analysis', 'add a chart for...'). Never create any resource proactively during research, trend analysis, or answering questions. If you think a dashboard would be useful, suggest it and wait for explicit confirmation before creating anything. Never add new items to the sidebar or modify existing dashboards without an explicit user directive. " +
283
288
  "DASHBOARD MUTATION RULE — For dashboard edits, default to `mutate-dashboard` with the typed `dashboard.*` script API so the main payload is a string and avoids native-array serialization traps. It can move panels by id, edit titles/SQL/config, insert, duplicate, remove, and patch dashboard fields in one atomic save. The script API is constrained: no variables/imports/loops/functions, only JSON-compatible arguments on documented dashboard methods. Do not count shifting `/panels/<index>` positions for ordinary dashboard edits unless the user specifically asks for low-level JSON-pointer operations. " +
289
+ 'FIRST-PARTY DASHBOARD TIME RULE — AI-generated `source: "first-party"` panels are dashboard-time-bound by default: set `config.timeScope` to `dashboard` and include a matching dashboard time predicate. `{{timeRange}}` requires a matching `filters` entry with `id: "timeRange"` and `type: "select"`; `{{<id>Start}}`/`{{<id>End}}` require a matching `type: "date-range"` filter with that id. Allowed `timeScope` values are `dashboard`, `fixed-window`, `cohort-history`, and `all-time`; use `all-time` only when the user requests full available history and put all-time, lifetime, or historical in the title or description. Server validation rejects unbound first-party SQL. ' +
284
290
  "DASHBOARD READ RULE — `get-sql-dashboard` is compact by default: use its `panels` summaries plus `layout.panelOrder`, `layout.firstPanelIds`, and `layout.groups[].rows[].rowNumber/panelIds` for orientation and verification. Pass `includeConfig: true` only when you truly need full panel SQL/config. " +
285
291
  'DASHBOARD REORDER RULE — For simple chart/section moves, use `mutate-dashboard` code such as `dashboard.panels(["panel-a","panel-b"]).moveToTop();`. For visible placement requests like "second row" or "next to return rates", use row-aware placement such as `dashboard.insertPanel({...}).nextTo("retention-over-time")`, `.atRow(2)`, or `dashboard.panel("panel-a").moveNextTo("panel-b")`; these keep panels in the intended rendered row and expand/rebalance that row when needed. Never count shifting `/panels/<index>` positions for ordinary \'move this chart\' requests. Use `get-sql-dashboard.layout.groups[].rows` as proof of visible row placement, not only flat `panelOrder`. ' +
286
292
  "Use configured data sources and actions only. Call `data-source-status` when you need to know which providers are connected, and treat provider actions as unavailable for analysis if they return missing credentials, permission, syntax, quota, or network errors. " +