@agent-native/core 0.196.0-nightly-20260928172902 → 0.196.0

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 (86) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/CHANGELOG.md +17 -0
  3. package/corpus/templates/assets/CHANGELOG.md +18 -0
  4. package/corpus/templates/brain/CHANGELOG.md +6 -0
  5. package/corpus/templates/calendar/CHANGELOG.md +4 -0
  6. package/corpus/templates/clips/CHANGELOG.md +19 -0
  7. package/corpus/templates/clips/actions/create-recording-agent-link.ts +8 -2
  8. package/corpus/templates/clips/actions/update-recording.ts +5 -1
  9. package/corpus/templates/clips/app/routes/share.$shareId.tsx +10 -3
  10. package/corpus/templates/clips/changelog/2026-09-28-agent-links-stay-valid-when-clip-details-change.md +6 -0
  11. package/corpus/templates/clips/changelog/2026-09-28-password-changes-immediately-invalidate-old-recording-links.md +6 -0
  12. package/corpus/templates/clips/changelog/2026-09-28-password-protected-recording-links-stop-working-when-the-pas.md +6 -0
  13. package/corpus/templates/clips/server/db/schema.ts +3 -0
  14. package/corpus/templates/clips/server/lib/player-video-url.ts +9 -1
  15. package/corpus/templates/clips/server/lib/public-agent-context.ts +12 -4
  16. package/corpus/templates/clips/server/lib/share-password.ts +25 -0
  17. package/corpus/templates/clips/server/plugins/db.ts +36 -0
  18. package/corpus/templates/clips/server/routes/[...page].get.ts +8 -1
  19. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +33 -6
  20. package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +34 -6
  21. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +26 -5
  22. package/corpus/templates/content/CHANGELOG.md +1 -0
  23. package/corpus/templates/crm/CHANGELOG.md +12 -0
  24. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +265 -3
  25. package/corpus/templates/design/CHANGELOG.md +21 -0
  26. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +463 -6
  27. package/corpus/templates/design/app/pages/Index.tsx +1 -1
  28. package/corpus/templates/design/changelog/2026-09-27-auto-focused-app-fields-no-longer-interrupt-canvas-shortcuts.md +6 -0
  29. package/corpus/templates/design/changelog/2026-09-28-model-picker-loads-available-models.md +6 -0
  30. package/corpus/templates/design/changelog/2026-09-28-template-menus-stay-available-on-hover.md +6 -0
  31. package/corpus/templates/dispatch/CHANGELOG.md +18 -0
  32. package/corpus/templates/forms/CHANGELOG.md +6 -0
  33. package/corpus/templates/mail/.env.example +1 -1
  34. package/corpus/templates/mail/CHANGELOG.md +33 -0
  35. package/corpus/templates/mail/changelog/2026-09-28-fixed-inbox-loading-stalls-and-showed-progress-during-google-2.md +6 -0
  36. package/corpus/templates/mail/changelog/2026-09-28-fixed-inbox-loading-stalls-and-showed-progress-during-google.md +6 -0
  37. package/corpus/templates/mail/server/db/schema.ts +7 -0
  38. package/corpus/templates/mail/server/lib/ai-filter-backfill.ts +4 -0
  39. package/corpus/templates/mail/server/lib/jobs.ts +5 -2
  40. package/corpus/templates/mail/server/plugins/db.ts +16 -0
  41. package/corpus/templates/mail/server/plugins/mail-jobs.ts +355 -44
  42. package/corpus/templates/slides/CHANGELOG.md +28 -0
  43. package/corpus/templates/slides/app/pages/Index.tsx +1 -1
  44. package/corpus/templates/slides/changelog/2026-09-28-model-picker-loads-available-models.md +6 -0
  45. package/corpus/templates/slides/changelog/2026-09-28-template-menus-stay-available-on-hover.md +6 -0
  46. package/dist/automation/index.d.ts +1 -1
  47. package/dist/client/auth/AuthPage.js +3 -2
  48. package/dist/client/org/OrgSwitcher.js +31 -19
  49. package/dist/client/settings/UsageSection.js +3 -3
  50. package/dist/client/use-action.js +1 -1
  51. package/dist/collab/awareness.d.ts +2 -2
  52. package/dist/collab/routes.d.ts +1 -1
  53. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  54. package/dist/jobs/background-automation-runner.d.ts +2 -0
  55. package/dist/jobs/background-automation-runner.js +20 -1
  56. package/dist/jobs/sweep-hooks.d.ts +6 -2
  57. package/dist/jobs/sweep-hooks.js +17 -9
  58. package/dist/localization/core-messages/ar-SA.js +2 -2
  59. package/dist/localization/core-messages/de-DE.js +2 -2
  60. package/dist/localization/core-messages/en-US.d.ts +2 -2
  61. package/dist/localization/core-messages/en-US.js +2 -2
  62. package/dist/localization/core-messages/es-ES.js +2 -2
  63. package/dist/localization/core-messages/fr-FR.js +2 -2
  64. package/dist/localization/core-messages/hi-IN.js +2 -2
  65. package/dist/localization/core-messages/ja-JP.js +2 -2
  66. package/dist/localization/core-messages/ko-KR.js +2 -2
  67. package/dist/localization/core-messages/pt-BR.js +2 -2
  68. package/dist/localization/core-messages/zh-CN.js +2 -2
  69. package/dist/localization/core-messages/zh-TW.js +2 -2
  70. package/dist/localization/core-messages.d.ts +2 -2
  71. package/dist/notifications/routes.d.ts +5 -5
  72. package/dist/observability/routes.d.ts +6 -6
  73. package/dist/progress/routes.d.ts +1 -1
  74. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  75. package/dist/provider-api/actions/provider-api.d.ts +7 -7
  76. package/dist/resources/handlers.d.ts +1 -1
  77. package/dist/secrets/routes.d.ts +4 -4
  78. package/dist/server/agent-chat-plugin.js +6 -2
  79. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  80. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  81. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  82. package/dist/server/index.d.ts +1 -1
  83. package/dist/triggers/dispatcher.js +167 -36
  84. package/dist/triggers/event-queue.d.ts +18 -1
  85. package/dist/triggers/event-queue.js +143 -10
  86. package/package.json +4 -4
@@ -2271,6 +2271,7 @@ async function dispatchMailAiFilterBackfillContinuation(
2271
2271
  export async function processMailAiFilterBackfills(
2272
2272
  ownerEmail?: string,
2273
2273
  runId?: string,
2274
+ deadlineAt = Number.POSITIVE_INFINITY,
2274
2275
  ): Promise<void> {
2275
2276
  const now = Date.now();
2276
2277
  const conditions = [
@@ -2297,6 +2298,9 @@ export async function processMailAiFilterBackfills(
2297
2298
  let processed = 0;
2298
2299
  for (const row of rows) {
2299
2300
  if (processed >= MAX_RUNS_PER_TICK) break;
2301
+ if (Date.now() >= deadlineAt) {
2302
+ throw new Error("Mail AI-filter backfill sweep deadline reached.");
2303
+ }
2300
2304
  const claim = await claimRun(row);
2301
2305
  if (!claim) continue;
2302
2306
  processed += 1;
@@ -7,7 +7,7 @@ import {
7
7
  } from "@agent-native/core/oauth-tokens";
8
8
  import { markdownPreviewSnippet } from "@shared/markdown.js";
9
9
  import type { ComposeAttachment, EmailMessage } from "@shared/types.js";
10
- import { and, eq, inArray, isNull, lte, or } from "drizzle-orm";
10
+ import { and, asc, eq, inArray, isNull, lte, or } from "drizzle-orm";
11
11
  import { nanoid } from "nanoid";
12
12
 
13
13
  import { db, schema } from "../db/index.js";
@@ -921,6 +921,7 @@ export async function markJobProcessing(id: string): Promise<boolean> {
921
921
 
922
922
  export async function getDuePendingJobs(
923
923
  now: number,
924
+ limit: number,
924
925
  ): Promise<ScheduledJobRecord[]> {
925
926
  const due = await db
926
927
  .select()
@@ -930,7 +931,9 @@ export async function getDuePendingJobs(
930
931
  eq(schema.scheduledJobs.status, "pending"),
931
932
  lte(schema.scheduledJobs.runAt, now),
932
933
  ),
933
- );
934
+ )
935
+ .orderBy(asc(schema.scheduledJobs.runAt), asc(schema.scheduledJobs.id))
936
+ .limit(limit);
934
937
 
935
938
  return due as ScheduledJobRecord[];
936
939
  }
@@ -373,6 +373,22 @@ CREATE UNIQUE INDEX IF NOT EXISTS mail_ai_filter_backfills_owner_rule_set_active
373
373
  ON mail_ai_filter_backfills(owner_email, rule_set_key)
374
374
  WHERE rule_set_key IS NOT NULL AND status IN ('queued', 'running', 'undoing');`,
375
375
  },
376
+ {
377
+ version: 31,
378
+ name: "mail-gmail-watch-renewal-claims",
379
+ sql: `ALTER TABLE mail_sync_accounts
380
+ ADD COLUMN IF NOT EXISTS last_watch_renewed_at BIGINT;
381
+ ALTER TABLE mail_sync_accounts
382
+ ADD COLUMN IF NOT EXISTS last_watch_attempted_at BIGINT;
383
+ ALTER TABLE mail_sync_accounts
384
+ ADD COLUMN IF NOT EXISTS last_automation_attempted_at BIGINT;
385
+ ALTER TABLE mail_sync_accounts
386
+ ADD COLUMN IF NOT EXISTS watch_renew_claim_id TEXT;
387
+ ALTER TABLE mail_sync_accounts
388
+ ADD COLUMN IF NOT EXISTS watch_renew_claimed_at BIGINT;
389
+ CREATE INDEX IF NOT EXISTS idx_scheduled_jobs_status_run_at_id
390
+ ON scheduled_jobs(status, run_at, id);`,
391
+ },
376
392
  ],
377
393
  { table: "mail_migrations" },
378
394
  );
@@ -1,18 +1,25 @@
1
+ import { isProductionServerlessFunctionRuntime } from "@agent-native/core/db";
1
2
  import { registerEvent } from "@agent-native/core/event-bus";
2
3
  import { listOAuthAccounts } from "@agent-native/core/oauth-tokens";
3
4
  import {
5
+ isInBackgroundFunctionRuntime,
4
6
  registerRecurringSweepHandler,
5
7
  startIntervalJob,
8
+ type RecurringSweepContext,
6
9
  } from "@agent-native/core/server";
10
+ import { and, asc, eq, inArray, isNull, lte, or } from "drizzle-orm";
11
+ import { nanoid } from "nanoid";
7
12
  import { z } from "zod";
8
13
 
14
+ import { getDb, schema } from "../db/index.js";
9
15
  import {
10
16
  processMailAiFilterBackfills,
11
17
  purgeExpiredMailAiFilterBackfills,
12
18
  } from "../lib/ai-filter-backfill.js";
13
19
  import { purgeExpiredMailAiFilterRuleUndoSnapshots } from "../lib/ai-filter-rule-undo.js";
14
- import { processAutomations } from "../lib/automation-engine.js";
15
- import { getClientForAccount, startWatch } from "../lib/google-auth.js";
20
+ import { processAutomationsForAccount } from "../lib/automation-engine.js";
21
+ import { getClientFromAccount, startWatch } from "../lib/google-auth.js";
22
+ import { ensureSyncAccountRow } from "../lib/inbox-store.js";
16
23
  import {
17
24
  getDuePendingJobs,
18
25
  getSnoozeThreadId,
@@ -27,32 +34,242 @@ import {
27
34
 
28
35
  const INTERVAL_MS = 60_000;
29
36
  const AI_FILTER_BACKFILL_INTERVAL_MS = 10_000;
30
- const WATCH_RENEW_INTERVAL_MS = 12 * 60 * 60_000;
37
+ const WATCH_RENEW_INTERVAL_MS = 6 * 60 * 60_000;
38
+ const WATCH_RENEW_CLAIM_MS = 10 * 60_000;
39
+ const MAX_DUE_JOBS_PER_TICK = 20;
40
+ const MAX_AUTOMATION_ACCOUNTS_PER_TICK = 5;
41
+ const MAX_WATCH_ACCOUNTS_PER_TICK = 5;
31
42
  const TICK_ABORT_MS = Math.max(10_000, INTERVAL_MS * 4);
32
- let lastWatchRenewalAt = 0;
43
+ const MIN_DATABASE_TIME_MS = 0;
33
44
  let skippingLogged = false;
34
45
 
35
- async function renewAllWatches(): Promise<void> {
46
+ type WatchRenewalClaim = { accountRowId: string; claimId: string };
47
+ type OAuthAccount = Awaited<ReturnType<typeof listOAuthAccounts>>[number];
48
+
49
+ function mailSyncAccountId(account: OAuthAccount): string {
50
+ const ownerEmail = (account.owner || account.accountId).trim().toLowerCase();
51
+ return `${ownerEmail}:${account.accountId.trim().toLowerCase()}`;
52
+ }
53
+
54
+ function isDeadlineReached(deadlineAt: number): boolean {
55
+ return Date.now() >= deadlineAt;
56
+ }
57
+
58
+ function incompleteSweepError(message: string): Error {
59
+ return new Error(`Mail background sweep incomplete: ${message}`);
60
+ }
61
+
62
+ function makeAggregateError(errors: Iterable<unknown>, message: string): Error {
63
+ const NativeAggregateError = (
64
+ globalThis as unknown as {
65
+ AggregateError: new (errors: Iterable<unknown>, message: string) => Error;
66
+ }
67
+ ).AggregateError;
68
+ return new NativeAggregateError(errors, message);
69
+ }
70
+
71
+ async function orderAccountsByOldestAttempt(
72
+ accounts: OAuthAccount[],
73
+ attemptColumn:
74
+ | typeof schema.mailSyncAccounts.lastAutomationAttemptedAt
75
+ | typeof schema.mailSyncAccounts.lastWatchAttemptedAt,
76
+ ): Promise<OAuthAccount[]> {
77
+ if (accounts.length < 2) return accounts;
78
+
79
+ const accountIds = accounts.map(mailSyncAccountId);
80
+ const attempts = await getDb()
81
+ .select({
82
+ id: schema.mailSyncAccounts.id,
83
+ attemptedAt: attemptColumn,
84
+ })
85
+ .from(schema.mailSyncAccounts)
86
+ .where(inArray(schema.mailSyncAccounts.id, accountIds));
87
+ const attemptedAtById = new Map(
88
+ attempts.map(({ id, attemptedAt }) => [
89
+ id,
90
+ attemptedAt ?? MIN_DATABASE_TIME_MS,
91
+ ]),
92
+ );
93
+
94
+ return [...accounts].sort((left, right) => {
95
+ const leftId = mailSyncAccountId(left);
96
+ const rightId = mailSyncAccountId(right);
97
+ return (
98
+ (attemptedAtById.get(leftId) ?? MIN_DATABASE_TIME_MS) -
99
+ (attemptedAtById.get(rightId) ?? MIN_DATABASE_TIME_MS) ||
100
+ leftId.localeCompare(rightId)
101
+ );
102
+ });
103
+ }
104
+
105
+ async function markAccountAttempted(
106
+ accountId: string,
107
+ kind: "automation" | "watch",
108
+ ): Promise<void> {
109
+ const now = Date.now();
110
+ const attempt =
111
+ kind === "automation"
112
+ ? { lastAutomationAttemptedAt: now }
113
+ : { lastWatchAttemptedAt: now };
114
+ const rows = await getDb()
115
+ .update(schema.mailSyncAccounts)
116
+ .set({ ...attempt, updatedAt: now })
117
+ .where(eq(schema.mailSyncAccounts.id, accountId))
118
+ .returning({ id: schema.mailSyncAccounts.id });
119
+ if (rows.length === 0) {
120
+ throw new Error(`Mail account attempt cursor is missing for ${accountId}.`);
121
+ }
122
+ }
123
+
124
+ async function claimWatchRenewal(
125
+ accountRowId: string,
126
+ ): Promise<WatchRenewalClaim | null> {
127
+ const now = Date.now();
128
+ const claimId = nanoid(24);
129
+ const rows = await getDb()
130
+ .update(schema.mailSyncAccounts)
131
+ .set({
132
+ watchRenewClaimId: claimId,
133
+ watchRenewClaimedAt: now,
134
+ updatedAt: now,
135
+ })
136
+ .where(
137
+ and(
138
+ eq(schema.mailSyncAccounts.id, accountRowId),
139
+ or(
140
+ isNull(schema.mailSyncAccounts.lastWatchRenewedAt),
141
+ lte(
142
+ schema.mailSyncAccounts.lastWatchRenewedAt,
143
+ now - WATCH_RENEW_INTERVAL_MS,
144
+ ),
145
+ ),
146
+ or(
147
+ isNull(schema.mailSyncAccounts.watchRenewClaimId),
148
+ isNull(schema.mailSyncAccounts.watchRenewClaimedAt),
149
+ lte(
150
+ schema.mailSyncAccounts.watchRenewClaimedAt,
151
+ now - WATCH_RENEW_CLAIM_MS,
152
+ ),
153
+ ),
154
+ ),
155
+ )
156
+ .returning({ id: schema.mailSyncAccounts.id });
157
+ return rows.length > 0 ? { accountRowId, claimId } : null;
158
+ }
159
+
160
+ async function completeWatchRenewal(
161
+ accountId: string,
162
+ claim: WatchRenewalClaim,
163
+ ): Promise<void> {
164
+ const renewedAt = Date.now();
165
+ const rows = await getDb()
166
+ .update(schema.mailSyncAccounts)
167
+ .set({
168
+ lastWatchRenewedAt: renewedAt,
169
+ watchRenewClaimId: null,
170
+ watchRenewClaimedAt: null,
171
+ updatedAt: renewedAt,
172
+ })
173
+ .where(
174
+ and(
175
+ eq(schema.mailSyncAccounts.id, claim.accountRowId),
176
+ eq(schema.mailSyncAccounts.watchRenewClaimId, claim.claimId),
177
+ ),
178
+ )
179
+ .returning({ id: schema.mailSyncAccounts.id });
180
+ if (rows.length === 0) {
181
+ throw new Error(`Gmail watch renewal claim was lost for ${accountId}.`);
182
+ }
183
+ }
184
+
185
+ async function releaseWatchRenewal(claim: WatchRenewalClaim): Promise<void> {
186
+ const now = Date.now();
187
+ await getDb()
188
+ .update(schema.mailSyncAccounts)
189
+ .set({
190
+ watchRenewClaimId: null,
191
+ watchRenewClaimedAt: null,
192
+ updatedAt: now,
193
+ })
194
+ .where(
195
+ and(
196
+ eq(schema.mailSyncAccounts.id, claim.accountRowId),
197
+ eq(schema.mailSyncAccounts.watchRenewClaimId, claim.claimId),
198
+ ),
199
+ );
200
+ }
201
+
202
+ async function renewAllWatches(context: RecurringSweepContext): Promise<void> {
36
203
  if (!process.env.GMAIL_WATCH_TOPIC) return;
37
- const accounts = await listOAuthAccounts("google");
38
- for (const acc of accounts) {
204
+ const accounts = await orderAccountsByOldestAttempt(
205
+ await listOAuthAccounts("google"),
206
+ schema.mailSyncAccounts.lastWatchAttemptedAt,
207
+ );
208
+ const failures: unknown[] = [];
209
+ for (const acc of accounts.slice(0, MAX_WATCH_ACCOUNTS_PER_TICK)) {
210
+ if (isDeadlineReached(context.deadlineAt)) {
211
+ throw incompleteSweepError("Gmail watch renewals remain pending.");
212
+ }
213
+ const ownerEmail = (acc.owner || acc.accountId).trim().toLowerCase();
214
+ let claim: WatchRenewalClaim | null = null;
39
215
  try {
40
- const client = await getClientForAccount(acc.accountId);
41
- if (!client) continue;
42
- await startWatch(client.accessToken);
43
- } catch (err: any) {
44
- console.warn(
45
- `[gmail-watch] renew failed for ${acc.accountId}: ${err.message}`,
46
- );
216
+ const syncAccount = await ensureSyncAccountRow(ownerEmail, acc.accountId);
217
+ await markAccountAttempted(syncAccount.id, "watch");
218
+ if (isDeadlineReached(context.deadlineAt)) {
219
+ throw incompleteSweepError(
220
+ `Gmail watch renewal remains pending for ${acc.accountId}.`,
221
+ );
222
+ }
223
+ claim = await claimWatchRenewal(syncAccount.id);
224
+ if (!claim) continue;
225
+ const client = await getClientFromAccount({
226
+ ...acc,
227
+ owner: acc.owner ?? undefined,
228
+ });
229
+ if (!client) throw new Error("No usable Google account token.");
230
+ if (!(await startWatch(client.accessToken))) {
231
+ throw new Error("Gmail did not start the watch.");
232
+ }
233
+ await completeWatchRenewal(acc.accountId, claim);
234
+ } catch (error) {
235
+ if (claim) {
236
+ try {
237
+ await releaseWatchRenewal(claim);
238
+ } catch (releaseError) {
239
+ failures.push(
240
+ makeAggregateError(
241
+ [error, releaseError],
242
+ `Gmail watch renewal and claim release failed for ${acc.accountId}.`,
243
+ ),
244
+ );
245
+ console.warn(
246
+ `[gmail-watch] renew and claim release failed for ${acc.accountId}:`,
247
+ error,
248
+ releaseError,
249
+ );
250
+ continue;
251
+ }
252
+ }
253
+ failures.push(error);
254
+ console.warn(`[gmail-watch] renew failed for ${acc.accountId}:`, error);
47
255
  }
48
256
  }
257
+ if (failures.length > 0) {
258
+ throw makeAggregateError(
259
+ failures,
260
+ `Gmail watch renewal failed for ${failures.length} account(s).`,
261
+ );
262
+ }
49
263
  }
50
264
 
51
- async function processJobs(): Promise<void> {
265
+ async function processJobs(context: RecurringSweepContext): Promise<void> {
52
266
  const now = Date.now();
53
- const due = await getDuePendingJobs(now);
267
+ const due = await getDuePendingJobs(now, MAX_DUE_JOBS_PER_TICK);
54
268
 
55
269
  for (const job of due) {
270
+ if (isDeadlineReached(context.deadlineAt)) {
271
+ throw incompleteSweepError("scheduled Mail jobs remain pending.");
272
+ }
56
273
  if (!(await markJobProcessing(job.id))) continue;
57
274
 
58
275
  try {
@@ -83,11 +300,115 @@ async function processJobs(): Promise<void> {
83
300
  }
84
301
  }
85
302
 
86
- export default () => {
87
- registerRecurringSweepHandler("mail-ai-filter-backfills", () =>
88
- processMailAiFilterBackfills(),
303
+ async function processAutomations(
304
+ context: RecurringSweepContext,
305
+ ): Promise<void> {
306
+ const accounts = await orderAccountsByOldestAttempt(
307
+ await listOAuthAccounts("google"),
308
+ schema.mailSyncAccounts.lastAutomationAttemptedAt,
309
+ );
310
+ const failures: unknown[] = [];
311
+
312
+ for (const account of accounts.slice(0, MAX_AUTOMATION_ACCOUNTS_PER_TICK)) {
313
+ if (isDeadlineReached(context.deadlineAt)) {
314
+ throw incompleteSweepError("Mail automations remain pending.");
315
+ }
316
+ const ownerEmail = (account.owner || account.accountId)
317
+ .trim()
318
+ .toLowerCase();
319
+ try {
320
+ const syncAccount = await ensureSyncAccountRow(
321
+ ownerEmail,
322
+ account.accountId,
323
+ );
324
+ await markAccountAttempted(syncAccount.id, "automation");
325
+ if (isDeadlineReached(context.deadlineAt)) {
326
+ throw incompleteSweepError(
327
+ `Mail automation remains pending for ${account.accountId}.`,
328
+ );
329
+ }
330
+ const client = await getClientFromAccount({
331
+ ...account,
332
+ owner: account.owner ?? undefined,
333
+ });
334
+ if (!client) continue;
335
+ await processAutomationsForAccount(
336
+ ownerEmail,
337
+ account.accountId,
338
+ client.accessToken,
339
+ );
340
+ } catch (error) {
341
+ failures.push(error);
342
+ console.error(
343
+ `[mail-jobs] automation processing failed for ${account.accountId}:`,
344
+ error,
345
+ );
346
+ }
347
+ }
348
+
349
+ if (failures.length > 0) {
350
+ throw makeAggregateError(
351
+ failures,
352
+ `Mail automation processing failed for ${failures.length} account(s).`,
353
+ );
354
+ }
355
+ }
356
+
357
+ async function processMailBackgroundJobs(
358
+ context: RecurringSweepContext = { deadlineAt: Date.now() + TICK_ABORT_MS },
359
+ ): Promise<void> {
360
+ const failures: unknown[] = [];
361
+ const runStep = async (name: string, run: () => Promise<unknown>) => {
362
+ if (isDeadlineReached(context.deadlineAt)) {
363
+ const error = incompleteSweepError(
364
+ `${name} did not start before the deadline.`,
365
+ );
366
+ failures.push(error);
367
+ console.error(`[mail-jobs] ${name} skipped:`, error);
368
+ return;
369
+ }
370
+ try {
371
+ await run();
372
+ } catch (error) {
373
+ failures.push(error);
374
+ console.error(`[mail-jobs] ${name} failed:`, error);
375
+ }
376
+ };
377
+
378
+ await runStep("processJobs", () => processJobs(context));
379
+ await runStep("processAutomations", () => processAutomations(context));
380
+ await runStep("renewAllWatches", () => renewAllWatches(context));
381
+ await runStep(
382
+ "AI-filter undo cleanup",
383
+ purgeExpiredMailAiFilterRuleUndoSnapshots,
384
+ );
385
+ await runStep(
386
+ "AI-filter backfill cleanup",
387
+ purgeExpiredMailAiFilterBackfills,
89
388
  );
90
389
 
390
+ if (failures.length > 0) {
391
+ throw makeAggregateError(
392
+ failures,
393
+ "One or more Mail background jobs failed.",
394
+ );
395
+ }
396
+ }
397
+
398
+ export default () => {
399
+ registerRecurringSweepHandler("mail-ai-filter-backfills", async (context) => {
400
+ if (isDeadlineReached(context.deadlineAt)) {
401
+ throw incompleteSweepError(
402
+ "AI-filter backfills did not start before the deadline.",
403
+ );
404
+ }
405
+ await processMailAiFilterBackfills(
406
+ undefined,
407
+ undefined,
408
+ Math.min(context.deadlineAt, Date.now() + 45_000),
409
+ );
410
+ });
411
+
91
412
  registerEvent({
92
413
  name: "mail.message.received",
93
414
  description:
@@ -128,6 +449,12 @@ export default () => {
128
449
  const isProd = process.env.NODE_ENV === "production";
129
450
  const flag = process.env.RUN_BACKGROUND_JOBS;
130
451
  const enabled = flag === "1" || (isProd && flag !== "0");
452
+ if (enabled) {
453
+ registerRecurringSweepHandler(
454
+ "mail-background-jobs",
455
+ processMailBackgroundJobs,
456
+ );
457
+ }
131
458
  if (!enabled) {
132
459
  if (!skippingLogged) {
133
460
  console.log(
@@ -138,35 +465,19 @@ export default () => {
138
465
  return;
139
466
  }
140
467
 
468
+ if (
469
+ isProductionServerlessFunctionRuntime() ||
470
+ isInBackgroundFunctionRuntime()
471
+ ) {
472
+ return;
473
+ }
474
+
141
475
  startIntervalJob(
142
476
  async () => {
143
477
  try {
144
- await purgeExpiredMailAiFilterRuleUndoSnapshots();
145
- } catch (err) {
146
- console.error("[mail-jobs] AI-filter undo cleanup failed:", err);
147
- }
148
- try {
149
- await purgeExpiredMailAiFilterBackfills();
150
- } catch (err) {
151
- console.error("[mail-jobs] AI-filter backfill cleanup failed:", err);
152
- }
153
- try {
154
- await processJobs();
478
+ await processMailBackgroundJobs();
155
479
  } catch (err) {
156
- console.error("[mail-jobs] processJobs failed:", err);
157
- }
158
- try {
159
- await processAutomations();
160
- } catch (err) {
161
- console.error("[mail-jobs] processAutomations failed:", err);
162
- }
163
- if (Date.now() - lastWatchRenewalAt > WATCH_RENEW_INTERVAL_MS) {
164
- lastWatchRenewalAt = Date.now();
165
- try {
166
- await renewAllWatches();
167
- } catch (err) {
168
- console.error("[mail-jobs] renewAllWatches failed:", err);
169
- }
480
+ console.error("[mail-jobs] background job tick failed:", err);
170
481
  }
171
482
  },
172
483
  {
@@ -3,14 +3,32 @@
3
3
  All notable user-facing changes to Agent-Native Slides are documented here. Open it any
4
4
  time from the command menu (Cmd+K → "What's new").
5
5
 
6
+ ## 2026-09-28
7
+
8
+ ### Improved
9
+
10
+ - Starter prompts now sit below the chat composer
11
+ - The deck list search and import controls stay legible on narrow screens.
12
+
13
+ ### Fixed
14
+
15
+ - The model picker shows available models when AI is configured.
16
+ - Reference attachments stay available when a new deck prompt starts a chat.
17
+ - Template menus stay available when you hover over a card.
18
+
6
19
  ## 2026-09-27
7
20
 
8
21
  ### Improved
9
22
 
23
+ - Created decks appear in chat as compact cards with a direct link to Slides.
24
+ - Deck results no longer show a nested card frame.
25
+ - Generated decks show slide previews in chat
10
26
  - The Connect AI setup card now has even spacing above and below the composer.
11
27
 
12
28
  ### Fixed
13
29
 
30
+ - Deck creation returns to the prompt when chat delivery fails and offers a retry when generation does not start.
31
+ - Personalized presentation suggestions work when responses include extra text
14
32
  - Fixed deck edits that could be overwritten during unload
15
33
  - Pagehide saves include queued edits and stay within the keepalive budget
16
34
  - Text edits are preserved when leaving or reloading a deck, and link drops no longer navigate away.
@@ -21,6 +39,7 @@ time from the command menu (Cmd+K → "What's new").
21
39
 
22
40
  ### Added
23
41
 
42
+ - Requesters now get an email when a deck owner approves their access request.
24
43
  - Twelve new hand-designed deck templates — from a Swiss-grid pitch and data-dense quarterly review to a launch keynote, research report, lesson, roadmap, and portfolio — with the original starters still available below them.
25
44
 
26
45
  ### Improved
@@ -69,6 +88,7 @@ time from the command menu (Cmd+K → "What's new").
69
88
 
70
89
  ### Added
71
90
 
91
+ - Ask the agent to turn comment and reply emails on or off
72
92
  - Open an editable presentation from a template in one click, or inspect every slide in a large modal preview without AI generation.
73
93
  - Slides home quick actions now adapt to your onboarding role and appear only when an AI provider is ready.
74
94
  - Org admins can review agent runs from Settings
@@ -76,6 +96,8 @@ time from the command menu (Cmd+K → "What's new").
76
96
 
77
97
  ### Improved
78
98
 
99
+ - Image generation without Assets tries the provider your organization picks for it first
100
+ - Settings now opens from your name at the bottom of the sidebar, or with ⌘,.
79
101
  - Attach Figma frames and websites from focused dialogs while keeping prompt drafts and uploaded design.md files intact.
80
102
  - Recent decks now use the same consistent library card layout as templates.
81
103
  - Import PDF and PowerPoint files directly from the toolbar, paste Google Slides links in a compact popover, and find design and presentation references together under Design.
@@ -90,6 +112,8 @@ time from the command menu (Cmd+K → "What's new").
90
112
 
91
113
  ### Fixed
92
114
 
115
+ - Choosing references now keeps Continue disabled until a Figma or website link is a valid URL, or a Google Slides link is a valid URL or picker file ID
116
+ - Opening a deck you can't access now shows an access denied page where you can request access with a note for the owner or switch accounts, instead of an endless loading screen.
93
117
  - Pressing Enter in a flex-anchored text box no longer adds extra blank lines when you leave editing, and Arabic text stays joined while you type
94
118
  - Slides generation recovers from long pauses and lets you continue from the saved deck
95
119
  - Slides guides users to connect AI before continuing deck questions.
@@ -112,6 +136,10 @@ time from the command menu (Cmd+K → "What's new").
112
136
 
113
137
  ## 2026-09-24
114
138
 
139
+ ### Added
140
+
141
+ - The Slides agent can check every slide's text color contrast against accessibility guidelines and fix what fails.
142
+
115
143
  ### Improved
116
144
 
117
145
  - Slides completes multi-slide edits with fewer rounds of work
@@ -2130,7 +2130,7 @@ export default function Index({ active = true }: { active?: boolean }) {
2130
2130
  disabled={!isHome || !agentEngineConfigured}
2131
2131
  submissionDisabled={!agentEngineConfigured}
2132
2132
  showModelSelector={agentEngineConfigured}
2133
- modelStatusChecksEnabled={false}
2133
+ modelStatusChecksEnabled={agentEngineConfigured}
2134
2134
  open={showNewDeckPrompt}
2135
2135
  active={isHome}
2136
2136
  onOpenChange={setNewDeckPromptOpen}
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-28
4
+ ---
5
+
6
+ The model picker shows available models when AI is configured.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-28
4
+ ---
5
+
6
+ Template menus stay available when you hover over a card.
@@ -139,8 +139,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
139
139
  duplicate?: undefined;
140
140
  eventId?: undefined;
141
141
  } | {
142
- error?: undefined;
143
142
  accepted: boolean;
144
143
  duplicate: boolean;
145
144
  eventId: string;
145
+ error?: undefined;
146
146
  }>;
@@ -1,5 +1,6 @@
1
1
  /** @jsxRuntime classic */
2
2
  import { AuthForm } from "@agent-native/toolkit/onboarding";
3
+ import { IconLoader2 } from "@tabler/icons-react";
3
4
  import * as React from "react";
4
5
  import { normalizeLocaleCode } from "../../localization/shared.js";
5
6
  import { canonicalTrackingEvent } from "../../shared/analytics-events.js";
@@ -1999,8 +2000,8 @@ export function AuthPage(props) {
1999
2000
  React.createElement("div", { id: "full-auth-options", className: "full-auth-options", hidden: !fullAuthOptionsVisible },
2000
2001
  googleOnly && !showGoogle ? (React.createElement("p", { className: "google-error show", id: "google-err", role: "status", "data-i18n": "googleNotConfigured" }, t("googleNotConfigured"))) : null,
2001
2002
  showGoogle ? (React.createElement("div", { className: "google-signin", id: "google-signin" },
2002
- React.createElement("button", { className: `btn-google ${view === "magicLink" && isValidEmail(magicLinkEmail) ? "magic-link-secondary" : ""}`, id: "google-btn", type: "button", disabled: googleBusy, onClick: () => void startGoogle() },
2003
- googleSvg(),
2003
+ React.createElement("button", { className: `btn-google ${view === "magicLink" && isValidEmail(magicLinkEmail) ? "magic-link-secondary" : ""}`, id: "google-btn", type: "button", disabled: googleBusy, "aria-busy": googleBusy, onClick: () => void startGoogle() },
2004
+ googleBusy ? (React.createElement(IconLoader2, { className: "animate-spin", "aria-hidden": "true" })) : (googleSvg()),
2004
2005
  React.createElement("span", { "data-i18n": "googleButton" }, t("googleButton"))),
2005
2006
  notice("google"),
2006
2007
  React.createElement("p", { className: "google-debug", id: "google-debug" }))) : null,