@agent-native/core 0.119.3 → 0.119.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 (217) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +13 -0
  3. package/corpus/core/package.json +3 -3
  4. package/corpus/core/src/agent/production-agent.ts +107 -125
  5. package/corpus/core/src/agent/run-manager.ts +45 -2
  6. package/corpus/core/src/agent/thread-data-builder.ts +85 -8
  7. package/corpus/core/src/client/AssistantChat.tsx +37 -9
  8. package/corpus/core/src/client/chat/message-components.tsx +9 -1
  9. package/corpus/core/src/client/chat/repo-helpers.ts +77 -0
  10. package/corpus/core/src/client/chat/tool-call-display.tsx +51 -15
  11. package/corpus/core/src/server/agent-chat/framework-prompts.ts +3 -1
  12. package/corpus/core/src/server/agent-chat/plugin-options.ts +9 -0
  13. package/corpus/core/src/server/agent-chat-plugin.ts +12 -3
  14. package/corpus/core/src/templates/chat/.agents/skills/actions/SKILL.md +512 -0
  15. package/corpus/core/src/templates/chat/.agents/skills/adding-a-feature/SKILL.md +189 -0
  16. package/corpus/core/src/templates/chat/.agents/skills/agent-native-docs/SKILL.md +115 -0
  17. package/corpus/core/src/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +177 -0
  18. package/corpus/core/src/templates/chat/.agents/skills/capture-learnings/SKILL.md +89 -0
  19. package/corpus/core/src/templates/chat/.agents/skills/create-skill/SKILL.md +221 -0
  20. package/corpus/core/src/templates/chat/.agents/skills/customizing-agent-native/SKILL.md +220 -0
  21. package/corpus/core/src/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +263 -0
  22. package/corpus/core/src/templates/chat/.agents/skills/feature-flags/SKILL.md +169 -0
  23. package/corpus/core/src/templates/chat/.agents/skills/frontend-design/SKILL.md +160 -0
  24. package/corpus/core/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +232 -0
  25. package/corpus/core/src/templates/chat/.agents/skills/security/SKILL.md +280 -0
  26. package/corpus/core/src/templates/chat/.agents/skills/self-modifying-code/SKILL.md +118 -0
  27. package/corpus/core/src/templates/chat/.agents/skills/shadcn-ui/SKILL.md +119 -0
  28. package/corpus/core/src/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +100 -0
  29. package/corpus/core/src/templates/chat/.env.example +5 -0
  30. package/corpus/core/src/templates/chat/.ignore +0 -0
  31. package/corpus/core/src/templates/chat/.oxfmtrc.json +8 -0
  32. package/corpus/core/src/templates/chat/AGENTS.md +69 -0
  33. package/corpus/core/src/templates/chat/CHANGELOG.md +10 -0
  34. package/corpus/core/src/templates/chat/DEVELOPING.md +185 -0
  35. package/corpus/core/src/templates/chat/README.md +32 -0
  36. package/corpus/core/src/templates/chat/_gitignore +43 -0
  37. package/corpus/core/src/templates/chat/actions/hello.ts +13 -0
  38. package/corpus/core/src/templates/chat/actions/navigate.ts +41 -0
  39. package/corpus/core/src/templates/chat/actions/run.ts +2 -0
  40. package/corpus/core/src/templates/chat/actions/view-screen.ts +31 -0
  41. package/corpus/core/src/templates/chat/app/components/layout/Header.tsx +60 -0
  42. package/corpus/core/src/templates/chat/app/components/layout/Layout.tsx +185 -0
  43. package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +450 -0
  44. package/corpus/core/src/templates/chat/app/components/ui/button.tsx +1 -0
  45. package/corpus/core/src/templates/chat/app/components/ui/card.tsx +1 -0
  46. package/corpus/core/src/templates/chat/app/components/ui/dropdown-menu.tsx +1 -0
  47. package/corpus/core/src/templates/chat/app/components/ui/input.tsx +1 -0
  48. package/corpus/core/src/templates/chat/app/components/ui/label.tsx +1 -0
  49. package/corpus/core/src/templates/chat/app/components/ui/sheet.tsx +1 -0
  50. package/corpus/core/src/templates/chat/app/components/ui/toolkit-provider.tsx +17 -0
  51. package/corpus/core/src/templates/chat/app/components/ui/tooltip.tsx +1 -0
  52. package/corpus/core/src/templates/chat/app/design-system.ts +3 -0
  53. package/corpus/core/src/templates/chat/app/entry.client.tsx +19 -0
  54. package/corpus/core/src/templates/chat/app/entry.server.tsx +10 -0
  55. package/corpus/core/src/templates/chat/app/global.css +93 -0
  56. package/corpus/core/src/templates/chat/app/hooks/use-navigation-state.ts +93 -0
  57. package/corpus/core/src/templates/chat/app/i18n/ar-SA.ts +68 -0
  58. package/corpus/core/src/templates/chat/app/i18n/de-DE.ts +71 -0
  59. package/corpus/core/src/templates/chat/app/i18n/en-US.ts +70 -0
  60. package/corpus/core/src/templates/chat/app/i18n/es-ES.ts +71 -0
  61. package/corpus/core/src/templates/chat/app/i18n/fr-FR.ts +71 -0
  62. package/corpus/core/src/templates/chat/app/i18n/hi-IN.ts +69 -0
  63. package/corpus/core/src/templates/chat/app/i18n/index.ts +34 -0
  64. package/corpus/core/src/templates/chat/app/i18n/ja-JP.ts +70 -0
  65. package/corpus/core/src/templates/chat/app/i18n/ko-KR.ts +70 -0
  66. package/corpus/core/src/templates/chat/app/i18n/pt-BR.ts +71 -0
  67. package/corpus/core/src/templates/chat/app/i18n/zh-CN.ts +66 -0
  68. package/corpus/core/src/templates/chat/app/i18n/zh-TW.ts +66 -0
  69. package/corpus/core/src/templates/chat/app/i18n-data.ts +556 -0
  70. package/corpus/core/src/templates/chat/app/lib/agent-page.tsx +49 -0
  71. package/corpus/core/src/templates/chat/app/lib/app-config.ts +11 -0
  72. package/corpus/core/src/templates/chat/app/lib/tab-id.ts +1 -0
  73. package/corpus/core/src/templates/chat/app/lib/utils.ts +1 -0
  74. package/corpus/core/src/templates/chat/app/root.tsx +173 -0
  75. package/corpus/core/src/templates/chat/app/routes/_index.tsx +94 -0
  76. package/corpus/core/src/templates/chat/app/routes/agent.tsx +24 -0
  77. package/corpus/core/src/templates/chat/app/routes/chat.$threadId.tsx +1 -0
  78. package/corpus/core/src/templates/chat/app/routes/database.tsx +17 -0
  79. package/corpus/core/src/templates/chat/app/routes/extensions.$id.$slug.tsx +2 -0
  80. package/corpus/core/src/templates/chat/app/routes/extensions.$id.tsx +11 -0
  81. package/corpus/core/src/templates/chat/app/routes/extensions._index.tsx +11 -0
  82. package/corpus/core/src/templates/chat/app/routes/extensions.tsx +5 -0
  83. package/corpus/core/src/templates/chat/app/routes/observability.tsx +19 -0
  84. package/corpus/core/src/templates/chat/app/routes/settings.tsx +89 -0
  85. package/corpus/core/src/templates/chat/app/routes/team.tsx +11 -0
  86. package/corpus/core/src/templates/chat/app/routes.ts +4 -0
  87. package/corpus/core/src/templates/chat/app/vite-env.d.ts +6 -0
  88. package/corpus/core/src/templates/chat/changelog/2026-06-24-a-new-settings-page-gives-quick-access-to-language-workspace.md +6 -0
  89. package/corpus/core/src/templates/chat/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +6 -0
  90. package/corpus/core/src/templates/chat/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
  91. package/corpus/core/src/templates/chat/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  92. package/corpus/core/src/templates/chat/changelog/2026-06-29-chat-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  93. package/corpus/core/src/templates/chat/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  94. package/corpus/core/src/templates/chat/changelog/2026-07-10-chat-now-makes-ai-connection-setup-clear-without-shifting-th.md +6 -0
  95. package/corpus/core/src/templates/chat/changelog/2026-07-13-a-full-agent-page-now-brings-context-files-connections-jobs-.md +6 -0
  96. package/corpus/core/src/templates/chat/changelog/2026-07-14-chat-opens-reliably-on-hosted-deployments-instead-of-failing.md +6 -0
  97. package/corpus/core/src/templates/chat/changelog/2026-07-14-fixed-chat-template-startup-with-older-core-versions.md +6 -0
  98. package/corpus/core/src/templates/chat/changelog/2026-07-15-chat-navigation-focuses-on-chat-and-agent.md +6 -0
  99. package/corpus/core/src/templates/chat/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  100. package/corpus/core/src/templates/chat/changelog/2026-07-22-compact-sidebar-footer.md +6 -0
  101. package/corpus/core/src/templates/chat/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
  102. package/corpus/core/src/templates/chat/changelog/2026-07-22-manage-agent-navigation-now-uses-the-connected-nodes-icon.md +6 -0
  103. package/corpus/core/src/templates/chat/changelog/2026-07-22-recent-chats-are-easier-to-scan-and-expand.md +6 -0
  104. package/corpus/core/src/templates/chat/changelog/2026-07-23-full-page-chat-is-better-centered-with-quieter-chat-history-.md +6 -0
  105. package/corpus/core/src/templates/chat/components.json +20 -0
  106. package/corpus/core/src/templates/chat/learnings.defaults.md +5 -0
  107. package/corpus/core/src/templates/chat/netlify.toml +10 -0
  108. package/corpus/core/src/templates/chat/package.json +92 -0
  109. package/corpus/core/src/templates/chat/public/agent-native-icon-dark.svg +10 -0
  110. package/corpus/core/src/templates/chat/public/agent-native-icon-light.svg +10 -0
  111. package/corpus/core/src/templates/chat/public/agent-native-logo-dark.svg +21 -0
  112. package/corpus/core/src/templates/chat/public/agent-native-logo-light.svg +21 -0
  113. package/corpus/core/src/templates/chat/public/favicon.svg +1 -0
  114. package/corpus/core/src/templates/chat/public/icon-180.svg +1 -0
  115. package/corpus/core/src/templates/chat/public/icon-192.svg +1 -0
  116. package/corpus/core/src/templates/chat/public/icon-512.svg +1 -0
  117. package/corpus/core/src/templates/chat/public/manifest.json +21 -0
  118. package/corpus/core/src/templates/chat/react-router.config.ts +7 -0
  119. package/corpus/core/src/templates/chat/server/middleware/auth.ts +15 -0
  120. package/corpus/core/src/templates/chat/server/plugins/agent-chat.ts +21 -0
  121. package/corpus/core/src/templates/chat/server/plugins/auth.ts +17 -0
  122. package/corpus/core/src/templates/chat/server/routes/[...page].get.ts +5 -0
  123. package/corpus/core/src/templates/chat/ssr-entry.ts +15 -0
  124. package/corpus/core/src/templates/chat/tsconfig.json +21 -0
  125. package/corpus/core/src/templates/chat/vite.config.ts +19 -0
  126. package/corpus/templates/analytics/AGENTS.md +19 -11
  127. package/corpus/templates/analytics/actions/list-data-dictionary.ts +3 -1
  128. package/corpus/templates/analytics/actions/save-data-dictionary-entry.ts +14 -0
  129. package/corpus/templates/analytics/actions/search-analytics-query-catalog.ts +41 -0
  130. package/corpus/templates/analytics/app/components/dashboard/DashboardPanelSkeleton.tsx +13 -0
  131. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +12 -11
  132. package/corpus/templates/analytics/app/pages/adhoc/DashboardSkeleton.tsx +3 -3
  133. package/corpus/templates/analytics/changelog/2026-07-23-dashboard-chart-loading-placeholders-are-easier-to-see-in-da.md +6 -0
  134. package/corpus/templates/analytics/changelog/2026-07-23-routine-analytics-lookups-reuse-existing-query-definitions.md +6 -0
  135. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +1 -12
  136. package/corpus/templates/analytics/server/lib/analytics-query-catalog.ts +405 -0
  137. package/corpus/templates/analytics/server/plugins/agent-chat.ts +11 -16
  138. package/corpus/templates/content/changelog/2026-07-23-database-toolbar-sort-and-filter-controls-open-reliably.md +6 -0
  139. package/corpus/templates/forms/app/lib/public-form-link.ts +14 -0
  140. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +6 -14
  141. package/corpus/templates/forms/changelog/2026-07-23-published-form-links-can-be-copied-without-being-asked-to-re.md +6 -0
  142. package/corpus/templates/mail/changelog/2026-07-23-scheduled-emails-now-send-only-once-when-multiple-requests-r.md +6 -0
  143. package/corpus/templates/mail/server/lib/jobs.ts +13 -4
  144. package/corpus/templates/mail/server/plugins/mail-jobs.ts +1 -1
  145. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +7 -18
  146. package/corpus/templates/slides/app/lib/agent-visible-message.ts +28 -0
  147. package/corpus/templates/slides/app/pages/Index.tsx +6 -34
  148. package/corpus/templates/slides/changelog/2026-07-23-full-prompts-stay-visible-in-chat-when-creating-decks-or-add.md +6 -0
  149. package/corpus/toolkit/CHANGELOG.md +13 -0
  150. package/corpus/toolkit/package.json +1 -1
  151. package/corpus/toolkit/src/editor/SharedRichEditor.tsx +23 -2
  152. package/corpus/toolkit/src/editor/SlashCommandMenu.tsx +68 -2
  153. package/corpus/toolkit/src/editor/index.ts +1 -0
  154. package/corpus/toolkit/src/editor.css +75 -0
  155. package/corpus/toolkit/src/ui/button.tsx +3 -2
  156. package/dist/agent/production-agent.d.ts.map +1 -1
  157. package/dist/agent/production-agent.js +96 -115
  158. package/dist/agent/production-agent.js.map +1 -1
  159. package/dist/agent/run-manager.d.ts +9 -1
  160. package/dist/agent/run-manager.d.ts.map +1 -1
  161. package/dist/agent/run-manager.js +26 -0
  162. package/dist/agent/run-manager.js.map +1 -1
  163. package/dist/agent/thread-data-builder.d.ts +2 -0
  164. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  165. package/dist/agent/thread-data-builder.js +69 -9
  166. package/dist/agent/thread-data-builder.js.map +1 -1
  167. package/dist/client/AssistantChat.d.ts.map +1 -1
  168. package/dist/client/AssistantChat.js +32 -10
  169. package/dist/client/AssistantChat.js.map +1 -1
  170. package/dist/client/chat/message-components.d.ts +2 -1
  171. package/dist/client/chat/message-components.d.ts.map +1 -1
  172. package/dist/client/chat/message-components.js +9 -3
  173. package/dist/client/chat/message-components.js.map +1 -1
  174. package/dist/client/chat/repo-helpers.d.ts +4 -0
  175. package/dist/client/chat/repo-helpers.d.ts.map +1 -1
  176. package/dist/client/chat/repo-helpers.js +61 -0
  177. package/dist/client/chat/repo-helpers.js.map +1 -1
  178. package/dist/client/chat/tool-call-display.d.ts +2 -1
  179. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  180. package/dist/client/chat/tool-call-display.js +10 -9
  181. package/dist/client/chat/tool-call-display.js.map +1 -1
  182. package/dist/collab/routes.d.ts +1 -1
  183. package/dist/notifications/routes.d.ts +1 -1
  184. package/dist/observability/routes.d.ts +5 -5
  185. package/dist/progress/routes.d.ts +1 -1
  186. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  187. package/dist/provider-api/actions/provider-api.d.ts +7 -7
  188. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  189. package/dist/resources/handlers.d.ts +1 -1
  190. package/dist/secrets/routes.d.ts +9 -9
  191. package/dist/server/agent-chat/framework-prompts.d.ts.map +1 -1
  192. package/dist/server/agent-chat/framework-prompts.js +3 -1
  193. package/dist/server/agent-chat/framework-prompts.js.map +1 -1
  194. package/dist/server/agent-chat/plugin-options.d.ts +9 -0
  195. package/dist/server/agent-chat/plugin-options.d.ts.map +1 -1
  196. package/dist/server/agent-chat/plugin-options.js.map +1 -1
  197. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  198. package/dist/server/agent-chat-plugin.js +11 -2
  199. package/dist/server/agent-chat-plugin.js.map +1 -1
  200. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  201. package/dist/server/realtime-token.d.ts +1 -1
  202. package/dist/server/transcribe-voice.d.ts +1 -1
  203. package/package.json +4 -4
  204. package/src/agent/production-agent.ts +107 -125
  205. package/src/agent/run-manager.ts +45 -2
  206. package/src/agent/thread-data-builder.ts +85 -8
  207. package/src/client/AssistantChat.tsx +37 -9
  208. package/src/client/chat/message-components.tsx +9 -1
  209. package/src/client/chat/repo-helpers.ts +77 -0
  210. package/src/client/chat/tool-call-display.tsx +51 -15
  211. package/src/server/agent-chat/framework-prompts.ts +3 -1
  212. package/src/server/agent-chat/plugin-options.ts +9 -0
  213. package/src/server/agent-chat-plugin.ts +12 -3
  214. package/src/templates/chat/actions/hello.spec.ts +0 -23
  215. package/src/templates/chat/actions/navigate.spec.ts +0 -68
  216. package/src/templates/chat/actions/view-screen.spec.ts +0 -40
  217. package/src/templates/chat/app/lib/agent-page.spec.tsx +0 -61
@@ -0,0 +1,405 @@
1
+ import { getAllSettings, listOrgSettings } from "@agent-native/core/settings";
2
+
3
+ import { dashboardCatalogEntries } from "./dashboard-catalog";
4
+ import { listDashboards, type DashboardRecord } from "./dashboards-store";
5
+
6
+ const DATA_DICTIONARY_KEY_PREFIX = "data-dict-";
7
+ const MAX_QUERY_LENGTH = 12_000;
8
+ const STOP_WORDS = new Set([
9
+ "a",
10
+ "all",
11
+ "an",
12
+ "and",
13
+ "are",
14
+ "by",
15
+ "count",
16
+ "data",
17
+ "day",
18
+ "days",
19
+ "do",
20
+ "exact",
21
+ "find",
22
+ "for",
23
+ "from",
24
+ "get",
25
+ "give",
26
+ "how",
27
+ "i",
28
+ "in",
29
+ "last",
30
+ "look",
31
+ "many",
32
+ "me",
33
+ "metric",
34
+ "number",
35
+ "of",
36
+ "on",
37
+ "over",
38
+ "our",
39
+ "please",
40
+ "show",
41
+ "that",
42
+ "the",
43
+ "this",
44
+ "time",
45
+ "today",
46
+ "total",
47
+ "up",
48
+ "week",
49
+ "what",
50
+ "were",
51
+ "we",
52
+ "yesterday",
53
+ ]);
54
+
55
+ type DictionaryEntry = Record<string, unknown>;
56
+ type DashboardPanel = Record<string, unknown>;
57
+
58
+ export type AnalyticsQueryCatalogCandidate =
59
+ | {
60
+ kind: "dashboard-panel";
61
+ origin: "saved-dashboard" | "dashboard-template";
62
+ score: number;
63
+ matchedTerms: string[];
64
+ dashboardId: string;
65
+ dashboardTitle: string;
66
+ dashboardDescription?: string;
67
+ panelId: string;
68
+ panelTitle: string;
69
+ panelDescription?: string;
70
+ source?: string;
71
+ query: string | Record<string, unknown>;
72
+ timeScope?: string;
73
+ }
74
+ | {
75
+ kind: "data-dictionary";
76
+ origin: "data-dictionary";
77
+ score: number;
78
+ matchedTerms: string[];
79
+ id: string;
80
+ metric: string;
81
+ definition?: string;
82
+ source?: string;
83
+ action?: string;
84
+ table?: string;
85
+ columnsUsed?: string;
86
+ queryTemplate?: string;
87
+ knownGotchas?: string;
88
+ approved?: boolean;
89
+ aiGenerated?: boolean;
90
+ sourceUrl?: string;
91
+ };
92
+
93
+ function text(value: unknown): string {
94
+ return typeof value === "string" ? value.trim() : "";
95
+ }
96
+
97
+ function compactQuery(value: unknown): string | Record<string, unknown> | null {
98
+ if (typeof value === "string") {
99
+ const trimmed = value.trim();
100
+ return trimmed ? trimmed.slice(0, MAX_QUERY_LENGTH) : null;
101
+ }
102
+ if (value && typeof value === "object" && !Array.isArray(value)) {
103
+ return value as Record<string, unknown>;
104
+ }
105
+ return null;
106
+ }
107
+
108
+ function searchTerms(search: string): string[] {
109
+ const normalized = search
110
+ .toLowerCase()
111
+ .replace(/[^a-z0-9]+/g, " ")
112
+ .trim();
113
+ if (!normalized) return [];
114
+ const meaningful = normalized
115
+ .split(/\s+/)
116
+ .filter((term) => term.length > 1 && !STOP_WORDS.has(term));
117
+ return meaningful.length ? Array.from(new Set(meaningful)) : [normalized];
118
+ }
119
+
120
+ function matchScore(
121
+ search: string,
122
+ weightedFields: Array<{ value: unknown; weight: number }>,
123
+ ): { score: number; matchedTerms: string[] } {
124
+ const terms = searchTerms(search);
125
+ if (!terms.length) return { score: 0, matchedTerms: [] };
126
+
127
+ const normalizedSearch = search.toLowerCase().trim();
128
+ const matched = new Set<string>();
129
+ let score = 0;
130
+ for (const field of weightedFields) {
131
+ const value = text(field.value).toLowerCase();
132
+ if (!value) continue;
133
+ if (normalizedSearch.length > 2 && value.includes(normalizedSearch)) {
134
+ score += field.weight * 4;
135
+ }
136
+ for (const term of terms) {
137
+ if (!value.includes(term)) continue;
138
+ matched.add(term);
139
+ score += field.weight;
140
+ }
141
+ }
142
+
143
+ if (matched.size === terms.length) score += 40;
144
+ return { score, matchedTerms: [...matched] };
145
+ }
146
+
147
+ function dashboardPanelCandidates(args: {
148
+ dashboardId: string;
149
+ dashboardTitle: string;
150
+ dashboardDescription?: string;
151
+ config: Record<string, unknown>;
152
+ origin: "saved-dashboard" | "dashboard-template";
153
+ search: string;
154
+ }): AnalyticsQueryCatalogCandidate[] {
155
+ const panels = Array.isArray(args.config.panels)
156
+ ? (args.config.panels as DashboardPanel[])
157
+ : [];
158
+
159
+ return panels.flatMap((panel) => {
160
+ const query = compactQuery(panel.sql);
161
+ if (!query) return [];
162
+ const panelConfig =
163
+ panel.config &&
164
+ typeof panel.config === "object" &&
165
+ !Array.isArray(panel.config)
166
+ ? (panel.config as Record<string, unknown>)
167
+ : {};
168
+ const panelTitle = text(panel.title) || text(panel.id);
169
+ const panelDescription = text(panelConfig.description);
170
+ const { score: rawScore, matchedTerms } = matchScore(args.search, [
171
+ { value: panelTitle, weight: 24 },
172
+ { value: panelDescription, weight: 12 },
173
+ { value: args.dashboardTitle, weight: 10 },
174
+ { value: args.dashboardDescription, weight: 6 },
175
+ { value: panel.source, weight: 5 },
176
+ {
177
+ value: typeof query === "string" ? query : JSON.stringify(query),
178
+ weight: 2,
179
+ },
180
+ ]);
181
+ const requestedTerms = new Set(searchTerms(args.search));
182
+ const titleSpecificityPenalty =
183
+ searchTerms(panelTitle).filter((term) => !requestedTerms.has(term))
184
+ .length * 12;
185
+ const aggregateIntent =
186
+ /\b(how many|count|number|total)\b/i.test(args.search) &&
187
+ text(panel.chartType) === "metric"
188
+ ? 20
189
+ : 0;
190
+ const score = rawScore - titleSpecificityPenalty + aggregateIntent;
191
+ if (score <= 0) return [];
192
+
193
+ return [
194
+ {
195
+ kind: "dashboard-panel" as const,
196
+ origin: args.origin,
197
+ score,
198
+ matchedTerms,
199
+ dashboardId: args.dashboardId,
200
+ dashboardTitle: args.dashboardTitle,
201
+ ...(args.dashboardDescription
202
+ ? { dashboardDescription: args.dashboardDescription }
203
+ : {}),
204
+ panelId: text(panel.id),
205
+ panelTitle,
206
+ ...(panelDescription ? { panelDescription } : {}),
207
+ ...(text(panel.source) ? { source: text(panel.source) } : {}),
208
+ query,
209
+ ...(text(panelConfig.timeScope)
210
+ ? { timeScope: text(panelConfig.timeScope) }
211
+ : {}),
212
+ },
213
+ ];
214
+ });
215
+ }
216
+
217
+ function dictionaryCandidates(
218
+ entries: DictionaryEntry[],
219
+ search: string,
220
+ ): AnalyticsQueryCatalogCandidate[] {
221
+ return entries.flatMap((entry) => {
222
+ const { score, matchedTerms } = matchScore(search, [
223
+ { value: entry.metric, weight: 28 },
224
+ { value: entry.commonQuestions, weight: 16 },
225
+ { value: entry.definition, weight: 12 },
226
+ { value: entry.table, weight: 8 },
227
+ { value: entry.columnsUsed, weight: 6 },
228
+ { value: entry.queryTemplate, weight: 5 },
229
+ { value: entry.source, weight: 5 },
230
+ { value: entry.action, weight: 5 },
231
+ { value: entry.knownGotchas, weight: 2 },
232
+ ]);
233
+ if (!score) return [];
234
+
235
+ const id = text(entry.id);
236
+ const metric = text(entry.metric);
237
+ if (!id || !metric) return [];
238
+ return [
239
+ {
240
+ kind: "data-dictionary" as const,
241
+ origin: "data-dictionary" as const,
242
+ score: score + (entry.approved === true ? 12 : 0),
243
+ matchedTerms,
244
+ id,
245
+ metric,
246
+ ...(text(entry.definition)
247
+ ? { definition: text(entry.definition) }
248
+ : {}),
249
+ ...(text(entry.source) ? { source: text(entry.source) } : {}),
250
+ ...(text(entry.action) ? { action: text(entry.action) } : {}),
251
+ ...(text(entry.table) ? { table: text(entry.table) } : {}),
252
+ ...(text(entry.columnsUsed)
253
+ ? { columnsUsed: text(entry.columnsUsed) }
254
+ : {}),
255
+ ...(text(entry.queryTemplate)
256
+ ? { queryTemplate: text(entry.queryTemplate) }
257
+ : {}),
258
+ ...(text(entry.knownGotchas)
259
+ ? { knownGotchas: text(entry.knownGotchas) }
260
+ : {}),
261
+ ...(typeof entry.approved === "boolean"
262
+ ? { approved: entry.approved }
263
+ : {}),
264
+ ...(typeof entry.aiGenerated === "boolean"
265
+ ? { aiGenerated: entry.aiGenerated }
266
+ : {}),
267
+ ...(text(entry.sourceUrl) ? { sourceUrl: text(entry.sourceUrl) } : {}),
268
+ },
269
+ ];
270
+ });
271
+ }
272
+
273
+ export function rankAnalyticsQueryCatalog(args: {
274
+ search: string;
275
+ dashboards: Array<{
276
+ id: string;
277
+ title: string;
278
+ description?: string;
279
+ config: Record<string, unknown>;
280
+ origin: "saved-dashboard" | "dashboard-template";
281
+ }>;
282
+ dictionaryEntries: DictionaryEntry[];
283
+ limit: number;
284
+ }): AnalyticsQueryCatalogCandidate[] {
285
+ const candidates = [
286
+ ...args.dashboards.flatMap((dashboard) =>
287
+ dashboardPanelCandidates({
288
+ dashboardId: dashboard.id,
289
+ dashboardTitle: dashboard.title,
290
+ dashboardDescription: dashboard.description,
291
+ config: dashboard.config,
292
+ origin: dashboard.origin,
293
+ search: args.search,
294
+ }),
295
+ ),
296
+ ...dictionaryCandidates(args.dictionaryEntries, args.search),
297
+ ];
298
+
299
+ return candidates
300
+ .sort((a, b) => {
301
+ if (b.score !== a.score) return b.score - a.score;
302
+ if (a.kind !== b.kind) return a.kind === "data-dictionary" ? -1 : 1;
303
+ return JSON.stringify(a).localeCompare(JSON.stringify(b));
304
+ })
305
+ .slice(0, args.limit);
306
+ }
307
+
308
+ async function listDictionaryEntries(args: {
309
+ email: string;
310
+ orgId: string | null;
311
+ }): Promise<DictionaryEntry[]> {
312
+ const entries: DictionaryEntry[] = [];
313
+ const seen = new Set<string>();
314
+ const collect = (raw: unknown) => {
315
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
316
+ const entry = raw as DictionaryEntry;
317
+ const id = text(entry.id);
318
+ if (!id || seen.has(id)) return;
319
+ seen.add(id);
320
+ entries.push(entry);
321
+ };
322
+
323
+ if (args.orgId) {
324
+ const orgEntries = await listOrgSettings(
325
+ args.orgId,
326
+ DATA_DICTIONARY_KEY_PREFIX,
327
+ );
328
+ for (const value of Object.values(orgEntries)) collect(value);
329
+ }
330
+
331
+ const userPrefix = `u:${args.email}:${DATA_DICTIONARY_KEY_PREFIX}`;
332
+ const all = await getAllSettings();
333
+ for (const [key, value] of Object.entries(all)) {
334
+ if (key.startsWith(userPrefix)) collect(value);
335
+ }
336
+ return entries;
337
+ }
338
+
339
+ function savedDashboardInput(dashboard: DashboardRecord) {
340
+ const config = dashboard.config as Record<string, unknown>;
341
+ return {
342
+ id: dashboard.id,
343
+ title: text(config.name) || dashboard.title,
344
+ description: text(config.description) || undefined,
345
+ config,
346
+ origin: "saved-dashboard" as const,
347
+ };
348
+ }
349
+
350
+ export async function searchAnalyticsQueryCatalog(args: {
351
+ search: string;
352
+ email: string;
353
+ orgId: string | null;
354
+ limit: number;
355
+ }): Promise<AnalyticsQueryCatalogCandidate[]> {
356
+ const [savedDashboardsResult, dictionaryEntriesResult] =
357
+ await Promise.allSettled([
358
+ listDashboards(
359
+ { email: args.email, orgId: args.orgId },
360
+ { kind: "sql", archived: "active", hidden: "visible" },
361
+ ),
362
+ listDictionaryEntries({ email: args.email, orgId: args.orgId }),
363
+ ]);
364
+ const savedDashboards =
365
+ savedDashboardsResult.status === "fulfilled"
366
+ ? savedDashboardsResult.value
367
+ : [];
368
+ const dictionaryEntries =
369
+ dictionaryEntriesResult.status === "fulfilled"
370
+ ? dictionaryEntriesResult.value
371
+ : [];
372
+
373
+ const savedIds = new Set(savedDashboards.map((dashboard) => dashboard.id));
374
+ const templateDashboards = dashboardCatalogEntries
375
+ .filter((entry) => !savedIds.has(entry.defaultDashboardId))
376
+ .flatMap((entry) => {
377
+ try {
378
+ const config = entry.buildConfig() as unknown as Record<
379
+ string,
380
+ unknown
381
+ >;
382
+ return [
383
+ {
384
+ id: entry.defaultDashboardId,
385
+ title: text(config.name) || entry.name,
386
+ description: text(config.description) || entry.description,
387
+ config,
388
+ origin: "dashboard-template" as const,
389
+ },
390
+ ];
391
+ } catch {
392
+ return [];
393
+ }
394
+ });
395
+
396
+ return rankAnalyticsQueryCatalog({
397
+ search: args.search,
398
+ dashboards: [
399
+ ...savedDashboards.map(savedDashboardInput),
400
+ ...templateDashboards,
401
+ ],
402
+ dictionaryEntries,
403
+ limit: args.limit,
404
+ });
405
+ }
@@ -46,11 +46,11 @@ const ANALYTICS_DATA_SOURCES_LINK = buildDeepLink({
46
46
  to: "/data-sources",
47
47
  });
48
48
 
49
- export const SIMPLE_TIME_BOUNDED_METRIC_FAST_PATH_GUIDANCE =
50
- "SIMPLE TIME-BOUNDED METRIC FAST PATH — When the data dictionary or a known canonical source identifies the metric, run one bounded aggregate. Once it returns a valid result, answer the explicit question immediately with the source, time window, row count, and only necessary caveats. Do not schema-discover, retry, enrich, cross-check, or add breakdowns after that successful result unless the query failed or the result conflicts with the known metric definition. This does not waive the real-data requirement: never answer from a guess, stale value, or unverified result. ";
49
+ export const BOUNDED_STRUCTURED_LOOKUP_GUIDANCE =
50
+ "BOUNDED STRUCTURED LOOKUP FAST PATH — Treat existing analytics work like an engineer treats existing code: grep before writing. For an ordinary count, aggregate, grouped metric, trend, or record lookup, first call `search-analytics-query-catalog` once with focused metric/entity terms. It searches accessible dashboard names, chart titles/descriptions/saved queries, shipped dashboard patterns, and data-dictionary definitions together. Prefer the strongest approved dictionary or saved-chart match, preserve its source and business logic, adapt only the requested filters and explicit time window, then run one bounded query against that source. A user-named source wins, but still use a matching saved definition when it supplies the source's proven query shape. If there is no useful match, inspect only the most likely source schema or ask one clarification; do not fan out across providers. Do not separately list every dashboard, call data-source status, browse the whole dictionary, load provider catalogs/corpus tools, or query a second source after a strong match. Once the query succeeds, answer immediately with its source, time window, filters, row count, and only necessary caveats. Do not enrich, cross-check, retry, or add breakdowns unless the user requested them, the first query failed, or its result conflicts with the known definition. The words `all`, `total`, or `exact` in a structured aggregate do not by themselves make it a corpus investigation. Never repeat an identical invalid or failed tool call; correct its arguments once or surface the error. This does not waive the real-data requirement: never answer from a guess, stale value, or unverified result. ";
51
51
 
52
52
  export const BUILT_IN_FIRST_PARTY_SOURCE_GUIDANCE =
53
- "BUILT-IN FIRST-PARTY SOURCE — Analytics always provides one built-in first-party source alongside connected external providers such as BigQuery, HubSpot, Gong, Slack, and the other configured integrations. This does not replace or restrict external sources. For Builder/product signups, page views, app/template usage, conversions, and LLM observability, use `query-agent-native-analytics` over `analytics_events` (or `session_recordings` for replay summaries) when the event lives in first-party Analytics. When the user names an external provider, or the data dictionary identifies one as authoritative, query that provider instead. Do not report the first-party source as disconnected merely because an external provider is not configured. If the first-party query returns no rows, report that grounded result with its scope and time window. ";
53
+ "BUILT-IN FIRST-PARTY SOURCE — Analytics always provides one built-in first-party source alongside connected external providers such as BigQuery, HubSpot, Gong, Slack, and the other configured integrations. This does not replace or restrict external sources. When `search-analytics-query-catalog` identifies a first-party dashboard/chart definition, preserve its event semantics and use `query-agent-native-analytics` over `analytics_events` or `session_recordings` as appropriate. When the user names an external provider, or the catalog identifies one as authoritative, query that provider instead. Do not report the first-party source as disconnected merely because an external provider is not configured. If the authoritative query returns no rows, report that grounded result with its scope and time window. ";
54
54
 
55
55
  export const ANALYTICS_OBSERVABILITY_INCIDENT_GUIDANCE =
56
56
  "OBSERVABILITY INCIDENT WORKFLOW — For a named user's session or error question, resolve the user's email from context, then use list-session-recordings with userId over a bounded recent window to discover the relevant sessions. Do not require hasErrors=true for this initial lookup: replay/network/stuck-run evidence can exist while the recording's JavaScript errorCount is zero. Use hasErrors=true only when the user specifically asks for recordings with captured JavaScript errors or the recording metadata confirms that filter is appropriate. Use list-error-issues with userId or sessionRecordingId to identify a grouped issue, then get-error-issue for stack, breadcrumbs, occurrences, and linked recordings. For console diagnostics or failed network requests, create-session-replay-agent-link first and use its scoped diagnostics endpoint for detailed error text, stacks, request metadata, and bounded 5xx snippets; enumerate with kind/limit and fromMs/toMs or offset when needed. Use get-session-replay-summary and get-session-replay-timeline for the page-navigation and click sequence, and use get-session-replay-events only for additional bounded replay-event details. If no grouped error exists, correlate first-party observability events such as agent_chat_stuck_detected with query-agent-native-analytics in execution mode. In Plan mode, query-agent-native-analytics is intentionally unavailable: do not claim a live event correlation or root cause from it, and describe the event lookup as a planned next step. Prefer these first-party actions over generic SQL. Report the matching evidence and do not claim a root cause without a corroborating error, event, or replay signal. ";
@@ -75,7 +75,7 @@ export function analyticsSourceGuidanceOpening(): string {
75
75
  "<data-source-guidance>\n" +
76
76
  "Apply real-data requirements only when presenting analytics results, source records, or derived metrics. Do not call data-source tools for workflow migration, recurring-job setup, UI/code fixes, settings help, conceptual planning, or other non-data tasks unless the user explicitly asks for data. " +
77
77
  NON_ANALYTICS_REQUEST_GUIDANCE +
78
- SIMPLE_TIME_BOUNDED_METRIC_FAST_PATH_GUIDANCE +
78
+ BOUNDED_STRUCTURED_LOOKUP_GUIDANCE +
79
79
  BUILT_IN_FIRST_PARTY_SOURCE_GUIDANCE +
80
80
  ANALYTICS_OBSERVABILITY_INCIDENT_GUIDANCE +
81
81
  `DATA-SOURCE SETUP UX — Chat remains available when no external data source is connected. For a live-data request that needs an unavailable external provider, explain what is missing in the context of the user's question and guide them naturally to [Connect data sources](${ANALYTICS_DATA_SOURCES_LINK}). Use that real link from the app; do not emit a generic canned no-data sentence. For general conversation, conceptual questions, and questions the built-in first-party source can answer, continue helping normally. ` +
@@ -85,7 +85,7 @@ export function analyticsSourceGuidanceOpening(): string {
85
85
 
86
86
  export function analyticsDataDictionaryRoutingContext(): string {
87
87
  return `<data-dictionary-routing>
88
- Data-dictionary definitions are available on demand instead of being embedded in every chat request. Before writing SQL or making a metric-definition claim, call \`list-data-dictionary\` with a focused \`search\` or \`department\` filter. If the user asks what definitions exist and no useful filter is available, call it without filters. Treat approved entries as canonical, verify unreviewed human entries when stakes are high, and treat AI-generated unapproved entries as suggestions only. If no matching definition exists, inspect the configured source schema or ask the user instead of guessing.
88
+ Data-dictionary definitions are available through \`search-analytics-query-catalog\`, which combines focused dictionary lookup with a search over existing dashboard/chart SQL. Use that combined catalog search as the normal preflight for a bounded metric lookup. Call \`list-data-dictionary\` separately only when the user asks to browse definitions or filter them by department. Treat approved entries as canonical, verify unreviewed human entries when stakes are high, and treat AI-generated unapproved entries as suggestions only. After the catalog identifies one source and query shape, query that source once and stop on success. If no matching definition or chart exists, inspect only the most likely source schema or ask the user instead of fanning out across providers.
89
89
  </data-dictionary-routing>`;
90
90
  }
91
91
 
@@ -675,6 +675,7 @@ export default createAgentChatPlugin({
675
675
  loadActionsFromStaticRegistry(actionsRegistry),
676
676
  ),
677
677
  initialToolNames: INITIAL_TOOL_NAMES,
678
+ corpusTools: "lazy",
678
679
  finalResponseGuard: realDataFinalGuard,
679
680
  // Enable sandboxed JavaScript execution for analytics data processing.
680
681
  // Code runs in an isolated Node.js child process with no access to app
@@ -717,18 +718,12 @@ export default createAgentChatPlugin({
717
718
  '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`. ' +
718
719
  "Use configured data sources and actions only. The built-in first-party Analytics source is an additional source and is always available through `query-agent-native-analytics`, even when no external provider credentials are connected. External provider actions remain available and are the authoritative path when the user names a provider or the data lives there. Call `data-source-status` when you need to know which external providers are connected, and treat provider actions as unavailable for analysis only if they return missing credentials, permission, syntax, quota, or network errors. " +
719
720
  "The built-in `demo` dashboard source is a demo-environment Prometheus source reserved for the Node Exporter demo. It must never satisfy REAL_DATA_REQUIRED or be cited as user analytics evidence unless the user explicitly asks to inspect the demo dashboard. " +
720
- "When the user names a provider or tool such as Jira, Pylon, HubSpot, Gong, Slack, Sentry, GA4, or BigQuery, that named source is authoritative for the turn: use that provider's real tool/API surface, not a warehouse or different-provider substitute, unless the user explicitly asks for the copy/fallback. For bounded lookups where a first-class action fully models the requested source, object, filter, and pagination need, that shortcut is fine. For broad provider searches, cross-source joins, corpus-wide counts, exact cohort coverage, or any answer where absence matters, do not start and stop with shortcuts; use the broad provider API/MCP and corpus/code workflow as the primary path. " +
721
- "Provider-specific actions are shortcuts, not limits. If a first-class action cannot express the exact endpoint, object type, filters, request body, pagination mode, API version, or corpus coverage needed, call `provider-api-catalog` and `provider-api-docs` as needed, then call `provider-api-request` against the provider's real HTTP API. Use this raw provider API escape hatch instead of weakening the analysis, broadening filters, sampling default pages, or claiming the integration cannot do something the underlying API can do. " +
722
- "When one provider cohort becomes the input to a second provider search, join, or exhaustive corpus scan, stage the upstream cohort with `provider-api-request`/`stageAs` (or a native provider API/MCP bulk primitive) before the downstream search. Avoid starting such workflows with convenience list/search actions that return display-shaped pages or can time out before the corpus path is established. " +
723
- 'For broad unstructured provider records such as transcripts, messages, tickets, issues, notes, events, documents, or conversation logs, prefer `provider-corpus-job` so the scan has durable checkpoints, stored snippets, coverage counts, and provider quota pause/resume. Use mode="paginated-search" for any provider endpoint that already pages over the target records; use mode="batch-search" when a staged upstream cohort of ids/records must feed a second provider endpoint by `itemBodyPath` or `itemQueryParam`. Continue paused jobs until completed or quota_wait, then read results. Use `run-code` with `providerSearchAll`, `providerFetch`, `appAction`, `workspaceRead`, and Resources-backed workspace files for shorter reductions, joins, classification, and aggregation after the corpus path is established. Convenience actions are bounded shortcuts for common checks, not the ceiling for what the underlying provider API can answer. ' +
724
- "When the user asks to search source-record body text, use the raw body endpoint or native provider search for that record type. Parent/container metadata such as call lists, titles, summaries, briefs, channel lists, ticket lists, or issue lists can discover scope but cannot support a complete body-text absence claim. " +
725
- "For source-record mention searches, phrase searches, and absence checks across transcripts/messages/tickets/issues, prefer provider-side search, `provider-corpus-job`, staged corpus, `providerSearchAll`, or action-side search arguments that return counts, coverage, and snippets instead of loading raw full records into chat. Use full transcripts/messages only for selected evidence after the search narrows the corpus, and state whether the search covered every matching record or only a bounded sample. " +
726
- "For complex provider questions, broad searches, corpus-wide counts, cross-source joins, or any answer where absence matters, prefer a corpus-first workflow: inspect the provider API, fetch every relevant page or an explicitly bounded cohort, stage large responses with `saveToFile`/`stageAs`/`fetchAllPages`, use `provider-corpus-job` for durable long-running searches, and use `run-code` with `providerSearchAll`, `providerFetch`, `appAction`, and Resources-backed workspace files to join, search, classify, and aggregate. Use `scratch/` for temporary staging and durable folder names only for files the user should keep. Do not infer no results from sampled records, default limits, truncated excerpts, or aborted calls. If full coverage is not possible in the turn, say exactly what was inspected and what remains uncovered. " +
727
- 'For HubSpot deal cohorts, structured `hubspot-deals` filters can enumerate a bounded cohort for direct deal-list answers: `product` for the `products` field, `pipeline` for pipeline label/id, `closedStatus` for won/lost/open/closed, and `closedDateFrom`/`closedDateTo` for close-date windows. The `query` argument is full-text deal search and is not proof that a specific property matched; do not use `query: "Publish"` when the user asked for products field = Publish. If the cohort feeds transcript/message/ticket search, a cross-source join, or exhaustive/absence-sensitive coverage, use `provider-api-request` with HubSpot search and `stageAs` for the cohort first, then continue with the provider API/corpus workflow. Report the returned filter values and cohort count in the methodology. ' +
728
- "For named deal, account, renewal, churn-risk, or customer deep dives that need HubSpot and Gong context, `account-deep-dive` can provide a bounded evidence bundle. Use it when the user asks for a targeted entity deep dive, then use the broad provider API/corpus workflow for any requested exhaustive transcript/message/ticket search or absence claim. Do not answer a requested transcript deep dive from call metadata alone. " +
721
+ "When the user names a provider such as first-party Analytics, BigQuery, HubSpot, Gong, Jira, Pylon, Slack, Sentry, GA4, or another connected source, that source is authoritative for the turn. Use its first-class query action when available; if it is not on the initial tool surface, use tool-search for that provider instead of loading unrelated catalogs. For an ordinary structured lookup, make one bounded query and stop on success. " +
722
+ "Load provider API, corpus, staging, or code tools only when the user explicitly requests cross-source work, exhaustive unstructured-record coverage, or an absence claim that the first-class action cannot support. For those genuinely broad workflows, fetch every relevant page or an explicitly bounded cohort, preserve coverage counts, and state any uncovered records. " +
723
+ "For named deal, account, renewal, churn-risk, or customer deep dives that need HubSpot and Gong context, `account-deep-dive` can provide a bounded evidence bundle. Do not answer a requested transcript deep dive from call metadata alone. " +
729
724
  "When the user refers to the current analysis, this analysis, this project, or asks to spin off, adapt, modify, or reuse a saved analysis, call `view-screen` first and use the returned analysis details; if an analysis id or @mention is provided, call `get-analysis` before responding. " +
730
- "If a provider action fails, stop using that provider for the turn, surface the actual error, and wait for the user to choose whether to fix SQL, use another source, or retry. Do not loop through more queries after a failed provider call. " +
731
- "For ordinary ad-hoc, non-coverage-sensitive data questions, answer the explicit question after the first relevant successful query or bounded evidence batch instead of continuing into suggested follow-up investigations. " +
725
+ "If a query action fails because its arguments are invalid, correct the arguments once. Never repeat the identical failed call. For credential, permission, quota, network, or repeated schema failures, stop using that source for the turn and surface the actual error instead of trying unrelated providers. " +
726
+ "For ordinary ad-hoc structured data questions, answer the explicit question after the first relevant successful query or bounded evidence batch. The words all, total, or exact do not require cross-source validation when a single structured query fully covers the requested source and filters. " +
732
727
  "If the user challenges coverage, asks why more records were not included, or asks for the updated answer, rerun the relevant source query or revise from the corrected cohort and provide the updated deliverable directly. Do not claim an analysis was revised unless the revised answer is included in the response or saved with `save-analysis`. " +
733
728
  "Unstructured source records are valid analytics evidence: Pylon tickets, Jira issues, Gong calls/transcripts, Slack messages, and similar text records may be coded for themes, mention counts, sentiment, objections, and qualitative patterns as long as the answer states the inspected sample size and does not imply unsupported statistical certainty. " +
734
729
  "For schema questions, prefer data-dictionary entries and configured warehouse schemas over assumptions; use `search-bigquery-schema` for BigQuery metadata before inventing datasets, tables, or columns. " +
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-23
4
+ ---
5
+
6
+ Database toolbar Sort and Filter controls now open reliably with pointer and keyboard activation.
@@ -0,0 +1,14 @@
1
+ import { appPath } from "@agent-native/core/client/api-path";
2
+
3
+ interface PublicFormLink {
4
+ status: string;
5
+ slug: string;
6
+ }
7
+
8
+ export function getPublishedFormUrl(
9
+ form: PublicFormLink,
10
+ origin: string,
11
+ ): string | undefined {
12
+ if (form.status !== "published") return undefined;
13
+ return `${origin}${appPath(`/f/${form.slug}`)}`;
14
+ }
@@ -85,6 +85,7 @@ import {
85
85
  type FormBuilderTab,
86
86
  } from "@/lib/form-builder-tabs";
87
87
  import { normalizeFields } from "@/lib/normalize-fields";
88
+ import { getPublishedFormUrl } from "@/lib/public-form-link";
88
89
  import { cn } from "@/lib/utils";
89
90
 
90
91
  type Translator = ReturnType<typeof useT>;
@@ -175,8 +176,8 @@ export function FormBuilderPage() {
175
176
  const { isLocal } = useDbStatus();
176
177
  const [showCloudUpgrade, setShowCloudUpgrade] = useState(false);
177
178
  const publishedFormUrl =
178
- form?.status === "published" && typeof window !== "undefined"
179
- ? `${window.location.origin}${appPath(`/f/${form.slug}`)}`
179
+ form && typeof window !== "undefined"
180
+ ? getPublishedFormUrl(form, window.location.origin)
180
181
  : undefined;
181
182
  const [agentPopoverOpen, setAgentPopoverOpen] = useState(false);
182
183
  const [agentPrompt, setAgentPrompt] = useState("");
@@ -533,16 +534,11 @@ export function FormBuilderPage() {
533
534
  }
534
535
 
535
536
  function copyShareLink() {
536
- if (loadedForm.status !== "published") {
537
+ if (!publishedFormUrl) {
537
538
  toast.info(t("builder.publishBeforeCopyToast"));
538
539
  return;
539
540
  }
540
- if (isLocal) {
541
- setShowCloudUpgrade(true);
542
- return;
543
- }
544
- const url = `${window.location.origin}${appPath(`/f/${loadedForm.slug}`)}`;
545
- navigator.clipboard.writeText(url);
541
+ navigator.clipboard.writeText(publishedFormUrl);
546
542
  setCopied(true);
547
543
  setTimeout(() => setCopied(false), 2000);
548
544
  toast.success(t("builder.linkCopiedToast"));
@@ -611,11 +607,7 @@ export function FormBuilderPage() {
611
607
  className="h-10 w-10 active:scale-[0.96] motion-reduce:active:scale-100"
612
608
  asChild
613
609
  >
614
- <a
615
- href={appPath(`/f/${form.slug}`)}
616
- target="_blank"
617
- rel="noopener"
618
- >
610
+ <a href={publishedFormUrl} target="_blank" rel="noopener">
619
611
  <IconExternalLink className="h-4 w-4" />
620
612
  </a>
621
613
  </Button>
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-23
4
+ ---
5
+
6
+ Published form links can be copied without being asked to reconnect the database.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-23
4
+ ---
5
+
6
+ Scheduled emails now send only once when multiple requests race.
@@ -722,7 +722,7 @@ export async function sendScheduledJobNowForOwner(
722
722
  throw new Error(`Scheduled email is already ${job.status}`);
723
723
  }
724
724
 
725
- await db
725
+ const claim = await db
726
726
  .update(schema.scheduledJobs)
727
727
  .set({ status: "processing" } as any)
728
728
  .where(
@@ -732,6 +732,9 @@ export async function sendScheduledJobNowForOwner(
732
732
  eq(schema.scheduledJobs.status, "pending"),
733
733
  ),
734
734
  );
735
+ if (claim.rowsAffected === 0) {
736
+ throw new Error("Scheduled email is already processing");
737
+ }
735
738
 
736
739
  try {
737
740
  await sendScheduledEmail(
@@ -769,11 +772,17 @@ export async function markJobDone(id: string): Promise<void> {
769
772
  .where(eq(schema.scheduledJobs.id, id));
770
773
  }
771
774
 
772
- export async function markJobProcessing(id: string): Promise<void> {
773
- await db
775
+ export async function markJobProcessing(id: string): Promise<boolean> {
776
+ const result = await db
774
777
  .update(schema.scheduledJobs)
775
778
  .set({ status: "processing" } as any)
776
- .where(eq(schema.scheduledJobs.id, id));
779
+ .where(
780
+ and(
781
+ eq(schema.scheduledJobs.id, id),
782
+ eq(schema.scheduledJobs.status, "pending"),
783
+ ),
784
+ );
785
+ return result.rowsAffected > 0;
777
786
  }
778
787
 
779
788
  export async function getDuePendingJobs(
@@ -48,7 +48,7 @@ async function processJobs(): Promise<void> {
48
48
  const due = await getDuePendingJobs(now);
49
49
 
50
50
  for (const job of due) {
51
- await markJobProcessing(job.id);
51
+ if (!(await markJobProcessing(job.id))) continue;
52
52
 
53
53
  try {
54
54
  const ownerEmail = job.ownerEmail || job.accountEmail;