@agent-native/core 0.157.6 → 0.157.8

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 (93) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/agent-native.config.ts +5 -0
  3. package/corpus/templates/analytics/netlify.toml +1 -0
  4. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +23 -5
  5. package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -0
  6. package/corpus/templates/assets/netlify.toml +3 -0
  7. package/corpus/templates/brain/netlify.toml +3 -0
  8. package/corpus/templates/calendar/agent-native.config.ts +5 -0
  9. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +4 -1
  10. package/corpus/templates/calendar/netlify.toml +3 -0
  11. package/corpus/templates/chat/agent-native.config.ts +5 -0
  12. package/corpus/templates/chat/netlify.toml +3 -0
  13. package/corpus/templates/clips/actions/lib/meeting-content.ts +28 -0
  14. package/corpus/templates/clips/actions/list-meetings.ts +33 -11
  15. package/corpus/templates/clips/app/i18n/en-US.ts +1 -0
  16. package/corpus/templates/clips/app/lib/meeting-history-query.ts +11 -0
  17. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +47 -21
  18. package/corpus/templates/clips/netlify.toml +4 -0
  19. package/corpus/templates/content/netlify.toml +3 -0
  20. package/corpus/templates/design/app/i18n-data.ts +2 -0
  21. package/corpus/templates/design/netlify.toml +3 -0
  22. package/corpus/templates/dispatch/netlify.toml +3 -0
  23. package/corpus/templates/factory/netlify.toml +3 -0
  24. package/corpus/templates/forms/netlify.toml +3 -0
  25. package/corpus/templates/macros/netlify.toml +3 -0
  26. package/corpus/templates/mail/agent-native.config.ts +5 -0
  27. package/corpus/templates/mail/netlify.toml +3 -0
  28. package/corpus/templates/plan/netlify.toml +3 -0
  29. package/corpus/templates/slides/netlify.toml +3 -0
  30. package/dist/agent/harness/acp-adapter.js +17 -2
  31. package/dist/agent/harness/ai-sdk-adapter.js +14 -3
  32. package/dist/agent/harness/background.js +10 -5
  33. package/dist/agent/harness/hosted.d.ts +20 -0
  34. package/dist/agent/harness/hosted.js +100 -0
  35. package/dist/agent/harness/runner.js +100 -77
  36. package/dist/agent/observational-memory/observer.js +12 -0
  37. package/dist/agent/production-agent.d.ts +3 -0
  38. package/dist/agent/production-agent.js +74 -4
  39. package/dist/agent/thread-data-builder.d.ts +12 -0
  40. package/dist/agent/thread-data-builder.js +23 -0
  41. package/dist/agent/types.d.ts +6 -0
  42. package/dist/client/AgentPanel.d.ts +5 -1
  43. package/dist/client/AgentPanel.js +113 -17
  44. package/dist/client/AgentTaskCard.d.ts +2 -2
  45. package/dist/client/AgentTaskCard.js +8 -22
  46. package/dist/client/AssistantChat.d.ts +8 -0
  47. package/dist/client/AssistantChat.js +106 -59
  48. package/dist/client/MultiTabAssistantChat.js +50 -6
  49. package/dist/client/NewWorkspaceAppFlow.js +7 -1
  50. package/dist/client/agent-chat-adapter.d.ts +6 -0
  51. package/dist/client/agent-chat-adapter.js +36 -8
  52. package/dist/client/app-chat-sidebar.d.ts +5 -0
  53. package/dist/client/app-chat-sidebar.js +53 -9
  54. package/dist/client/chat/composer-draft.d.ts +4 -0
  55. package/dist/client/chat/composer-draft.js +63 -0
  56. package/dist/client/chat-first/apps-rail.js +10 -1
  57. package/dist/client/chat-first/primary-nav.js +10 -5
  58. package/dist/client/frame-protocol.d.ts +1 -0
  59. package/dist/client/hooks/index.d.ts +1 -1
  60. package/dist/client/hooks/index.js +1 -1
  61. package/dist/client/i18n.js +5 -2
  62. package/dist/client/progress/RunsTray.js +129 -16
  63. package/dist/client/settings/SettingsRow.js +1 -1
  64. package/dist/client/use-chat-threads.js +11 -6
  65. package/dist/config.d.ts +11 -0
  66. package/dist/config.js +52 -0
  67. package/dist/extensions/actions.js +22 -1
  68. package/dist/hosted-harness/actions/get-hosted-harness-config.d.ts +2 -0
  69. package/dist/hosted-harness/actions/get-hosted-harness-config.js +13 -0
  70. package/dist/hosted-harness/actions/set-hosted-harness-enabled.d.ts +4 -0
  71. package/dist/hosted-harness/actions/set-hosted-harness-enabled.js +32 -0
  72. package/dist/localization/default-messages.d.ts +4 -1
  73. package/dist/localization/default-messages.js +5 -2
  74. package/dist/notifications/routes.d.ts +1 -1
  75. package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
  76. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  77. package/dist/server/action-discovery.js +12 -0
  78. package/dist/server/agent-chat-plugin.js +24 -4
  79. package/dist/server/agent-teams.d.ts +20 -12
  80. package/dist/server/agent-teams.js +69 -35
  81. package/dist/server/hosted-harness-policy.d.ts +17 -0
  82. package/dist/server/hosted-harness-policy.js +38 -0
  83. package/dist/server/poll.d.ts +4 -4
  84. package/dist/server/poll.js +45 -13
  85. package/dist/server/realtime-token.d.ts +1 -1
  86. package/dist/server/request-context.d.ts +4 -0
  87. package/dist/styles/agent-native.css +39 -0
  88. package/dist/templates/chat/agent-native.config.ts +5 -0
  89. package/dist/templates/chat/netlify.toml +3 -0
  90. package/dist/vite/action-types-plugin.js +8 -0
  91. package/package.json +5 -3
  92. package/src/templates/chat/agent-native.config.ts +5 -0
  93. package/src/templates/chat/netlify.toml +3 -0
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8250
34
+ - template files: 8256
@@ -0,0 +1,5 @@
1
+ import { defineAgentNativeConfig } from "@agent-native/core/config";
2
+
3
+ export default defineAgentNativeConfig({
4
+ harness: true,
5
+ });
@@ -19,6 +19,7 @@ AGENT_PROD_CODE_EXECUTION = "sandboxed"
19
19
  AGENT_CHAT_DURABLE_BACKGROUND = "true"
20
20
 
21
21
  [context.production.environment]
22
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
22
23
  # Analytics owns its schema at release time (`migrate:production` above), so
23
24
  # production request functions skip migrations. Preview functions still run
24
25
  # request-path migrations because previews do not run the production release
@@ -651,9 +651,13 @@ function rewriteSqlFunctionCalls(
651
651
  return match ? result : result + sql.slice(cursor);
652
652
  }
653
653
 
654
- function coerceEventDateComparisonOperands(sql: string): string {
655
- const comparisonRe =
656
- /\b(?:[A-Za-z_][A-Za-z0-9_]*\.)?event_date\s*(?:<=|>=|<>|=|<|>)\s*/gi;
654
+ function coerceDateComparisonOperands(sql: string): string {
655
+ const dateField = "(?:event_date|cohort_date)";
656
+ const qualifiedDateField = `(?:[A-Za-z_][A-Za-z0-9_]*\\.)?${dateField}`;
657
+ const comparisonRe = new RegExp(
658
+ `\\b${qualifiedDateField}\\s*(?:<=|>=|<>|=|<|>)\\s*`,
659
+ "gi",
660
+ );
657
661
  let cursor = 0;
658
662
  let result = "";
659
663
  let match = comparisonRe.exec(sql);
@@ -685,7 +689,10 @@ function coerceEventDateComparisonOperands(sql: string): string {
685
689
  }
686
690
  result += sql.slice(cursor);
687
691
  return result.replace(
688
- /(\b(?:[A-Za-z_][A-Za-z0-9_]*\.)?event_date\s*(?:<=|>=|<>|=|<|>)\s*)'(\d{4}-\d{2}-\d{2})'/gi,
692
+ new RegExp(
693
+ `(\\b${qualifiedDateField}\\s*(?:<=|>=|<>|=|<|>)\\s*)'(\\d{4}-\\d{2}-\\d{2})'`,
694
+ "gi",
695
+ ),
689
696
  "$1DATE '$2'",
690
697
  );
691
698
  }
@@ -813,6 +820,17 @@ function translatePostgresJsonOperators(sql: string): string {
813
820
 
814
821
  function translateFirstPartyAnalyticsBigQuerySql(sql: string): string {
815
822
  let translated = translatePostgresJsonOperators(sql);
823
+ translated = rewriteSqlFunctionCalls(translated, "coalesce", (args) => {
824
+ const uniqueArgs: string[] = [];
825
+ const seen = new Set<string>();
826
+ for (const arg of args) {
827
+ const normalized = arg.trim();
828
+ if (seen.has(normalized)) continue;
829
+ seen.add(normalized);
830
+ uniqueArgs.push(arg);
831
+ }
832
+ return `COALESCE(${uniqueArgs.join(", ")})`;
833
+ });
816
834
  translated = translated.replace(
817
835
  /\bINTERVAL\s*'(\d+)\s+(day|days|week|weeks|month|months)'/gi,
818
836
  (_match, amount: string, unit: string) =>
@@ -975,7 +993,7 @@ export function renderFirstPartyAnalyticsBigQuerySql(
975
993
  translateFirstPartyAnalyticsBigQuerySql(normalizedScopeSql);
976
994
  const bound = bindSqlArguments(translated, args);
977
995
  return addPartitionPrunedEventDeduplication(
978
- coerceEventDateComparisonOperands(qualifyQuerySources(bound, table)),
996
+ coerceDateComparisonOperands(qualifyQuerySources(bound, table)),
979
997
  table,
980
998
  );
981
999
  }
@@ -61,6 +61,10 @@ function hasSuccessfulDashboardSave(
61
61
  "update-dashboard",
62
62
  "mutate-dashboard",
63
63
  "compose-dashboard",
64
+ // An extension edit is the whole job when the dashboard panel IS the
65
+ // extension. Leaving it out meant a turn that saved exactly what the user
66
+ // asked for still had to prove itself with a data query.
67
+ "update-extension",
64
68
  ]);
65
69
  return (toolResults ?? []).some((result) => {
66
70
  if (result.isError) return false;
@@ -11,6 +11,9 @@ NITRO_PRESET = "netlify"
11
11
  NPM_CONFIG_PRODUCTION = "false"
12
12
  AGENT_CHAT_DURABLE_BACKGROUND = "true"
13
13
 
14
+ [context.production.environment]
15
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
16
+
14
17
  # Emit the Netlify background function used by Assets' durable agent-chat opt-in.
15
18
  # The normal server timeout still protects non-agent image/video HTTP handlers.
16
19
  [functions."*"]
@@ -10,6 +10,9 @@ GTM_CONTAINER_ID = "GTM-N3WSTXZ"
10
10
  NITRO_PRESET = "netlify"
11
11
  NPM_CONFIG_PRODUCTION = "false"
12
12
 
13
+ [context.production.environment]
14
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
15
+
13
16
  # A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
14
17
  # The framework does not impose a default agent-run timeout; if a template sets
15
18
  # AGENT_RUN_SOFT_TIMEOUT_MS, keep it below this function timeout so the hidden
@@ -0,0 +1,5 @@
1
+ import { defineAgentNativeConfig } from "@agent-native/core/config";
2
+
3
+ export default defineAgentNativeConfig({
4
+ harness: true,
5
+ });
@@ -385,7 +385,10 @@ export function AppLayout({ children }: AppLayoutProps) {
385
385
  open={shortcutsHelpOpen}
386
386
  onClose={() => setShortcutsHelpOpen(false)}
387
387
  />
388
- <div className="agent-layout-shell flex h-screen overflow-hidden bg-background">
388
+ <div
389
+ className="agent-layout-shell flex h-screen overflow-hidden bg-background"
390
+ data-agent-native-shell-variant="custom"
391
+ >
389
392
  <Sidebar
390
393
  open={sidebarOpen}
391
394
  onClose={() => setSidebarOpen(false)}
@@ -10,6 +10,9 @@ GTM_CONTAINER_ID = "GTM-N3WSTXZ"
10
10
  NITRO_PRESET = "netlify"
11
11
  NPM_CONFIG_PRODUCTION = "false"
12
12
 
13
+ [context.production.environment]
14
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
15
+
13
16
  # A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
14
17
  # The framework does not impose a default agent-run timeout; if a template sets
15
18
  # AGENT_RUN_SOFT_TIMEOUT_MS, keep it below this function timeout so the hidden
@@ -0,0 +1,5 @@
1
+ import { defineAgentNativeConfig } from "@agent-native/core/config";
2
+
3
+ export default defineAgentNativeConfig({
4
+ harness: true,
5
+ });
@@ -9,3 +9,6 @@ GA_MEASUREMENT_ID = "G-ESF7FYXGN9"
9
9
  GTM_CONTAINER_ID = "GTM-N3WSTXZ"
10
10
  NITRO_PRESET = "netlify"
11
11
  NPM_CONFIG_PRODUCTION = "false"
12
+
13
+ [context.production.environment]
14
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Whether a persisted meeting contains something a user can reopen.
3
+ *
4
+ * Calendar events are materialized before they are recorded, so the meetings
5
+ * table also contains empty calendar rows. History should keep notes and
6
+ * completed meeting rows even when no recording was linked.
7
+ */
8
+ export interface MeetingContentFields {
9
+ recordingId?: string | null;
10
+ actualStart?: string | null;
11
+ actualEnd?: string | null;
12
+ summaryMd?: string | null;
13
+ userNotesMd?: string | null;
14
+ bulletsJson?: string | null;
15
+ actionItemsJson?: string | null;
16
+ }
17
+
18
+ export function meetingRowHasContent(row: MeetingContentFields): boolean {
19
+ return Boolean(
20
+ row.recordingId ||
21
+ row.actualStart ||
22
+ row.actualEnd ||
23
+ (row.summaryMd ?? "").trim() ||
24
+ (row.userNotesMd ?? "").trim() ||
25
+ (row.bulletsJson ?? "[]") !== "[]" ||
26
+ (row.actionItemsJson ?? "[]") !== "[]",
27
+ );
28
+ }
@@ -26,6 +26,7 @@ import {
26
26
  lt,
27
27
  gte,
28
28
  lte,
29
+ ne,
29
30
  or,
30
31
  sql,
31
32
  } from "drizzle-orm";
@@ -48,9 +49,23 @@ import {
48
49
  } from "../server/lib/calendar-event-meetings.js";
49
50
  import { listEvents } from "../server/lib/google-calendar-client.js";
50
51
  import { booleanParam } from "./lib/cli-params.js";
52
+ import { meetingRowHasContent } from "./lib/meeting-content.js";
51
53
 
52
54
  const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
53
55
 
56
+ /** SQL mirror of `meetingRowHasContent`. Keep history filtering consistent. */
57
+ function meetingHasContentFilter() {
58
+ return or(
59
+ isNotNull(schema.meetings.recordingId),
60
+ isNotNull(schema.meetings.actualStart),
61
+ isNotNull(schema.meetings.actualEnd),
62
+ sql`trim(${schema.meetings.summaryMd}) <> ''`,
63
+ sql`trim(${schema.meetings.userNotesMd}) <> ''`,
64
+ ne(schema.meetings.bulletsJson, "[]"),
65
+ ne(schema.meetings.actionItemsJson, "[]"),
66
+ )!;
67
+ }
68
+
54
69
  export default defineAction({
55
70
  description:
56
71
  "List meetings (Granola-style) the current user has access to. Connected calendars are read live; use view='upcoming' / 'past' / 'all' / 'trash' to filter by lifecycle.",
@@ -64,6 +79,11 @@ export default defineAction({
64
79
  recordedOnly: booleanParam
65
80
  .default(false)
66
81
  .describe("Only return persisted meetings that have a linked recording."),
82
+ hasContent: booleanParam
83
+ .default(false)
84
+ .describe(
85
+ "Only return persisted meetings that contain a recording, completed capture, notes, a summary, bullets, or action items.",
86
+ ),
67
87
  includeLiveCalendar: booleanParam
68
88
  .default(true)
69
89
  .describe(
@@ -109,8 +129,9 @@ export default defineAction({
109
129
  // slice(offset, offset + limit) at the end. To make that final slice
110
130
  // correct we must fetch enough rows from BOTH sources to cover the whole
111
131
  // offset + limit window before merging — fetching only `limit` would drop
112
- // events once offset > 0 or the calendar is large. Keep the hard caps
113
- // (500 persisted, 250 live) so a huge calendar can't blow up the request.
132
+ // events once offset > 0 or the calendar is large. Keep the live-event cap
133
+ // so a huge calendar can't blow up the request. Persisted history needs
134
+ // the full sentinel window so pages remain available after 500 rows.
114
135
  const windowCount = args.offset + args.limit;
115
136
  const upcomingWindowMaxIso = args.upcomingWithinMin
116
137
  ? new Date(
@@ -160,6 +181,9 @@ export default defineAction({
160
181
  if (args.recordedOnly) {
161
182
  whereClauses.push(isNotNull(schema.meetings.recordingId));
162
183
  }
184
+ if (args.hasContent) {
185
+ whereClauses.push(meetingHasContentFilter());
186
+ }
163
187
 
164
188
  const orderBy =
165
189
  args.view === "upcoming"
@@ -175,7 +199,7 @@ export default defineAction({
175
199
  .from(schema.meetings)
176
200
  .where(and(...whereClauses))
177
201
  .orderBy(...orderBy)
178
- .limit(Math.min(500, windowCount))
202
+ .limit(windowCount + 1)
179
203
  .offset(0);
180
204
 
181
205
  // Add a derived `summaryPreview` (first ~100 chars of summaryMd) so the
@@ -383,13 +407,7 @@ export default defineAction({
383
407
  if (
384
408
  liveEventEmitted &&
385
409
  meeting.source === "calendar" &&
386
- !meeting.recordingId &&
387
- !meeting.actualStart &&
388
- !meeting.actualEnd &&
389
- !(meeting.summaryMd ?? "").trim() &&
390
- !(meeting.userNotesMd ?? "").trim() &&
391
- (meeting.bulletsJson ?? "[]") === "[]" &&
392
- (meeting.actionItemsJson ?? "[]") === "[]"
410
+ !meetingRowHasContent(meeting)
393
411
  ) {
394
412
  continue;
395
413
  }
@@ -407,6 +425,10 @@ export default defineAction({
407
425
 
408
426
  const meetings = combined.slice(args.offset, args.offset + args.limit);
409
427
 
410
- return { meetings, calendarErrors };
428
+ return {
429
+ meetings,
430
+ calendarErrors,
431
+ hasMore: combined.length > args.offset + args.limit,
432
+ };
411
433
  },
412
434
  });
@@ -1575,6 +1575,7 @@ All notable user-facing changes to Clips are documented here. Open it any time f
1575
1575
  transcriptPending: "Transcript pending",
1576
1576
  notesPending: "Notes pending",
1577
1577
  pastRecordings: "Past recordings",
1578
+ loadOlder: "Load older",
1578
1579
  calendarNeedsReconnect:
1579
1580
  "Google Calendar needs to be reconnected to keep showing your upcoming meetings.",
1580
1581
  connectGoogleCalendar: "Connect Google Calendar",
@@ -0,0 +1,11 @@
1
+ export const MEETING_HISTORY_PAGE_SIZE = 50;
2
+
3
+ export function buildMeetingHistoryQuery(offset: number) {
4
+ return {
5
+ view: "past" as const,
6
+ hasContent: true,
7
+ includeLiveCalendar: false,
8
+ limit: MEETING_HISTORY_PAGE_SIZE,
9
+ offset,
10
+ };
11
+ }
@@ -1,5 +1,5 @@
1
1
  import { agentNativePath } from "@agent-native/core/client/api-path";
2
- import { useActionQuery } from "@agent-native/core/client/hooks";
2
+ import { callAction, useActionQuery } from "@agent-native/core/client/hooks";
3
3
  import { useT } from "@agent-native/core/client/i18n";
4
4
  import {
5
5
  IconAlertTriangle,
@@ -17,7 +17,7 @@ import {
17
17
  IconSettings,
18
18
  IconX,
19
19
  } from "@tabler/icons-react";
20
- import { useQueryClient } from "@tanstack/react-query";
20
+ import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
21
21
  import { useCallback, useEffect, useMemo, useState } from "react";
22
22
  import { NavLink, useSearchParams } from "react-router";
23
23
  import { toast } from "sonner";
@@ -50,6 +50,10 @@ import {
50
50
  } from "@/components/ui/dropdown-menu";
51
51
  import { Input } from "@/components/ui/input";
52
52
  import enMessages from "@/i18n/en-US";
53
+ import {
54
+ buildMeetingHistoryQuery,
55
+ MEETING_HISTORY_PAGE_SIZE,
56
+ } from "@/lib/meeting-history-query";
53
57
 
54
58
  export function meta() {
55
59
  return [{ title: enMessages.meetingsRoute.pageTitle }];
@@ -88,6 +92,7 @@ interface CalendarFetchError {
88
92
  interface ListMeetingsResponse {
89
93
  meetings?: Meeting[];
90
94
  calendarErrors?: CalendarFetchError[];
95
+ hasMore?: boolean;
91
96
  }
92
97
 
93
98
  interface CalendarAccount {
@@ -849,13 +854,20 @@ export default function MeetingsIndexRoute() {
849
854
  {},
850
855
  { retry: false },
851
856
  );
852
- const meetingsQuery = useActionQuery<
853
- { meetings: Meeting[] } | Meeting[] | undefined
854
- >(
855
- "list-meetings",
856
- { view: "past", recordedOnly: true, includeLiveCalendar: false },
857
- { retry: false },
858
- );
857
+ const history = useInfiniteQuery({
858
+ queryKey: ["action", "list-meetings", "history"],
859
+ initialPageParam: 0,
860
+ queryFn: ({ pageParam, signal }) =>
861
+ callAction("list-meetings", buildMeetingHistoryQuery(pageParam), {
862
+ method: "GET",
863
+ signal,
864
+ }) as Promise<ListMeetingsResponse>,
865
+ getNextPageParam: (lastPage, allPages) =>
866
+ lastPage?.hasMore
867
+ ? allPages.length * MEETING_HISTORY_PAGE_SIZE
868
+ : undefined,
869
+ retry: false,
870
+ });
859
871
  // Upcoming calendar events, read live from connected calendars. This is the
860
872
  // Granola-style surface that lets the user record/join a meeting that's about
861
873
  // to start. Poll every 30s so a freshly-added calendar event (or one moving
@@ -886,7 +898,7 @@ export default function MeetingsIndexRoute() {
886
898
  queryClient.setQueriesData<any>(
887
899
  { queryKey: ["action", "list-meetings"] },
888
900
  (prev: any) => {
889
- if (!prev || Array.isArray(prev)) return prev;
901
+ if (!prev || !Array.isArray(prev.meetings)) return prev;
890
902
  return { ...prev, calendarErrors: [] };
891
903
  },
892
904
  );
@@ -905,7 +917,7 @@ export default function MeetingsIndexRoute() {
905
917
  if (connected) break;
906
918
  await new Promise((resolve) => window.setTimeout(resolve, 500));
907
919
  }
908
- await meetingsQuery.refetch();
920
+ await history.refetch();
909
921
  if (connected) toast.success(t("meetingsRoute.calendarConnected"));
910
922
  } catch (err) {
911
923
  toast.error(
@@ -919,14 +931,12 @@ export default function MeetingsIndexRoute() {
919
931
  queryKey: ["action", "list-meetings"],
920
932
  });
921
933
  }
922
- }, [accounts, clearCalendarConnectionWarnings, meetingsQuery, queryClient]);
934
+ }, [accounts, clearCalendarConnectionWarnings, history, queryClient]);
923
935
 
924
- const meetings: Meeting[] = useMemo(() => {
925
- const data = meetingsQuery.data;
926
- if (!data) return [];
927
- if (Array.isArray(data)) return data;
928
- return data.meetings ?? [];
929
- }, [meetingsQuery.data]);
936
+ const meetings: Meeting[] = useMemo(
937
+ () => (history.data?.pages ?? []).flatMap((page) => page?.meetings ?? []),
938
+ [history.data],
939
+ );
930
940
 
931
941
  const upcomingMeetings: Meeting[] = useMemo(() => {
932
942
  const data = upcomingQuery.data;
@@ -961,11 +971,11 @@ export default function MeetingsIndexRoute() {
961
971
  );
962
972
  }, [handleCalendarConnected]);
963
973
 
964
- const isLoading = accounts.isLoading || meetingsQuery.isLoading;
974
+ const isLoading = accounts.isLoading || history.isLoading;
965
975
 
966
976
  const calendarLoadError = accounts.isError
967
977
  ? "Couldn't check your calendar connection. Try again in a moment."
968
- : meetingsQuery.isError
978
+ : history.isError
969
979
  ? "Couldn't load meetings. Try again in a moment."
970
980
  : null;
971
981
 
@@ -1111,10 +1121,26 @@ export default function MeetingsIndexRoute() {
1111
1121
  <UpcomingMeetingsList meetings={filteredUpcoming} />
1112
1122
  )}
1113
1123
  <RecordedMeetingsList meetings={recordedMeetings} />
1124
+ {history.hasNextPage && (
1125
+ <div className="flex justify-center">
1126
+ <Button
1127
+ variant="outline"
1128
+ size="sm"
1129
+ onClick={() => history.fetchNextPage()}
1130
+ disabled={history.isFetchingNextPage}
1131
+ className="gap-1.5 cursor-pointer"
1132
+ >
1133
+ {history.isFetchingNextPage ? (
1134
+ <IconLoader2 className="h-3.5 w-3.5 animate-spin" />
1135
+ ) : null}
1136
+ {t("meetingsRoute.loadOlder", { defaultValue: "Load older" })}
1137
+ </Button>
1138
+ </div>
1139
+ )}
1114
1140
  </div>
1115
1141
  )}
1116
1142
 
1117
- {(meetingsQuery.isFetching || upcomingQuery.isFetching) && !isLoading && (
1143
+ {(history.isFetching || upcomingQuery.isFetching) && !isLoading && (
1118
1144
  <div className="flex items-center justify-center mt-6 text-xs text-muted-foreground gap-1.5">
1119
1145
  <IconLoader2 className="h-3.5 w-3.5 animate-spin" />
1120
1146
  {t("meetingsRoute.refreshing")}
@@ -13,6 +13,10 @@ AUTO_CREATE_DEFAULT_ORG = "1"
13
13
  # Emit the shared 15-minute Netlify background function used by agent chat and
14
14
  # Clips post-recording transcription/seekability workers.
15
15
  AGENT_CHAT_DURABLE_BACKGROUND = "true"
16
+
17
+ [context.production.environment]
18
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
19
+
16
20
  # Netlify Functions run on linux/x64 (AWS Lambda). This tells the deploy build
17
21
  # to bundle the matching ffmpeg-static binary into the server bundle so frame
18
22
  # extraction, WebM seekability, filmstrips, and audio-only transcription work
@@ -14,6 +14,9 @@ AGENT_RUN_SOFT_TIMEOUT_MS = "55000"
14
14
  # use the async worker so those calls are not cut off by the foreground wall.
15
15
  AGENT_CHAT_DURABLE_BACKGROUND = "true"
16
16
 
17
+ [context.production.environment]
18
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
19
+
17
20
  # A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
18
21
  # The framework does not impose a default agent-run timeout; if a template sets
19
22
  # AGENT_RUN_SOFT_TIMEOUT_MS, keep it below this function timeout so the hidden
@@ -1084,6 +1084,8 @@ const enUS = {
1084
1084
  "PNG capture isn't available in read-only previews",
1085
1085
  pngSaveError: "Could not save PNG",
1086
1086
  pngExportError: "Could not export PNG",
1087
+ pdfExportError: "Could not export PDF",
1088
+ pdfDownloaded: "PDF downloaded",
1087
1089
  pdfAllScreensDownloaded: "PDF downloaded (all screens)",
1088
1090
  openScreenSvg: "Open a screen before exporting SVG",
1089
1091
  svgDownloaded: "SVG downloaded",
@@ -11,6 +11,9 @@ NITRO_PRESET = "netlify"
11
11
  NPM_CONFIG_PRODUCTION = "false"
12
12
  AGENT_CHAT_DURABLE_BACKGROUND = "true"
13
13
 
14
+ [context.production.environment]
15
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
16
+
14
17
  # Emit the Netlify background function used by Design's durable agent-chat opt-in.
15
18
  # The normal server timeout still protects non-background HTTP handlers.
16
19
  [functions."*"]
@@ -13,6 +13,9 @@ NPM_CONFIG_PRODUCTION = "false"
13
13
  # function, including workspace-resource updates delegated by sibling apps.
14
14
  AGENT_CHAT_DURABLE_BACKGROUND = "true"
15
15
 
16
+ [context.production.environment]
17
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
18
+
16
19
  # A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
17
20
  # The framework does not impose a default agent-run timeout; if a template sets
18
21
  # AGENT_RUN_SOFT_TIMEOUT_MS, keep it below this function timeout so the hidden
@@ -8,3 +8,6 @@ GA_MEASUREMENT_ID = "G-ESF7FYXGN9"
8
8
  GTM_CONTAINER_ID = "GTM-N3WSTXZ"
9
9
  NITRO_PRESET = "netlify"
10
10
  NPM_CONFIG_PRODUCTION = "false"
11
+
12
+ [context.production.environment]
13
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
@@ -10,6 +10,9 @@ GTM_CONTAINER_ID = "GTM-N3WSTXZ"
10
10
  NITRO_PRESET = "netlify"
11
11
  NPM_CONFIG_PRODUCTION = "false"
12
12
 
13
+ [context.production.environment]
14
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
15
+
13
16
  # A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
14
17
  # The framework does not impose a default agent-run timeout; if a template sets
15
18
  # AGENT_RUN_SOFT_TIMEOUT_MS, keep it below this function timeout so the hidden
@@ -9,3 +9,6 @@ GA_MEASUREMENT_ID = "G-ESF7FYXGN9"
9
9
  GTM_CONTAINER_ID = "GTM-N3WSTXZ"
10
10
  NITRO_PRESET = "netlify"
11
11
  NPM_CONFIG_PRODUCTION = "false"
12
+
13
+ [context.production.environment]
14
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
@@ -0,0 +1,5 @@
1
+ import { defineAgentNativeConfig } from "@agent-native/core/config";
2
+
3
+ export default defineAgentNativeConfig({
4
+ harness: true,
5
+ });
@@ -10,6 +10,9 @@ GTM_CONTAINER_ID = "GTM-N3WSTXZ"
10
10
  NITRO_PRESET = "netlify"
11
11
  NPM_CONFIG_PRODUCTION = "false"
12
12
 
13
+ [context.production.environment]
14
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
15
+
13
16
  # A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
14
17
  # The framework does not impose a default agent-run timeout; if a template sets
15
18
  # AGENT_RUN_SOFT_TIMEOUT_MS, keep it below this function timeout so the hidden
@@ -11,6 +11,9 @@ NITRO_PRESET = "netlify"
11
11
  NPM_CONFIG_PRODUCTION = "false"
12
12
  AGENT_CHAT_DURABLE_BACKGROUND = "true"
13
13
 
14
+ [context.production.environment]
15
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
16
+
14
17
  # A2A/MCP calls may include plan generation, artifact serialization, and
15
18
  # feedback polling. Keep this aligned with other agent-heavy hosted templates.
16
19
  # Agent chat runs are dispatched to the emitted Netlify background function;
@@ -14,6 +14,9 @@ NITRO_PRESET = "netlify"
14
14
  NPM_CONFIG_PRODUCTION = "false"
15
15
  AGENT_CHAT_DURABLE_BACKGROUND = "true"
16
16
 
17
+ [context.production.environment]
18
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
19
+
17
20
  # Function wall for the non-agent long-request paths (deck import/export,
18
21
  # image generation). Agent chat opts into the separate Netlify background
19
22
  # function so hosted creative turns can use the longer background budget.
@@ -167,8 +167,12 @@ class AcpHarnessSession {
167
167
  this.acpSessionId = resume.sessionId;
168
168
  return;
169
169
  }
170
- catch {
171
- // Fall through to a fresh session if the agent could not load it.
170
+ catch (error) {
171
+ if (!isExplicitlyMissingAcpSession(error))
172
+ throw error;
173
+ // A provider can legitimately evict an old session. Only that
174
+ // explicit condition may fall back to a fresh session; transport or
175
+ // permission failures must remain visible to the caller.
172
176
  }
173
177
  }
174
178
  const created = await this.connection.newSession({
@@ -321,6 +325,9 @@ class AcpHarnessSession {
321
325
  return { content: sliceTextFile(raw, params.line, params.limit) };
322
326
  }
323
327
  async handleWriteTextFile(params) {
328
+ if (this.permissionMode === "allow-reads") {
329
+ throw new Error("[acp-harness] File writes are disabled in allow-reads mode.");
330
+ }
324
331
  const abs = resolveAcpWorkspacePath(this.cwd, params.path);
325
332
  const existed = await fileExists(abs);
326
333
  await fs.mkdir(path.dirname(abs), { recursive: true });
@@ -604,6 +611,14 @@ function acpErrorMessage(error) {
604
611
  }
605
612
  return typeof error === "string" ? error : "ACP request failed";
606
613
  }
614
+ function isExplicitlyMissingAcpSession(error) {
615
+ const message = error instanceof Error
616
+ ? error.message
617
+ : typeof error === "string"
618
+ ? error
619
+ : "";
620
+ return /(?:session|conversation).*(?:not found|does not exist|unknown|unsupported)|(?:unknown|unsupported).*(?:session|conversation)/i.test(message);
621
+ }
607
622
  /** Minimal async queue bridging ACP's callback updates to an async iterable. */
608
623
  class AsyncEventQueue {
609
624
  values = [];
@@ -199,8 +199,11 @@ async function createNativeSession(agent, options) {
199
199
  try {
200
200
  return await agent.createSession({ resumeState: options.resumeState });
201
201
  }
202
- catch {
203
- return agent.createSession();
202
+ catch (error) {
203
+ if (isExplicitlyMissingHarnessSession(error)) {
204
+ return agent.createSession();
205
+ }
206
+ throw error;
204
207
  }
205
208
  }
206
209
  if (typeof agent.createSession !== "function") {
@@ -237,7 +240,7 @@ class AiSdkHarnessSession {
237
240
  }
238
241
  async *continueTurn(input = {}) {
239
242
  if (typeof this.agent.continueStream !== "function") {
240
- return;
243
+ throw new Error("[agent-harness] This runtime cannot continue a harness turn.");
241
244
  }
242
245
  const result = await this.agent.continueStream({
243
246
  session: this.nativeSession,
@@ -353,3 +356,11 @@ function normalizeFileOperation(value) {
353
356
  ? value
354
357
  : "unknown";
355
358
  }
359
+ function isExplicitlyMissingHarnessSession(error) {
360
+ const message = error instanceof Error
361
+ ? error.message
362
+ : typeof error === "string"
363
+ ? error
364
+ : "";
365
+ return /(?:session|conversation).*(?:not found|does not exist|unknown|unsupported)|(?:unknown|unsupported).*(?:session|conversation)/i.test(message);
366
+ }