@agent-native/core 0.92.2 → 0.92.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 (172) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +32 -0
  3. package/corpus/core/docs/content/locales/ar-SA/server.mdx +35 -0
  4. package/corpus/core/docs/content/locales/de-DE/server.mdx +35 -0
  5. package/corpus/core/docs/content/locales/es-ES/server.mdx +35 -0
  6. package/corpus/core/docs/content/locales/fr-FR/server.mdx +35 -0
  7. package/corpus/core/docs/content/locales/hi-IN/server.mdx +35 -0
  8. package/corpus/core/docs/content/locales/ja-JP/server.mdx +35 -0
  9. package/corpus/core/docs/content/locales/ko-KR/server.mdx +35 -0
  10. package/corpus/core/docs/content/locales/pt-BR/server.mdx +35 -0
  11. package/corpus/core/docs/content/locales/zh-CN/server.mdx +35 -0
  12. package/corpus/core/docs/content/locales/zh-TW/server.mdx +35 -0
  13. package/corpus/core/docs/content/server.mdx +35 -0
  14. package/corpus/core/package.json +2 -2
  15. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +14 -4
  16. package/corpus/core/src/agent/engine/anthropic-engine.ts +15 -3
  17. package/corpus/core/src/agent/engine/builder-engine.ts +30 -4
  18. package/corpus/core/src/agent/engine/registry.ts +63 -2
  19. package/corpus/core/src/agent/model-config.ts +0 -2
  20. package/corpus/core/src/agent/production-agent.ts +12 -1
  21. package/corpus/core/src/agent/run-manager.ts +57 -36
  22. package/corpus/core/src/agent/run-store.ts +81 -2
  23. package/corpus/core/src/agent/thread-data-builder.ts +33 -2
  24. package/corpus/core/src/client/AssistantChat.tsx +314 -96
  25. package/corpus/core/src/client/agent-chat-adapter.ts +17 -1
  26. package/corpus/core/src/client/chat/repo-helpers.ts +45 -0
  27. package/corpus/core/src/client/chat/tool-call-display.tsx +36 -27
  28. package/corpus/core/src/client/chat-model-groups.ts +1 -5
  29. package/corpus/core/src/client/composer/TiptapComposer.tsx +0 -2
  30. package/corpus/core/src/client/embed-auth.ts +20 -0
  31. package/corpus/core/src/client/settings/SettingsSection.tsx +66 -5
  32. package/corpus/core/src/client/sse-event-processor.ts +49 -17
  33. package/corpus/core/src/client/use-db-sync.ts +31 -9
  34. package/corpus/core/src/collab/agent-presence.ts +6 -1
  35. package/corpus/core/src/collab/awareness-store.ts +185 -0
  36. package/corpus/core/src/collab/awareness.ts +58 -2
  37. package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +7 -8
  38. package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +7 -18
  39. package/corpus/core/src/server/core-routes-plugin.ts +21 -2
  40. package/corpus/core/src/server/embed-session.ts +20 -4
  41. package/corpus/core/src/styles/agent-native.css +51 -0
  42. package/corpus/core/src/tracking/providers.ts +33 -2
  43. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +8 -1
  44. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +9 -1
  45. package/corpus/templates/analytics/app/pages/monitoring/uptime/MonitorDetail.tsx +41 -11
  46. package/corpus/templates/analytics/app/pages/monitoring/uptime/types.ts +44 -0
  47. package/corpus/templates/analytics/changelog/2026-07-09-dashboard-action-menus-now-close-cleanly-before-opening-hist.md +6 -0
  48. package/corpus/templates/analytics/changelog/2026-07-09-dashboard-email-reports-still-send-when-the-screenshot-captu.md +6 -0
  49. package/corpus/templates/analytics/changelog/2026-07-09-uptime-alerts-now-suppress-recovery-noise-during-flapping-an.md +6 -0
  50. package/corpus/templates/analytics/changelog/2026-07-09-uptime-check-history-now-shows-timing-diagnostics-so-slow-or.md +6 -0
  51. package/corpus/templates/analytics/changelog/2026-07-09-uptime-recovery-alerts-now-send-when-the-original-outage-not.md +6 -0
  52. package/corpus/templates/analytics/docs/uptime-monitoring.md +29 -16
  53. package/corpus/templates/analytics/netlify.toml +3 -0
  54. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +126 -0
  55. package/corpus/templates/analytics/server/db/schema-monitoring.ts +5 -0
  56. package/corpus/templates/analytics/server/jobs/uptime-monitors.ts +5 -1
  57. package/corpus/templates/analytics/server/lib/uptime-monitors.ts +305 -31
  58. package/corpus/templates/analytics/server/plugins/db.ts +15 -0
  59. package/corpus/templates/analytics/server/plugins/uptime-monitor-jobs.ts +1 -1
  60. package/corpus/templates/analytics/server/routes/api/uptime-monitors/run.post.ts +57 -0
  61. package/corpus/templates/clips/actions/list-meetings.ts +12 -0
  62. package/corpus/templates/clips/changelog/2026-07-09-meeting-reminders-now-ignore-obvious-solo-personal-calendar-.md +6 -0
  63. package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +1 -0
  64. package/corpus/templates/clips/server/lib/calendar-event-classification.ts +79 -0
  65. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  66. package/dist/agent/engine/ai-sdk-engine.js +13 -4
  67. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  68. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  69. package/dist/agent/engine/anthropic-engine.js +14 -3
  70. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  71. package/dist/agent/engine/builder-engine.d.ts +1 -1
  72. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  73. package/dist/agent/engine/builder-engine.js +26 -4
  74. package/dist/agent/engine/builder-engine.js.map +1 -1
  75. package/dist/agent/engine/registry.d.ts.map +1 -1
  76. package/dist/agent/engine/registry.js +47 -2
  77. package/dist/agent/engine/registry.js.map +1 -1
  78. package/dist/agent/model-config.d.ts +2 -2
  79. package/dist/agent/model-config.d.ts.map +1 -1
  80. package/dist/agent/model-config.js +0 -2
  81. package/dist/agent/model-config.js.map +1 -1
  82. package/dist/agent/production-agent.d.ts.map +1 -1
  83. package/dist/agent/production-agent.js +12 -1
  84. package/dist/agent/production-agent.js.map +1 -1
  85. package/dist/agent/run-manager.d.ts.map +1 -1
  86. package/dist/agent/run-manager.js +52 -33
  87. package/dist/agent/run-manager.js.map +1 -1
  88. package/dist/agent/run-store.d.ts +24 -0
  89. package/dist/agent/run-store.d.ts.map +1 -1
  90. package/dist/agent/run-store.js +64 -2
  91. package/dist/agent/run-store.js.map +1 -1
  92. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  93. package/dist/agent/thread-data-builder.js +28 -2
  94. package/dist/agent/thread-data-builder.js.map +1 -1
  95. package/dist/client/AssistantChat.d.ts +6 -0
  96. package/dist/client/AssistantChat.d.ts.map +1 -1
  97. package/dist/client/AssistantChat.js +243 -36
  98. package/dist/client/AssistantChat.js.map +1 -1
  99. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  100. package/dist/client/agent-chat-adapter.js +15 -1
  101. package/dist/client/agent-chat-adapter.js.map +1 -1
  102. package/dist/client/chat/repo-helpers.d.ts.map +1 -1
  103. package/dist/client/chat/repo-helpers.js +43 -0
  104. package/dist/client/chat/repo-helpers.js.map +1 -1
  105. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  106. package/dist/client/chat/tool-call-display.js +4 -4
  107. package/dist/client/chat/tool-call-display.js.map +1 -1
  108. package/dist/client/chat-model-groups.d.ts.map +1 -1
  109. package/dist/client/chat-model-groups.js +1 -3
  110. package/dist/client/chat-model-groups.js.map +1 -1
  111. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  112. package/dist/client/composer/TiptapComposer.js +0 -2
  113. package/dist/client/composer/TiptapComposer.js.map +1 -1
  114. package/dist/client/embed-auth.d.ts.map +1 -1
  115. package/dist/client/embed-auth.js +14 -0
  116. package/dist/client/embed-auth.js.map +1 -1
  117. package/dist/client/settings/SettingsSection.d.ts.map +1 -1
  118. package/dist/client/settings/SettingsSection.js +32 -3
  119. package/dist/client/settings/SettingsSection.js.map +1 -1
  120. package/dist/client/sse-event-processor.d.ts.map +1 -1
  121. package/dist/client/sse-event-processor.js +44 -17
  122. package/dist/client/sse-event-processor.js.map +1 -1
  123. package/dist/client/use-db-sync.d.ts.map +1 -1
  124. package/dist/client/use-db-sync.js +29 -9
  125. package/dist/client/use-db-sync.js.map +1 -1
  126. package/dist/collab/agent-presence.d.ts.map +1 -1
  127. package/dist/collab/agent-presence.js +5 -2
  128. package/dist/collab/agent-presence.js.map +1 -1
  129. package/dist/collab/awareness-store.d.ts +1 -1
  130. package/dist/collab/awareness-store.d.ts.map +1 -1
  131. package/dist/collab/awareness-store.js +0 -0
  132. package/dist/collab/awareness-store.js.map +1 -1
  133. package/dist/collab/awareness.d.ts +4 -2
  134. package/dist/collab/awareness.d.ts.map +1 -1
  135. package/dist/collab/awareness.js +46 -2
  136. package/dist/collab/awareness.js.map +1 -1
  137. package/dist/collab/routes.d.ts +1 -1
  138. package/dist/collab/struct-routes.d.ts +1 -1
  139. package/dist/notifications/routes.d.ts +2 -2
  140. package/dist/observability/routes.d.ts +3 -3
  141. package/dist/progress/routes.d.ts +1 -1
  142. package/dist/resources/handlers.d.ts +1 -1
  143. package/dist/scripts/agent-engines/manage-agent-engine.d.ts.map +1 -1
  144. package/dist/scripts/agent-engines/manage-agent-engine.js +6 -6
  145. package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
  146. package/dist/scripts/agent-engines/set-agent-engine.d.ts.map +1 -1
  147. package/dist/scripts/agent-engines/set-agent-engine.js +5 -13
  148. package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
  149. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  150. package/dist/server/core-routes-plugin.d.ts +5 -0
  151. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  152. package/dist/server/core-routes-plugin.js +10 -3
  153. package/dist/server/core-routes-plugin.js.map +1 -1
  154. package/dist/server/embed-session.d.ts.map +1 -1
  155. package/dist/server/embed-session.js +17 -4
  156. package/dist/server/embed-session.js.map +1 -1
  157. package/dist/styles/agent-native.css +51 -0
  158. package/dist/tracking/providers.d.ts.map +1 -1
  159. package/dist/tracking/providers.js +23 -5
  160. package/dist/tracking/providers.js.map +1 -1
  161. package/docs/content/locales/ar-SA/server.mdx +35 -0
  162. package/docs/content/locales/de-DE/server.mdx +35 -0
  163. package/docs/content/locales/es-ES/server.mdx +35 -0
  164. package/docs/content/locales/fr-FR/server.mdx +35 -0
  165. package/docs/content/locales/hi-IN/server.mdx +35 -0
  166. package/docs/content/locales/ja-JP/server.mdx +35 -0
  167. package/docs/content/locales/ko-KR/server.mdx +35 -0
  168. package/docs/content/locales/pt-BR/server.mdx +35 -0
  169. package/docs/content/locales/zh-CN/server.mdx +35 -0
  170. package/docs/content/locales/zh-TW/server.mdx +35 -0
  171. package/docs/content/server.mdx +35 -0
  172. package/package.json +2 -2
@@ -0,0 +1,185 @@
1
+ /**
2
+ * SQL-backed awareness store — makes presence work across serverless
3
+ * instances.
4
+ *
5
+ * The in-memory awareness map in `awareness.ts` is per-process: on a
6
+ * multi-instance/serverless deployment, a cursor published to instance A is
7
+ * invisible to a client polling instance B, and an agent action running in
8
+ * its own invocation can't reach the SSE instance's memory at all. This
9
+ * store mirrors awareness rows into a `_collab_awareness` table (SQLite +
10
+ * Postgres portable) so every instance serves the same participant set.
11
+ *
12
+ * Everything here is best-effort: presence must never fail or slow down an
13
+ * edit. Writes are throttled per (docId, clientId) and skipped when the
14
+ * state hasn't changed; expired rows are purged opportunistically.
15
+ */
16
+
17
+ import { getDbExec, isPostgres } from "../db/client.js";
18
+ import { ensureTableExists } from "../db/ddl-guard.js";
19
+ import type { AwarenessEntry } from "./awareness.js";
20
+
21
+ /** Rows older than this are treated as expired (matches AWARENESS_TIMEOUT). */
22
+ const ROW_TTL_MS = 30_000;
23
+
24
+ /** Minimum interval between DB writes for an unchanged (docId, clientId). */
25
+ const WRITE_THROTTLE_MS = 2_000;
26
+
27
+ /** Minimum interval between opportunistic purges per document. */
28
+ const PURGE_INTERVAL_MS = 30_000;
29
+
30
+ let _initPromise: Promise<void> | undefined;
31
+
32
+ async function ensureTable(): Promise<void> {
33
+ if (!_initPromise) {
34
+ _initPromise = (async () => {
35
+ const client = getDbExec();
36
+ // client_id is a Yjs uint32 (can exceed int32 max) and last_seen is
37
+ // epoch milliseconds — both need BIGINT-range columns.
38
+ const createSql = `
39
+ CREATE TABLE IF NOT EXISTS _collab_awareness (
40
+ doc_id TEXT NOT NULL,
41
+ client_id BIGINT NOT NULL,
42
+ state TEXT NOT NULL,
43
+ last_seen BIGINT NOT NULL,
44
+ PRIMARY KEY (doc_id, client_id)
45
+ )
46
+ `;
47
+ if (isPostgres()) {
48
+ await ensureTableExists("_collab_awareness", createSql);
49
+ return;
50
+ }
51
+ await client.execute(createSql);
52
+ })().catch((err) => {
53
+ _initPromise = undefined;
54
+ throw err;
55
+ });
56
+ }
57
+ return _initPromise;
58
+ }
59
+
60
+ // (docId\0clientId) → { state, writtenAt } for write throttling.
61
+ const _lastWrites = new Map<string, { state: string; writtenAt: number }>();
62
+ // docId → last purge time.
63
+ const _lastPurges = new Map<string, number>();
64
+
65
+ function writeKey(docId: string, clientId: number): string {
66
+ return `${docId}\0${clientId}`;
67
+ }
68
+
69
+ /**
70
+ * Mirror a client's awareness state to SQL. Throttled: unchanged state is
71
+ * rewritten at most every {@link WRITE_THROTTLE_MS} (to refresh last_seen).
72
+ * Never throws.
73
+ */
74
+ export async function upsertAwarenessRow(
75
+ docId: string,
76
+ clientId: number,
77
+ state: string,
78
+ lastSeen: number,
79
+ ): Promise<void> {
80
+ try {
81
+ const key = writeKey(docId, clientId);
82
+ const prev = _lastWrites.get(key);
83
+ if (
84
+ prev &&
85
+ prev.state === state &&
86
+ lastSeen - prev.writtenAt < WRITE_THROTTLE_MS
87
+ ) {
88
+ return;
89
+ }
90
+
91
+ await ensureTable();
92
+ const client = getDbExec();
93
+ if (isPostgres()) {
94
+ await client.execute({
95
+ sql: `INSERT INTO _collab_awareness (doc_id, client_id, state, last_seen)
96
+ VALUES (?, ?, ?, ?)
97
+ ON CONFLICT (doc_id, client_id)
98
+ DO UPDATE SET state = EXCLUDED.state, last_seen = EXCLUDED.last_seen`,
99
+ args: [docId, clientId, state, lastSeen],
100
+ });
101
+ } else {
102
+ await client.execute({
103
+ sql: `INSERT OR REPLACE INTO _collab_awareness (doc_id, client_id, state, last_seen)
104
+ VALUES (?, ?, ?, ?)`,
105
+ args: [docId, clientId, state, lastSeen],
106
+ });
107
+ }
108
+ _lastWrites.set(key, { state, writtenAt: lastSeen });
109
+
110
+ await maybePurge(docId, lastSeen);
111
+ } catch {
112
+ // Best-effort — presence never fails a request.
113
+ }
114
+ }
115
+
116
+ /** Remove a client's row (participant left). Never throws. */
117
+ export async function deleteAwarenessRow(
118
+ docId: string,
119
+ clientId: number,
120
+ maxLastSeen?: number,
121
+ ): Promise<void> {
122
+ try {
123
+ await ensureTable();
124
+ const client = getDbExec();
125
+ if (maxLastSeen == null) {
126
+ await client.execute({
127
+ sql: `DELETE FROM _collab_awareness WHERE doc_id = ? AND client_id = ?`,
128
+ args: [docId, clientId],
129
+ });
130
+ } else {
131
+ await client.execute({
132
+ sql: `DELETE FROM _collab_awareness
133
+ WHERE doc_id = ? AND client_id = ? AND last_seen <= ?`,
134
+ args: [docId, clientId, maxLastSeen],
135
+ });
136
+ }
137
+ _lastWrites.delete(writeKey(docId, clientId));
138
+ } catch {
139
+ // Best-effort.
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Load non-expired awareness rows for a document. Returns [] on any
145
+ * failure (memory-only degradation).
146
+ */
147
+ export async function loadAwarenessRows(
148
+ docId: string,
149
+ now: number = Date.now(),
150
+ ): Promise<AwarenessEntry[]> {
151
+ try {
152
+ await ensureTable();
153
+ const client = getDbExec();
154
+ const { rows } = await client.execute({
155
+ sql: `SELECT client_id, state, last_seen FROM _collab_awareness
156
+ WHERE doc_id = ? AND last_seen >= ?`,
157
+ args: [docId, now - ROW_TTL_MS],
158
+ });
159
+ return rows.map((row: any) => ({
160
+ clientId: Number(row.client_id),
161
+ state: String(row.state),
162
+ lastSeen: Number(row.last_seen),
163
+ }));
164
+ } catch {
165
+ return [];
166
+ }
167
+ }
168
+
169
+ async function maybePurge(docId: string, now: number): Promise<void> {
170
+ const last = _lastPurges.get(docId) ?? 0;
171
+ if (now - last < PURGE_INTERVAL_MS) return;
172
+ _lastPurges.set(docId, now);
173
+ const client = getDbExec();
174
+ await client.execute({
175
+ sql: `DELETE FROM _collab_awareness WHERE doc_id = ? AND last_seen < ?`,
176
+ args: [docId, now - ROW_TTL_MS * 2],
177
+ });
178
+ }
179
+
180
+ /** Test hook: reset module-level throttles. */
181
+ export function _resetAwarenessStoreForTests(): void {
182
+ _lastWrites.clear();
183
+ _lastPurges.clear();
184
+ _initPromise = undefined;
185
+ }
@@ -24,6 +24,7 @@ import {
24
24
  } from "./awareness-store.js";
25
25
 
26
26
  const AWARENESS_TIMEOUT = 30_000; // 30 seconds
27
+ const CLEAR_TOMBSTONE_TTL = AWARENESS_TIMEOUT + 5_000;
27
28
 
28
29
  export interface AwarenessEntry {
29
30
  clientId: number;
@@ -106,6 +107,53 @@ export function emitAwarenessChange(
106
107
 
107
108
  // docId → Map<clientId, AwarenessEntry>
108
109
  const _awarenessMap = new Map<string, Map<number, AwarenessEntry>>();
110
+ // docId + clientId -> clearedAt. Prevents a stale SQL mirror from resurrecting
111
+ // a participant after an explicit leave/delete raced with the next poll.
112
+ const _awarenessClearTombstones = new Map<string, number>();
113
+
114
+ function awarenessKey(docId: string, clientId: number): string {
115
+ return `${docId}\0${clientId}`;
116
+ }
117
+
118
+ function pruneAwarenessClearTombstones(now: number): void {
119
+ for (const [key, clearedAt] of _awarenessClearTombstones) {
120
+ if (now - clearedAt > CLEAR_TOMBSTONE_TTL) {
121
+ _awarenessClearTombstones.delete(key);
122
+ }
123
+ }
124
+ }
125
+
126
+ export function rememberAwarenessClear(
127
+ docId: string,
128
+ clientId: number,
129
+ clearedAt: number = Date.now(),
130
+ ): void {
131
+ pruneAwarenessClearTombstones(clearedAt);
132
+ const key = awarenessKey(docId, clientId);
133
+ const prev = _awarenessClearTombstones.get(key);
134
+ if (prev == null || clearedAt > prev) {
135
+ _awarenessClearTombstones.set(key, clearedAt);
136
+ }
137
+ }
138
+
139
+ export function forgetAwarenessClear(docId: string, clientId: number): void {
140
+ _awarenessClearTombstones.delete(awarenessKey(docId, clientId));
141
+ }
142
+
143
+ function isBlockedByAwarenessClear(
144
+ docId: string,
145
+ clientId: number,
146
+ lastSeen: number,
147
+ now: number,
148
+ ): boolean {
149
+ pruneAwarenessClearTombstones(now);
150
+ const key = awarenessKey(docId, clientId);
151
+ const clearedAt = _awarenessClearTombstones.get(key);
152
+ if (clearedAt == null) return false;
153
+ if (lastSeen <= clearedAt) return true;
154
+ _awarenessClearTombstones.delete(key);
155
+ return false;
156
+ }
109
157
 
110
158
  export function getDocAwareness(docId: string): Map<number, AwarenessEntry> {
111
159
  let map = _awarenessMap.get(docId);
@@ -143,8 +191,13 @@ async function mergeStoredAwareness(
143
191
  docId: string,
144
192
  map: Map<number, AwarenessEntry>,
145
193
  ): Promise<void> {
146
- const rows = await loadAwarenessRows(docId);
194
+ const now = Date.now();
195
+ const rows = await loadAwarenessRows(docId, now);
147
196
  for (const row of rows) {
197
+ if (isBlockedByAwarenessClear(docId, row.clientId, row.lastSeen, now)) {
198
+ void deleteAwarenessRow(docId, row.clientId, row.lastSeen);
199
+ continue;
200
+ }
148
201
  const existing = map.get(row.clientId);
149
202
  if (!existing || row.lastSeen > existing.lastSeen) {
150
203
  map.set(row.clientId, row);
@@ -187,10 +240,13 @@ export const postAwareness = defineEventHandler(async (event: H3Event) => {
187
240
  const map = getDocAwareness(docId);
188
241
 
189
242
  if (state === null) {
243
+ const clearedAt = Date.now();
190
244
  map.delete(clientId);
245
+ rememberAwarenessClear(docId, clientId, clearedAt);
191
246
  // Best-effort cross-instance removal (never blocks the response).
192
- void deleteAwarenessRow(docId, clientId);
247
+ void deleteAwarenessRow(docId, clientId, clearedAt);
193
248
  } else {
249
+ forgetAwarenessClear(docId, clientId);
194
250
  // Store this client's state
195
251
  const entry = { clientId, state, lastSeen: Date.now() };
196
252
  map.set(clientId, entry);
@@ -113,12 +113,7 @@ async function runSetAppDefault(args: Record<string, string>): Promise<string> {
113
113
  if (!isAgentEnginePackageInstalled(entry)) {
114
114
  return `Error: Engine "${engine}" requires optional packages that are not installed in this app. Run: pnpm add ${entry.installPackage}`;
115
115
  }
116
- if (
117
- entry.name === "builder" &&
118
- normalizeModelForEngine(entry, model) !== model
119
- ) {
120
- return `Error: Model "${model}" is not supported by Builder. Choose one of: ${entry.supportedModels.join(", ")}`;
121
- }
116
+ const normalizedModel = normalizeModelForEngine(entry, model);
122
117
 
123
118
  const ctx = currentContext();
124
119
  const canUpdate = await canUpdateAgentAppModelDefaultSettings(
@@ -133,14 +128,18 @@ async function runSetAppDefault(args: Record<string, string>): Promise<string> {
133
128
 
134
129
  const settings = await writeAgentAppModelDefaultSettings(ctx, appId, {
135
130
  engine,
136
- model,
131
+ model: normalizedModel,
137
132
  updatedBy: ctx.userEmail,
138
133
  });
134
+ const normalizedNote =
135
+ normalizedModel === model
136
+ ? ""
137
+ : ` Requested model "${model}" is no longer supported, so "${normalizedModel}" was saved instead.`;
139
138
  return JSON.stringify(
140
139
  {
141
140
  ok: true,
142
141
  ...settings,
143
- message: `Default model for ${appId} set to ${model} via ${entry.label}.`,
142
+ message: `Default model for ${appId} set to ${normalizedModel} via ${entry.label}.${normalizedNote}`,
144
143
  },
145
144
  null,
146
145
  2,
@@ -53,20 +53,8 @@ export async function run(args: Record<string, string>): Promise<string> {
53
53
  return `Error: Engine "${engineName}" requires optional packages that are not installed in this app. Run: pnpm add ${entry.installPackage}`;
54
54
  }
55
55
 
56
- const resolvedModel = model ?? entry.defaultModel;
57
- if (
58
- entry.name === "builder" &&
59
- normalizeModelForEngine(entry, resolvedModel) !== resolvedModel
60
- ) {
61
- return `Error: Model "${resolvedModel}" is not supported by Builder. Choose one of: ${entry.supportedModels.join(", ")}`;
62
- }
63
-
64
- // supportedModels is a suggestion list, not an allowlist — accept any
65
- // string for BYO/custom engines (OpenRouter / Ollama / previews) and flag
66
- // uncurated saves. Builder is an enumerated gateway and is checked above.
67
- const modelIsCurated =
68
- entry.supportedModels.length === 0 ||
69
- entry.supportedModels.includes(resolvedModel);
56
+ const requestedModel = model ?? entry.defaultModel;
57
+ const resolvedModel = normalizeModelForEngine(entry, requestedModel);
70
58
 
71
59
  const usable = await isStoredEngineUsableForRequest(
72
60
  { engine: engineName },
@@ -82,14 +70,15 @@ export async function run(args: Record<string, string>): Promise<string> {
82
70
  model: resolvedModel,
83
71
  });
84
72
 
85
- const customNote = modelIsCurated
86
- ? ""
87
- : ` (model "${resolvedModel}" isn't in the curated list for ${entry.label}; saved as a custom model — verify it's a real ID for this provider)`;
73
+ const normalizedNote =
74
+ resolvedModel === requestedModel
75
+ ? ""
76
+ : ` Requested model "${requestedModel}" is no longer supported, so "${resolvedModel}" was saved instead.`;
88
77
 
89
78
  return JSON.stringify({
90
79
  ok: true,
91
80
  engine: engineName,
92
81
  model: resolvedModel,
93
- message: `Agent engine set to ${entry.label} with model ${resolvedModel}. Takes effect on the next conversation.${customNote}`,
82
+ message: `Agent engine set to ${entry.label} with model ${resolvedModel}. Takes effect on the next conversation.${normalizedNote}`,
94
83
  });
95
84
  }
@@ -25,6 +25,7 @@ import {
25
25
  detectEngineFromEnv,
26
26
  detectEngineFromUserSecrets,
27
27
  isStoredEngineUsableForRequest,
28
+ normalizeModelForEngine,
28
29
  } from "../agent/engine/registry.js";
29
30
  import {
30
31
  canUpdateAgentLoopSettings,
@@ -175,6 +176,16 @@ export const FRAMEWORK_ROUTE_PREFIX = "/_agent-native";
175
176
  export const FRAMEWORK_EVENTS_ROUTE = `${FRAMEWORK_ROUTE_PREFIX}/events`;
176
177
  export const LEGACY_FRAMEWORK_EVENTS_ROUTE = `${FRAMEWORK_ROUTE_PREFIX}/poll-events`;
177
178
 
179
+ export function normalizeAgentEngineStatusModel(
180
+ entry:
181
+ | { name: string; defaultModel: string; supportedModels: readonly string[] }
182
+ | undefined,
183
+ model: string | null | undefined,
184
+ ): string {
185
+ if (!entry) return model ?? DEFAULT_MODEL;
186
+ return normalizeModelForEngine(entry, model ?? entry.defaultModel);
187
+ }
188
+
178
189
  export function getFrameworkEnvKeys(): EnvKeyConfig[] {
179
190
  return [
180
191
  { key: "ENABLE_BUILDER", label: "Enable Builder.io features" },
@@ -2727,10 +2738,14 @@ export function createCoreRoutesPlugin(
2727
2738
  if (isAgentEngineSettingConfigured(stored)) {
2728
2739
  const engine = (stored as { engine: string }).engine;
2729
2740
  const entry = getAgentEngineEntry(engine);
2741
+ const model = normalizeAgentEngineStatusModel(
2742
+ entry,
2743
+ stored?.model,
2744
+ );
2730
2745
  return {
2731
2746
  configured: true,
2732
2747
  engine,
2733
- model: stored?.model ?? entry?.defaultModel ?? DEFAULT_MODEL,
2748
+ model,
2734
2749
  source: "settings" as const,
2735
2750
  openAiBaseUrlConfigured,
2736
2751
  };
@@ -2775,10 +2790,14 @@ export function createCoreRoutesPlugin(
2775
2790
  isStoredEngineUsableForRequest(stored, entry),
2776
2791
  ))
2777
2792
  ) {
2793
+ const model = normalizeAgentEngineStatusModel(
2794
+ entry,
2795
+ stored.model,
2796
+ );
2778
2797
  return {
2779
2798
  configured: true,
2780
2799
  engine: stored.engine,
2781
- model: stored.model ?? entry.defaultModel ?? DEFAULT_MODEL,
2800
+ model,
2782
2801
  source: "env" as const,
2783
2802
  envVar: entry.requiredEnvVars[0],
2784
2803
  openAiBaseUrlConfigured,
@@ -44,10 +44,22 @@ const EMBED_ROUTE_ALIASES: Record<string, string[]> = {
44
44
  // should survive that first-hop redirect instead of falling back to the
45
45
  // private deployment token gate.
46
46
  "/": ["/overview"],
47
- "/dashboard": ["/adhoc/agent-native-templates-first-party"],
48
- "/dashboards": ["/adhoc/agent-native-templates-first-party"],
49
- "/traffic": ["/adhoc/agent-native-templates-first-party"],
50
- "/traffic-dashboard": ["/adhoc/agent-native-templates-first-party"],
47
+ "/dashboard": [
48
+ "/dashboards/agent-native-templates-first-party",
49
+ "/adhoc/agent-native-templates-first-party",
50
+ ],
51
+ "/dashboards": [
52
+ "/dashboards/agent-native-templates-first-party",
53
+ "/adhoc/agent-native-templates-first-party",
54
+ ],
55
+ "/traffic": [
56
+ "/dashboards/agent-native-templates-first-party",
57
+ "/adhoc/agent-native-templates-first-party",
58
+ ],
59
+ "/traffic-dashboard": [
60
+ "/dashboards/agent-native-templates-first-party",
61
+ "/adhoc/agent-native-templates-first-party",
62
+ ],
51
63
  };
52
64
 
53
65
  let _initPromise: Promise<void> | undefined;
@@ -263,6 +275,10 @@ function openRouteTargetPathnames(targetPath: string): Set<string> {
263
275
  targets,
264
276
  `/adhoc/${encodeURIComponent(dashboardId)}`,
265
277
  );
278
+ addResolvedOpenRoutePath(
279
+ targets,
280
+ `/dashboards/${encodeURIComponent(dashboardId)}`,
281
+ );
266
282
  }
267
283
  const analysisId = safePathSegment(url.searchParams.get("analysisId"));
268
284
  if (view === "analyses" && analysisId) {
@@ -515,6 +515,57 @@
515
515
  --ring: 215 12% 60%;
516
516
  }
517
517
 
518
+ /* Settings sections: animate between closed and intrinsic open height. */
519
+ .agent-native-settings-section-body {
520
+ display: flow-root;
521
+ height: auto;
522
+ overflow: hidden;
523
+ overflow: clip;
524
+ opacity: 1;
525
+ clip-path: inset(0);
526
+ interpolate-size: allow-keywords;
527
+ transition:
528
+ height 200ms cubic-bezier(0.2, 0, 0, 1),
529
+ opacity 150ms ease,
530
+ clip-path 200ms cubic-bezier(0.2, 0, 0, 1);
531
+ }
532
+
533
+ .agent-native-settings-section-body[data-state="closed"] {
534
+ height: 0;
535
+ opacity: 0;
536
+ clip-path: inset(0 0 100% 0);
537
+ pointer-events: none;
538
+ }
539
+
540
+ @starting-style {
541
+ .agent-native-settings-section-body[data-state="open"] {
542
+ height: 0;
543
+ opacity: 0;
544
+ clip-path: inset(0 0 100% 0);
545
+ }
546
+ }
547
+
548
+ @supports not (interpolate-size: allow-keywords) {
549
+ .agent-native-settings-section-body {
550
+ height: auto;
551
+ max-height: 64rem;
552
+ transition:
553
+ max-height 200ms cubic-bezier(0.2, 0, 0, 1),
554
+ opacity 150ms ease,
555
+ clip-path 200ms cubic-bezier(0.2, 0, 0, 1);
556
+ }
557
+
558
+ .agent-native-settings-section-body[data-state="closed"] {
559
+ max-height: 0;
560
+ }
561
+ }
562
+
563
+ @media (prefers-reduced-motion: reduce) {
564
+ .agent-native-settings-section-body {
565
+ transition: none;
566
+ }
567
+ }
568
+
518
569
  /* Full settings page section bodies.
519
570
  The framework's shared settings sections (LLM, App model defaults, Agent
520
571
  limits, Email, Account, Secrets, Usage, Voice, Automations, A2A, hosting
@@ -30,6 +30,10 @@ interface QueuedEvent {
30
30
  headers?: Record<string, string>;
31
31
  }
32
32
 
33
+ interface EnqueueOptions {
34
+ flushImmediately?: boolean;
35
+ }
36
+
33
37
  // Use globalThis so multiple ESM graph instances (Vite dev + Nitro symlinks)
34
38
  // share one queue, matching the same pattern as the tracking registry.
35
39
  const QUEUE_KEY = Symbol.for("@agent-native/core/tracking.queue");
@@ -59,10 +63,11 @@ function enqueue(
59
63
  url: string,
60
64
  body: string,
61
65
  headers?: Record<string, string>,
66
+ options?: EnqueueOptions,
62
67
  ): void {
63
68
  const queue = getQueue();
64
69
  queue.push({ url, body, headers });
65
- if (queue.length >= MAX_BATCH_SIZE) {
70
+ if (options?.flushImmediately || queue.length >= MAX_BATCH_SIZE) {
66
71
  void drainQueue();
67
72
  } else if (!getTimer()) {
68
73
  const timer = setTimeout(() => {
@@ -132,6 +137,25 @@ function shouldSkipAgentNativeAnalyticsForLocalhost(): boolean {
132
137
  ].some(isLocalhostUrl);
133
138
  }
134
139
 
140
+ function isServerlessRuntime(): boolean {
141
+ return Boolean(
142
+ process.env.NETLIFY ||
143
+ process.env.VERCEL ||
144
+ process.env.AWS_LAMBDA_FUNCTION_NAME ||
145
+ process.env.AWS_EXECUTION_ENV ||
146
+ process.env.LAMBDA_TASK_ROOT ||
147
+ process.env.FUNCTION_NAME,
148
+ );
149
+ }
150
+
151
+ function agentNativeAnalyticsFlushesImmediately(): boolean {
152
+ const mode =
153
+ process.env.AGENT_NATIVE_ANALYTICS_FLUSH_MODE?.trim().toLowerCase();
154
+ if (mode === "batch") return false;
155
+ if (mode === "immediate") return true;
156
+ return isServerlessRuntime();
157
+ }
158
+
135
159
  // ─── PostHog ───────────────────────────────────────────────────────────────
136
160
 
137
161
  function isPostHogAiObservabilityEvent(eventName: string): boolean {
@@ -307,6 +331,7 @@ function createAgentNativeAnalyticsProvider(
307
331
  publicKey: string,
308
332
  endpoint: string,
309
333
  ): TrackingProvider {
334
+ const flushImmediately = agentNativeAnalyticsFlushesImmediately();
310
335
  return {
311
336
  name: "agent-native-analytics",
312
337
  track(event: TrackingEvent) {
@@ -319,6 +344,8 @@ function createAgentNativeAnalyticsProvider(
319
344
  userId: event.userId,
320
345
  timestamp: event.timestamp,
321
346
  }),
347
+ undefined,
348
+ { flushImmediately },
322
349
  );
323
350
  },
324
351
  identify(userId, traits) {
@@ -331,6 +358,8 @@ function createAgentNativeAnalyticsProvider(
331
358
  properties: traits ?? {},
332
359
  timestamp: new Date().toISOString(),
333
360
  }),
361
+ undefined,
362
+ { flushImmediately },
334
363
  );
335
364
  },
336
365
  flush: () => {
@@ -366,7 +395,9 @@ export function registerBuiltinProviders(): void {
366
395
  registerTrackingProvider(createAmplitudeProvider(amplitudeKey));
367
396
  }
368
397
 
369
- const agentNativeAnalyticsKey = process.env.AGENT_NATIVE_ANALYTICS_PUBLIC_KEY;
398
+ const agentNativeAnalyticsKey =
399
+ process.env.AGENT_NATIVE_ANALYTICS_PUBLIC_KEY ||
400
+ process.env.VITE_AGENT_NATIVE_ANALYTICS_PUBLIC_KEY;
370
401
  if (
371
402
  agentNativeAnalyticsKey &&
372
403
  !shouldSkipAgentNativeAnalyticsForLocalhost()
@@ -199,6 +199,7 @@ export default function ExplorerDashboardPage() {
199
199
  const [loaded, setLoaded] = useState(false);
200
200
  const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
201
201
  const [historyOpen, setHistoryOpen] = useState(false);
202
+ const [dashboardActionsOpen, setDashboardActionsOpen] = useState(false);
202
203
  const [activeDragChartId, setActiveDragChartId] = useState<string | null>(
203
204
  null,
204
205
  );
@@ -620,7 +621,10 @@ export default function ExplorerDashboardPage() {
620
621
  </Button>
621
622
  ) : null}
622
623
  {dashboardId || canEdit || canManage ? (
623
- <DropdownMenu>
624
+ <DropdownMenu
625
+ open={dashboardActionsOpen}
626
+ onOpenChange={setDashboardActionsOpen}
627
+ >
624
628
  <Tooltip>
625
629
  <TooltipTrigger asChild>
626
630
  <DropdownMenuTrigger asChild>
@@ -643,6 +647,7 @@ export default function ExplorerDashboardPage() {
643
647
  <DropdownMenuItem
644
648
  onSelect={(event) => {
645
649
  event.preventDefault();
650
+ setDashboardActionsOpen(false);
646
651
  setHistoryOpen(true);
647
652
  }}
648
653
  >
@@ -657,6 +662,7 @@ export default function ExplorerDashboardPage() {
657
662
  <DropdownMenuItem
658
663
  onSelect={(event) => {
659
664
  event.preventDefault();
665
+ setDashboardActionsOpen(false);
660
666
  void handleArchive();
661
667
  }}
662
668
  >
@@ -671,6 +677,7 @@ export default function ExplorerDashboardPage() {
671
677
  <DropdownMenuItem
672
678
  onSelect={(event) => {
673
679
  event.preventDefault();
680
+ setDashboardActionsOpen(false);
674
681
  setConfirmDeleteOpen(true);
675
682
  }}
676
683
  className="text-destructive focus:text-destructive"
@@ -464,6 +464,7 @@ export default function SqlDashboardPage() {
464
464
  const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
465
465
  const [emailReportOpen, setEmailReportOpen] = useState(false);
466
466
  const [historyOpen, setHistoryOpen] = useState(false);
467
+ const [dashboardActionsOpen, setDashboardActionsOpen] = useState(false);
467
468
  const [activeDropSlot, setActiveDropSlot] =
468
469
  useState<DashboardDropSlot | null>(null);
469
470
  const [activeDragPanelId, setActiveDragPanelId] = useState<string | null>(
@@ -1395,7 +1396,10 @@ export default function SqlDashboardPage() {
1395
1396
  </Button>
1396
1397
  </AddPanelPopover>
1397
1398
  ) : null}
1398
- <DropdownMenu>
1399
+ <DropdownMenu
1400
+ open={dashboardActionsOpen}
1401
+ onOpenChange={setDashboardActionsOpen}
1402
+ >
1399
1403
  <Tooltip>
1400
1404
  <TooltipTrigger asChild>
1401
1405
  <DropdownMenuTrigger asChild>
@@ -1468,6 +1472,7 @@ export default function SqlDashboardPage() {
1468
1472
  <DropdownMenuItem
1469
1473
  onSelect={(event) => {
1470
1474
  event.preventDefault();
1475
+ setDashboardActionsOpen(false);
1471
1476
  setEmailReportOpen(true);
1472
1477
  }}
1473
1478
  >
@@ -1477,6 +1482,7 @@ export default function SqlDashboardPage() {
1477
1482
  <DropdownMenuItem
1478
1483
  onSelect={(event) => {
1479
1484
  event.preventDefault();
1485
+ setDashboardActionsOpen(false);
1480
1486
  setHistoryOpen(true);
1481
1487
  }}
1482
1488
  >
@@ -1489,6 +1495,7 @@ export default function SqlDashboardPage() {
1489
1495
  <DropdownMenuItem
1490
1496
  onSelect={(event) => {
1491
1497
  event.preventDefault();
1498
+ setDashboardActionsOpen(false);
1492
1499
  void handleArchive();
1493
1500
  }}
1494
1501
  >
@@ -1503,6 +1510,7 @@ export default function SqlDashboardPage() {
1503
1510
  <DropdownMenuItem
1504
1511
  onSelect={(event) => {
1505
1512
  event.preventDefault();
1513
+ setDashboardActionsOpen(false);
1506
1514
  setConfirmDeleteOpen(true);
1507
1515
  }}
1508
1516
  className="text-destructive focus:text-destructive"