@agent-native/core 0.98.5 → 0.98.6

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 (123) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/docs/content/external-agents.mdx +62 -7
  4. package/corpus/core/docs/content/locales/ar-SA/external-agents.mdx +38 -7
  5. package/corpus/core/docs/content/locales/de-DE/external-agents.mdx +39 -7
  6. package/corpus/core/docs/content/locales/es-ES/external-agents.mdx +39 -7
  7. package/corpus/core/docs/content/locales/fr-FR/external-agents.mdx +39 -7
  8. package/corpus/core/docs/content/locales/hi-IN/external-agents.mdx +38 -7
  9. package/corpus/core/docs/content/locales/ja-JP/external-agents.mdx +38 -7
  10. package/corpus/core/docs/content/locales/ko-KR/external-agents.mdx +38 -7
  11. package/corpus/core/docs/content/locales/pt-BR/external-agents.mdx +39 -7
  12. package/corpus/core/docs/content/locales/zh-CN/external-agents.mdx +37 -7
  13. package/corpus/core/docs/content/locales/zh-TW/external-agents.mdx +37 -7
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/a2a/handlers.ts +95 -21
  16. package/corpus/core/src/a2a/task-store.ts +65 -3
  17. package/corpus/core/src/integrations/adapters/slack.ts +123 -13
  18. package/corpus/core/src/integrations/identity-links-store.ts +210 -0
  19. package/corpus/core/src/integrations/identity.ts +132 -0
  20. package/corpus/core/src/integrations/index.ts +6 -0
  21. package/corpus/core/src/integrations/plugin.ts +58 -1
  22. package/corpus/core/src/integrations/types.ts +7 -0
  23. package/corpus/core/src/mcp/build-server.ts +127 -22
  24. package/corpus/core/src/mcp/external-agent-policy.ts +18 -0
  25. package/corpus/core/src/mcp/index.ts +1 -0
  26. package/corpus/core/src/server/agent-chat/plugin-options.ts +12 -1
  27. package/corpus/core/src/server/agent-chat/script-entries.ts +20 -3
  28. package/corpus/core/src/server/agent-chat-plugin.ts +3 -0
  29. package/corpus/core/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
  30. package/corpus/templates/analytics/AGENTS.md +20 -5
  31. package/corpus/templates/analytics/actions/create-session-replay-agent-link.ts +0 -1
  32. package/corpus/templates/analytics/actions/get-session-replay-events.ts +0 -1
  33. package/corpus/templates/analytics/actions/get-session-replay-timeline.ts +40 -0
  34. package/corpus/templates/analytics/actions/list-error-issues.ts +12 -0
  35. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +4 -1
  36. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +48 -17
  37. package/corpus/templates/analytics/changelog/2026-07-12-connected-external-agents-can-now-look-up-sessions-error-iss.md +6 -0
  38. package/corpus/templates/analytics/changelog/2026-07-12-session-identities-stay-visible-when-demo-mode-is-off.md +6 -0
  39. package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +2 -2
  40. package/corpus/templates/analytics/server/lib/error-capture.ts +47 -5
  41. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +49 -17
  42. package/corpus/templates/analytics/server/lib/session-replay.ts +16 -7
  43. package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -0
  44. package/corpus/templates/analytics/server/plugins/db.ts +15 -0
  45. package/corpus/templates/forms/app/global.css +54 -8
  46. package/corpus/templates/forms/app/pages/AskPage.tsx +35 -13
  47. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -17
  48. package/corpus/templates/forms/app/pages/ResponsesPage.tsx +8 -5
  49. package/corpus/templates/forms/changelog/2026-07-12-ask-forms-suggestions-now-sit-beneath-the-composer-for-a-tig.md +6 -0
  50. package/corpus/templates/forms/changelog/2026-07-12-builder-response-tables-now-fill-the-view-without-a-redundan.md +6 -0
  51. package/corpus/templates/forms/changelog/2026-07-12-response-tables-now-fill-the-available-pane-with-a-flexible-.md +6 -0
  52. package/corpus/templates/mail/AGENTS.md +3 -1
  53. package/corpus/templates/mail/server/lib/mail-integrations.ts +3 -0
  54. package/dist/a2a/handlers.d.ts.map +1 -1
  55. package/dist/a2a/handlers.js +84 -15
  56. package/dist/a2a/handlers.js.map +1 -1
  57. package/dist/a2a/task-store.d.ts +20 -1
  58. package/dist/a2a/task-store.d.ts.map +1 -1
  59. package/dist/a2a/task-store.js +57 -4
  60. package/dist/a2a/task-store.js.map +1 -1
  61. package/dist/collab/struct-routes.d.ts +1 -1
  62. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  63. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  64. package/dist/integrations/adapters/slack.js +98 -13
  65. package/dist/integrations/adapters/slack.js.map +1 -1
  66. package/dist/integrations/identity-links-store.d.ts +23 -0
  67. package/dist/integrations/identity-links-store.d.ts.map +1 -0
  68. package/dist/integrations/identity-links-store.js +158 -0
  69. package/dist/integrations/identity-links-store.js.map +1 -0
  70. package/dist/integrations/identity.d.ts +11 -0
  71. package/dist/integrations/identity.d.ts.map +1 -0
  72. package/dist/integrations/identity.js +100 -0
  73. package/dist/integrations/identity.js.map +1 -0
  74. package/dist/integrations/index.d.ts +2 -0
  75. package/dist/integrations/index.d.ts.map +1 -1
  76. package/dist/integrations/index.js +2 -0
  77. package/dist/integrations/index.js.map +1 -1
  78. package/dist/integrations/plugin.d.ts.map +1 -1
  79. package/dist/integrations/plugin.js +43 -1
  80. package/dist/integrations/plugin.js.map +1 -1
  81. package/dist/integrations/types.d.ts +6 -0
  82. package/dist/integrations/types.d.ts.map +1 -1
  83. package/dist/integrations/types.js.map +1 -1
  84. package/dist/mcp/build-server.d.ts +9 -2
  85. package/dist/mcp/build-server.d.ts.map +1 -1
  86. package/dist/mcp/build-server.js +97 -20
  87. package/dist/mcp/build-server.js.map +1 -1
  88. package/dist/mcp/external-agent-policy.d.ts +19 -0
  89. package/dist/mcp/external-agent-policy.d.ts.map +1 -0
  90. package/dist/mcp/external-agent-policy.js +2 -0
  91. package/dist/mcp/external-agent-policy.js.map +1 -0
  92. package/dist/mcp/index.d.ts +1 -0
  93. package/dist/mcp/index.d.ts.map +1 -1
  94. package/dist/mcp/index.js.map +1 -1
  95. package/dist/notifications/routes.d.ts +1 -1
  96. package/dist/progress/routes.d.ts +1 -1
  97. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  98. package/dist/resources/handlers.d.ts +3 -3
  99. package/dist/server/agent-chat/plugin-options.d.ts +11 -1
  100. package/dist/server/agent-chat/plugin-options.d.ts.map +1 -1
  101. package/dist/server/agent-chat/plugin-options.js.map +1 -1
  102. package/dist/server/agent-chat/script-entries.d.ts.map +1 -1
  103. package/dist/server/agent-chat/script-entries.js +16 -2
  104. package/dist/server/agent-chat/script-entries.js.map +1 -1
  105. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  106. package/dist/server/agent-chat-plugin.js +3 -0
  107. package/dist/server/agent-chat-plugin.js.map +1 -1
  108. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  109. package/dist/server/transcribe-voice.d.ts +1 -1
  110. package/dist/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
  111. package/docs/content/external-agents.mdx +62 -7
  112. package/docs/content/locales/ar-SA/external-agents.mdx +38 -7
  113. package/docs/content/locales/de-DE/external-agents.mdx +39 -7
  114. package/docs/content/locales/es-ES/external-agents.mdx +39 -7
  115. package/docs/content/locales/fr-FR/external-agents.mdx +39 -7
  116. package/docs/content/locales/hi-IN/external-agents.mdx +38 -7
  117. package/docs/content/locales/ja-JP/external-agents.mdx +38 -7
  118. package/docs/content/locales/ko-KR/external-agents.mdx +38 -7
  119. package/docs/content/locales/pt-BR/external-agents.mdx +39 -7
  120. package/docs/content/locales/zh-CN/external-agents.mdx +37 -7
  121. package/docs/content/locales/zh-TW/external-agents.mdx +37 -7
  122. package/package.json +1 -1
  123. package/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
@@ -21,7 +21,7 @@ import {
21
21
  IconSettings,
22
22
  } from "@tabler/icons-react";
23
23
  import { useQuery } from "@tanstack/react-query";
24
- import { useCallback, useEffect, useMemo, useState } from "react";
24
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
25
25
  import { Link, useNavigate, useSearchParams } from "react-router";
26
26
  import { toast } from "sonner";
27
27
 
@@ -169,6 +169,40 @@ type SessionRecordingDevice = Pick<SessionRecordingSummary, "metadata">;
169
169
  const RANGE_OPTIONS: ReplayRange[] = ["24h", "7d", "30d", "90d", "all"];
170
170
  const SESSION_QUERY_DEBOUNCE_MS = 250;
171
171
 
172
+ /**
173
+ * Local input state for a URL-backed filter, debounced into the URL.
174
+ *
175
+ * `urlValue` only resyncs local state when it changes for a reason other
176
+ * than this hook's own debounced write (back/forward navigation, an agent
177
+ * driven URL change, etc). React Router commits `setSearchParams` inside a
178
+ * transition, so without this guard the echo of our own write can land
179
+ * after a newer keystroke and clobber it.
180
+ */
181
+ export function useDebouncedUrlFilter(
182
+ urlValue: string,
183
+ onCommit: (value: string) => void,
184
+ ): [string, (value: string) => void] {
185
+ const [input, setInput] = useState(urlValue);
186
+ const lastPushedRef = useRef(urlValue);
187
+
188
+ useEffect(() => {
189
+ if (urlValue === lastPushedRef.current) return;
190
+ lastPushedRef.current = urlValue;
191
+ setInput(urlValue);
192
+ }, [urlValue]);
193
+
194
+ useEffect(() => {
195
+ if (input === urlValue) return;
196
+ const timeout = window.setTimeout(() => {
197
+ lastPushedRef.current = input;
198
+ onCommit(input);
199
+ }, SESSION_QUERY_DEBOUNCE_MS);
200
+ return () => window.clearTimeout(timeout);
201
+ }, [input, urlValue, onCommit]);
202
+
203
+ return [input, setInput];
204
+ }
205
+
172
206
  export default function SessionsPage() {
173
207
  const t = useT();
174
208
  const navigate = useNavigate();
@@ -176,13 +210,8 @@ export default function SessionsPage() {
176
210
  const range = readRange(searchParams.get("range"));
177
211
  const app = searchParams.get("app") ?? "";
178
212
  const query = searchParams.get("q") ?? "";
179
- const [queryInput, setQueryInput] = useState(query);
180
213
  const from = useMemo(() => rangeToFrom(range), [range]);
181
214
 
182
- useEffect(() => {
183
- setQueryInput(query);
184
- }, [query]);
185
-
186
215
  const updateFilter = useCallback(
187
216
  (key: string, value: string) => {
188
217
  setSearchParams(
@@ -200,13 +229,17 @@ export default function SessionsPage() {
200
229
  [setSearchParams],
201
230
  );
202
231
 
203
- useEffect(() => {
204
- if (queryInput === query) return;
205
- const timeout = window.setTimeout(() => {
206
- updateFilter("q", queryInput);
207
- }, SESSION_QUERY_DEBOUNCE_MS);
208
- return () => window.clearTimeout(timeout);
209
- }, [query, queryInput, updateFilter]);
232
+ const commitQuery = useCallback(
233
+ (value: string) => updateFilter("q", value),
234
+ [updateFilter],
235
+ );
236
+ const [queryInput, setQueryInput] = useDebouncedUrlFilter(query, commitQuery);
237
+
238
+ const commitApp = useCallback(
239
+ (value: string) => updateFilter("app", value),
240
+ [updateFilter],
241
+ );
242
+ const [appInput, setAppInput] = useDebouncedUrlFilter(app, commitApp);
210
243
 
211
244
  const { data, isLoading, isFetching, refetch, error } = useActionQuery<
212
245
  SessionRecordingSummary[]
@@ -291,10 +324,8 @@ export default function SessionsPage() {
291
324
  {t("sessions.userFilters")}
292
325
  </div>
293
326
  <Input
294
- value={app}
295
- onChange={(event) =>
296
- updateFilter("app", event.target.value)
297
- }
327
+ value={appInput}
328
+ onChange={(event) => setAppInput(event.target.value)}
298
329
  placeholder={t("sessions.appPlaceholder")}
299
330
  className="h-9"
300
331
  />
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-12
4
+ ---
5
+
6
+ Connected external agents can now look up sessions, error issues, and sanitized replay timelines directly, with the app agent remaining the default path.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-12
4
+ ---
5
+
6
+ Session identities stay visible when Demo mode is off
@@ -2,12 +2,12 @@
2
2
  * The deliberately small authenticated MCP surface for Analytics.
3
3
  *
4
4
  * Keep this list read-only and incident-focused. In particular, do not add
5
- * replay-event/blob actions or dashboard/data mutation actions here.
5
+ * raw replay-event/blob actions or dashboard/data mutation actions here.
6
6
  */
7
7
  export const ANALYTICS_CONNECTOR_CATALOG = [
8
8
  "list-session-recordings",
9
9
  "get-session-replay-summary",
10
- "get-session-replay-events",
10
+ "get-session-replay-timeline",
11
11
  "query-agent-native-analytics",
12
12
  "list-error-issues",
13
13
  "get-error-issue",
@@ -18,7 +18,7 @@ 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 { readAppState } from "@agent-native/core/application-state";
21
+ import { appStateGet } from "@agent-native/core/application-state";
22
22
  import { notifyWithDelivery } from "@agent-native/core/notifications";
23
23
  import { recordChange } from "@agent-native/core/server";
24
24
  import { accessFilter } from "@agent-native/core/sharing";
@@ -26,6 +26,7 @@ import {
26
26
  and,
27
27
  desc,
28
28
  eq,
29
+ exists,
29
30
  inArray,
30
31
  isNull,
31
32
  notInArray,
@@ -1073,6 +1074,8 @@ export interface ListErrorIssuesFilters {
1073
1074
  status?: IssueStatus | "all";
1074
1075
  query?: string;
1075
1076
  app?: string;
1077
+ sessionRecordingId?: string;
1078
+ userId?: string;
1076
1079
  sort?: "lastSeen" | "eventCount" | "firstSeen";
1077
1080
  limit?: number;
1078
1081
  }
@@ -1106,10 +1109,14 @@ export function anonymizeErrorReportingEmails<T>(value: T): T {
1106
1109
  return value;
1107
1110
  }
1108
1111
 
1109
- async function isErrorDemoModeEnabled(): Promise<boolean> {
1112
+ async function isErrorDemoModeEnabled(
1113
+ userEmail?: string | null,
1114
+ ): Promise<boolean> {
1110
1115
  if (process.env.DEMO_MODE === "true") return true;
1116
+ if (!userEmail) return false;
1111
1117
  try {
1112
- const state = await readAppState("demo-mode");
1118
+ // Keep demo state tied to the caller whose issues are being read.
1119
+ const state = await appStateGet(userEmail, "demo-mode");
1113
1120
  return state?.enabled === true;
1114
1121
  } catch {
1115
1122
  return false;
@@ -1195,7 +1202,7 @@ export async function listErrorIssues(
1195
1202
  scope: ErrorReadScope,
1196
1203
  filters: ListErrorIssuesFilters = {},
1197
1204
  ): Promise<ErrorIssueSummary[]> {
1198
- const demoModeEnabled = isErrorDemoModeEnabled();
1205
+ const demoModeEnabled = isErrorDemoModeEnabled(scope.userEmail);
1199
1206
  const db = getDb() as any;
1200
1207
  const limit = Math.min(
1201
1208
  MAX_ISSUE_LIMIT,
@@ -1206,6 +1213,41 @@ export async function listErrorIssues(
1206
1213
  conditions.push(eq(schema.errorIssues.status, filters.status));
1207
1214
  }
1208
1215
  if (filters.app) conditions.push(eq(schema.errorIssues.app, filters.app));
1216
+ const sessionRecordingId = filters.sessionRecordingId?.trim();
1217
+ const userId = filters.userId?.trim();
1218
+ if (sessionRecordingId || userId) {
1219
+ const occurrenceConditions: any[] = [
1220
+ eq(schema.errorEvents.issueId, schema.errorIssues.id),
1221
+ // Keep the child occurrence in the same tenant as its parent issue even
1222
+ // when the issue is visible through an org share.
1223
+ eq(schema.errorEvents.ownerEmail, schema.errorIssues.ownerEmail),
1224
+ or(
1225
+ eq(schema.errorEvents.orgId, schema.errorIssues.orgId),
1226
+ and(isNull(schema.errorEvents.orgId), isNull(schema.errorIssues.orgId)),
1227
+ ),
1228
+ ];
1229
+ if (sessionRecordingId) {
1230
+ occurrenceConditions.push(
1231
+ eq(schema.errorEvents.sessionRecordingId, sessionRecordingId),
1232
+ );
1233
+ }
1234
+ if (userId) {
1235
+ occurrenceConditions.push(
1236
+ or(
1237
+ eq(schema.errorEvents.userId, userId),
1238
+ eq(schema.errorEvents.userKey, userId),
1239
+ ),
1240
+ );
1241
+ }
1242
+ conditions.push(
1243
+ exists(
1244
+ db
1245
+ .select({ id: schema.errorEvents.id })
1246
+ .from(schema.errorEvents)
1247
+ .where(and(...occurrenceConditions)),
1248
+ ),
1249
+ );
1250
+ }
1209
1251
  const query = filters.query?.trim();
1210
1252
  if (query) {
1211
1253
  conditions.push(
@@ -1343,7 +1385,7 @@ export async function getErrorIssue(
1343
1385
  issueId: string,
1344
1386
  options: { eventsLimit?: number } = {},
1345
1387
  ): Promise<ErrorIssueDetail> {
1346
- const demoModeEnabled = isErrorDemoModeEnabled();
1388
+ const demoModeEnabled = isErrorDemoModeEnabled(scope.userEmail);
1347
1389
  const db = getDb() as any;
1348
1390
  const [issueRow] = await db
1349
1391
  .select()
@@ -22,6 +22,7 @@ import {
22
22
  } from "../../shared/session-replay-diagnostics.js";
23
23
  import {
24
24
  compactSessionRecordingSummary,
25
+ getSessionReplayEvents,
25
26
  getSessionReplaySummary,
26
27
  getSessionReplayTokenizedEvents,
27
28
  getSessionReplayTokenizedSummary,
@@ -82,20 +83,16 @@ function replayStartedAt(events: AgentReplayEvent[]): number {
82
83
 
83
84
  function pathLabel(href: string): string {
84
85
  try {
85
- const parsed = new URL(href);
86
- return parsed.pathname || parsed.hostname;
86
+ const parsed = new URL(href, "https://placeholder.agent-native.local");
87
+ return boundedDiagnosticText(
88
+ parsed.pathname || parsed.hostname,
89
+ MAX_DIAGNOSTIC_URL_CHARS,
90
+ );
87
91
  } catch {
88
- return href;
92
+ return boundedDiagnosticText(href, MAX_DIAGNOSTIC_URL_CHARS);
89
93
  }
90
94
  }
91
95
 
92
- function markerDetail(event: AgentReplayEvent): string | null {
93
- const payload = event.data?.payload;
94
- if (typeof payload?.message === "string") return payload.message;
95
- if (typeof event.data?.href === "string") return event.data.href;
96
- return null;
97
- }
98
-
99
96
  const TIMELINE_MARKER_CAP = 200;
100
97
  const TIMELINE_ERROR_MARKER_RESERVE = 100;
101
98
  const MAX_DIAGNOSTIC_MESSAGE_CHARS = 1_000;
@@ -528,10 +525,9 @@ function buildReplayTimeline(events: AgentReplayEvent[]) {
528
525
  if (isFailedSessionReplayNetworkStatus(status)) {
529
526
  const method =
530
527
  boundedDiagnosticText(tagged.payload.method, 16) || "GET";
531
- const url = boundedDiagnosticText(
532
- tagged.payload.url,
533
- MAX_DIAGNOSTIC_URL_CHARS,
534
- );
528
+ // External-agent timelines intentionally expose only a path label.
529
+ // Query strings commonly contain emails, tokens, and other secrets.
530
+ const url = pathLabel(String(tagged.payload.url ?? ""));
535
531
  markers.push({
536
532
  timestamp,
537
533
  offsetMs: Math.max(0, timestamp - startedAt),
@@ -553,7 +549,7 @@ function buildReplayTimeline(events: AgentReplayEvent[]) {
553
549
  offsetMs: Math.max(0, timestamp - startedAt),
554
550
  kind: "navigation",
555
551
  label: pathLabel(event.data.href),
556
- detail: event.data.href,
552
+ detail: pathLabel(event.data.href),
557
553
  });
558
554
  } else if (
559
555
  event.type === RRWEB_EVENT_TYPE.IncrementalSnapshot &&
@@ -606,8 +602,14 @@ function buildReplayTimeline(events: AgentReplayEvent[]) {
606
602
  timestamp,
607
603
  offsetMs: Math.max(0, timestamp - startedAt),
608
604
  kind: "custom",
609
- label: String(event.data?.tag ?? "Custom event"),
610
- detail: markerDetail(event),
605
+ label: boundedDiagnosticText(
606
+ String(event.data?.tag ?? "Custom event"),
607
+ MAX_DIAGNOSTIC_MESSAGE_CHARS,
608
+ ),
609
+ // Custom-event payloads are application-defined and can contain input
610
+ // values or secrets. The tag and timestamp are sufficient metadata for
611
+ // this sanitized external-agent timeline.
612
+ detail: null,
611
613
  });
612
614
  }
613
615
  }
@@ -615,6 +617,36 @@ function buildReplayTimeline(events: AgentReplayEvent[]) {
615
617
  return capReplayTimelineMarkers(markers);
616
618
  }
617
619
 
620
+ export async function getSessionReplayTimeline(
621
+ recordingId: string,
622
+ scope: ReplayScope,
623
+ options: { eventLimit?: number } = {},
624
+ ) {
625
+ const eventLimit = Math.min(
626
+ 10_000,
627
+ Math.max(1, Math.floor(options.eventLimit ?? 10_000)),
628
+ );
629
+ const eventsResponse = await getSessionReplayEvents(recordingId, scope, {
630
+ limit: eventLimit,
631
+ });
632
+ const events = eventsResponse.chunks.flatMap((chunk) =>
633
+ chunk.events.filter(
634
+ (event): event is AgentReplayEvent =>
635
+ Boolean(event) && typeof event === "object",
636
+ ),
637
+ );
638
+ const markers = buildReplayTimeline(events);
639
+
640
+ return {
641
+ recording: compactSessionRecordingSummary(eventsResponse.recording),
642
+ markerCount: markers.length,
643
+ markers,
644
+ eventCount: eventsResponse.eventCount,
645
+ truncated: eventsResponse.truncated,
646
+ unavailableChunks: eventsResponse.unavailableChunks,
647
+ };
648
+ }
649
+
618
650
  export function verifySessionReplayAgentAccess(
619
651
  recordingId: string,
620
652
  token: string,
@@ -2,14 +2,18 @@ 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 { readAppState } from "@agent-native/core/application-state";
5
+ import { appStateGet } from "@agent-native/core/application-state";
6
6
  import {
7
7
  deletePrivateBlob,
8
8
  putPrivateBlob,
9
9
  readPrivateBlob,
10
10
  type PrivateBlobHandle,
11
11
  } from "@agent-native/core/private-blob";
12
- import { recordChange, runWithRequestContext } from "@agent-native/core/server";
12
+ import {
13
+ getRequestUserEmail,
14
+ recordChange,
15
+ runWithRequestContext,
16
+ } from "@agent-native/core/server";
13
17
  import {
14
18
  accessFilter,
15
19
  resolveAccess,
@@ -301,10 +305,15 @@ function isBuilderEmail(value: unknown): boolean {
301
305
  return normalizedEmail(value)?.endsWith("@builder.io") === true;
302
306
  }
303
307
 
304
- async function isSessionDemoModeEnabled(): Promise<boolean> {
308
+ async function isSessionDemoModeEnabled(
309
+ userEmail?: string | null,
310
+ ): Promise<boolean> {
305
311
  if (process.env.DEMO_MODE === "true") return true;
312
+ if (!userEmail) return false;
306
313
  try {
307
- const state = await readAppState("demo-mode");
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");
308
317
  return state?.enabled === true;
309
318
  } catch {
310
319
  return false;
@@ -1677,7 +1686,7 @@ export async function listSessionRecordings(
1677
1686
  filters: SessionReplayListFilters = {},
1678
1687
  ): Promise<SessionRecordingSummary[]> {
1679
1688
  const db = getDb() as any;
1680
- const demoMode = await isSessionDemoModeEnabled();
1689
+ const demoMode = await isSessionDemoModeEnabled(scope.userEmail);
1681
1690
  const limit = Math.min(
1682
1691
  MAX_SESSION_RECORDINGS_LIMIT,
1683
1692
  Math.max(1, filters.limit ?? DEFAULT_SESSION_RECORDINGS_LIMIT),
@@ -1759,7 +1768,7 @@ export async function getSessionReplaySummary(
1759
1768
  recordingId: string,
1760
1769
  scope: SessionReplayScope,
1761
1770
  ): Promise<SessionRecordingSummary> {
1762
- const demoMode = await isSessionDemoModeEnabled();
1771
+ const demoMode = await isSessionDemoModeEnabled(scope.userEmail);
1763
1772
  const access = await resolveAccess("session-recording", recordingId, {
1764
1773
  userEmail: scope.userEmail,
1765
1774
  orgId: scope.orgId ?? undefined,
@@ -1778,7 +1787,7 @@ export async function getSessionReplaySummary(
1778
1787
  export async function getSessionReplayTokenizedSummary(
1779
1788
  recordingId: string,
1780
1789
  ): Promise<SessionRecordingSummary> {
1781
- const demoMode = await isSessionDemoModeEnabled();
1790
+ const demoMode = await isSessionDemoModeEnabled(getRequestUserEmail());
1782
1791
  const db = getDb() as any;
1783
1792
  // guard:allow-unscoped -- called only after verifySessionReplayAgentAccess(recordingId, token) verifies a signed, recording-scoped agent_access token.
1784
1793
  const [row] = await db
@@ -258,6 +258,10 @@ export default createAgentChatPlugin({
258
258
  // AGENT_PROD_CODE_EXECUTION=trusted to also enable bash/read/edit/write.
259
259
  codeExecution: { production: "sandboxed" },
260
260
  connectorCatalog: [...ANALYTICS_CONNECTOR_CATALOG],
261
+ externalAgents: {
262
+ authenticatedReads: "auto",
263
+ writes: "ask_app_only",
264
+ },
261
265
  resolveOrgId: async (event) => {
262
266
  const ctx = await getOrgContext(event);
263
267
  return ctx.orgId;
@@ -1249,6 +1249,21 @@ const runAnalyticsMigrations = runMigrations(
1249
1249
  sqlite: `ALTER TABLE monitor_incidents ADD COLUMN IF NOT EXISTS notification_delivered INTEGER NOT NULL DEFAULT 0`,
1250
1250
  },
1251
1251
  },
1252
+ {
1253
+ version: 118,
1254
+ name: "error-events-session-recording-filter-idx",
1255
+ sql: `CREATE INDEX IF NOT EXISTS error_events_session_recording_filter_idx ON error_events (session_recording_id, owner_email, org_id, issue_id)`,
1256
+ },
1257
+ {
1258
+ version: 119,
1259
+ name: "error-events-user-id-filter-idx",
1260
+ sql: `CREATE INDEX IF NOT EXISTS error_events_user_id_filter_idx ON error_events (user_id, owner_email, org_id, issue_id)`,
1261
+ },
1262
+ {
1263
+ version: 120,
1264
+ name: "error-events-user-key-filter-idx",
1265
+ sql: `CREATE INDEX IF NOT EXISTS error_events_user_key_filter_idx ON error_events (user_key, owner_email, org_id, issue_id)`,
1266
+ },
1252
1267
  ],
1253
1268
  { table: "analytics_migrations" },
1254
1269
  );
@@ -181,7 +181,7 @@
181
181
 
182
182
  .forms-ask-chat-panel [data-agent-empty-state="centered"] {
183
183
  justify-content: center;
184
- padding-bottom: clamp(4rem, 12vh, 8rem);
184
+ padding-bottom: clamp(2rem, 6vh, 4rem);
185
185
  }
186
186
 
187
187
  .forms-ask-chat-panel [data-agent-empty-state="centered"] .agent-chat-scroll {
@@ -269,12 +269,28 @@
269
269
  }
270
270
 
271
271
  .forms-ask-chat-panel [data-agent-empty-state="centered"] .forms-chat-intro {
272
- display: grid;
273
- justify-items: center;
274
- gap: 0.75rem;
275
- margin: 0 auto 1rem;
276
- padding-bottom: 1.25rem;
272
+ display: contents;
273
+ }
274
+
275
+ .forms-ask-chat-panel [data-agent-empty-state="centered"] .forms-chat-intro h1,
276
+ .forms-ask-chat-panel [data-agent-empty-state="centered"] .forms-chat-intro p {
277
+ width: 100%;
277
278
  text-align: center;
279
+ order: 1;
280
+ }
281
+
282
+ .forms-ask-chat-panel [data-agent-empty-state="centered"] .forms-chat-intro h1 {
283
+ margin: 0 0 0.5rem;
284
+ }
285
+
286
+ .forms-ask-chat-panel [data-agent-empty-state="centered"] .forms-chat-intro p {
287
+ margin: 0 auto 1rem;
288
+ }
289
+
290
+ .forms-ask-chat-panel
291
+ [data-agent-empty-state="centered"]
292
+ .agent-composer-stack {
293
+ order: 2;
278
294
  }
279
295
 
280
296
  .forms-chat-intro h1 {
@@ -297,10 +313,11 @@
297
313
  flex-wrap: wrap;
298
314
  justify-content: center;
299
315
  gap: 0.5rem;
300
- padding-top: 0.125rem;
316
+ order: 3;
317
+ margin: 0.75rem auto 0;
301
318
  }
302
319
 
303
- .forms-chat-pill-row span {
320
+ .forms-chat-pill-row button {
304
321
  display: inline-flex;
305
322
  align-items: center;
306
323
  gap: 0.375rem;
@@ -310,8 +327,37 @@
310
327
  box-shadow: var(--forms-shadow-control);
311
328
  padding: 0.375rem 0.625rem;
312
329
  color: hsl(var(--muted-foreground));
330
+ cursor: pointer;
313
331
  font-size: 0.75rem;
314
332
  font-weight: 600;
333
+ transition-property: background-color, border-color, color, scale;
334
+ transition-duration: 150ms;
335
+ transition-timing-function: ease-out;
336
+ }
337
+
338
+ .forms-chat-pill-row button:hover {
339
+ border-color: hsl(var(--foreground) / 0.2);
340
+ background: hsl(var(--background));
341
+ color: hsl(var(--foreground));
342
+ }
343
+
344
+ .forms-chat-pill-row button:focus-visible {
345
+ outline: none;
346
+ box-shadow: var(--forms-shadow-focus);
347
+ }
348
+
349
+ .forms-chat-pill-row button:active {
350
+ scale: 0.96;
351
+ }
352
+
353
+ @media (prefers-reduced-motion: reduce) {
354
+ .forms-chat-pill-row button {
355
+ transition: none;
356
+ }
357
+
358
+ .forms-chat-pill-row button:active {
359
+ scale: 1;
360
+ }
315
361
  }
316
362
 
317
363
  @media (min-width: 640px) {
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  AgentChatSurface,
3
3
  markAgentChatHomeHandoff,
4
+ sendToAgentChat,
4
5
  useT,
5
6
  } from "@agent-native/core/client";
6
7
  import { IconChartBar, IconDatabase, IconSettings } from "@tabler/icons-react";
@@ -26,6 +27,28 @@ export function AskPage() {
26
27
  };
27
28
  }, []);
28
29
 
30
+ function prefillSuggestion(message: string) {
31
+ sendToAgentChat({ message, submit: false, chatTarget: "local" });
32
+ }
33
+
34
+ const suggestions = [
35
+ {
36
+ label: t("home.pillForms"),
37
+ prompt: "@forms",
38
+ icon: IconDatabase,
39
+ },
40
+ {
41
+ label: t("home.pillAnalytics"),
42
+ prompt: "analytics",
43
+ icon: IconChartBar,
44
+ },
45
+ {
46
+ label: t("home.pillConfiguration"),
47
+ prompt: "configuration",
48
+ icon: IconSettings,
49
+ },
50
+ ];
51
+
29
52
  return (
30
53
  <div className="flex h-full min-h-0 flex-col bg-background">
31
54
  <AgentChatSurface
@@ -48,19 +71,18 @@ export function AskPage() {
48
71
  <div className="forms-chat-intro">
49
72
  <h1>{t("home.heading")}</h1>
50
73
  <p>{t("home.description")}</p>
51
- <div className="forms-chat-pill-row" aria-hidden="true">
52
- <span className="forms-chat-pill">
53
- <IconDatabase className="size-3.5" />
54
- {t("home.pillForms")}
55
- </span>
56
- <span className="forms-chat-pill">
57
- <IconChartBar className="size-3.5" />
58
- {t("home.pillAnalytics")}
59
- </span>
60
- <span className="forms-chat-pill">
61
- <IconSettings className="size-3.5" />
62
- {t("home.pillConfiguration")}
63
- </span>
74
+ <div className="forms-chat-pill-row">
75
+ {suggestions.map(({ icon: Icon, label, prompt }) => (
76
+ <button
77
+ key={prompt}
78
+ type="button"
79
+ className="forms-chat-pill"
80
+ onClick={() => prefillSuggestion(prompt)}
81
+ >
82
+ <Icon className="size-3.5" />
83
+ {label}
84
+ </button>
85
+ ))}
64
86
  </div>
65
87
  </div>
66
88
  }