@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/CHANGELOG.md +17 -0
- package/corpus/templates/assets/CHANGELOG.md +18 -0
- package/corpus/templates/brain/CHANGELOG.md +6 -0
- package/corpus/templates/calendar/CHANGELOG.md +4 -0
- package/corpus/templates/clips/CHANGELOG.md +19 -0
- package/corpus/templates/clips/actions/create-recording-agent-link.ts +8 -2
- package/corpus/templates/clips/actions/update-recording.ts +5 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +10 -3
- package/corpus/templates/clips/changelog/2026-09-28-agent-links-stay-valid-when-clip-details-change.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-28-password-changes-immediately-invalidate-old-recording-links.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-28-password-protected-recording-links-stop-working-when-the-pas.md +6 -0
- package/corpus/templates/clips/server/db/schema.ts +3 -0
- package/corpus/templates/clips/server/lib/player-video-url.ts +9 -1
- package/corpus/templates/clips/server/lib/public-agent-context.ts +12 -4
- package/corpus/templates/clips/server/lib/share-password.ts +25 -0
- package/corpus/templates/clips/server/plugins/db.ts +36 -0
- package/corpus/templates/clips/server/routes/[...page].get.ts +8 -1
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +33 -6
- package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +34 -6
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +26 -5
- package/corpus/templates/content/CHANGELOG.md +1 -0
- package/corpus/templates/crm/CHANGELOG.md +12 -0
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +265 -3
- package/corpus/templates/design/CHANGELOG.md +21 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +463 -6
- package/corpus/templates/design/app/pages/Index.tsx +1 -1
- package/corpus/templates/design/changelog/2026-09-27-auto-focused-app-fields-no-longer-interrupt-canvas-shortcuts.md +6 -0
- package/corpus/templates/design/changelog/2026-09-28-model-picker-loads-available-models.md +6 -0
- package/corpus/templates/design/changelog/2026-09-28-template-menus-stay-available-on-hover.md +6 -0
- package/corpus/templates/dispatch/CHANGELOG.md +18 -0
- package/corpus/templates/forms/CHANGELOG.md +6 -0
- package/corpus/templates/mail/.env.example +1 -1
- package/corpus/templates/mail/CHANGELOG.md +33 -0
- package/corpus/templates/mail/changelog/2026-09-28-fixed-inbox-loading-stalls-and-showed-progress-during-google-2.md +6 -0
- package/corpus/templates/mail/changelog/2026-09-28-fixed-inbox-loading-stalls-and-showed-progress-during-google.md +6 -0
- package/corpus/templates/mail/server/db/schema.ts +7 -0
- package/corpus/templates/mail/server/lib/ai-filter-backfill.ts +4 -0
- package/corpus/templates/mail/server/lib/jobs.ts +5 -2
- package/corpus/templates/mail/server/plugins/db.ts +16 -0
- package/corpus/templates/mail/server/plugins/mail-jobs.ts +355 -44
- package/corpus/templates/slides/CHANGELOG.md +28 -0
- package/corpus/templates/slides/app/pages/Index.tsx +1 -1
- package/corpus/templates/slides/changelog/2026-09-28-model-picker-loads-available-models.md +6 -0
- package/corpus/templates/slides/changelog/2026-09-28-template-menus-stay-available-on-hover.md +6 -0
- package/dist/automation/index.d.ts +1 -1
- package/dist/client/auth/AuthPage.js +3 -2
- package/dist/client/org/OrgSwitcher.js +31 -19
- package/dist/client/settings/UsageSection.js +3 -3
- package/dist/client/use-action.js +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/jobs/background-automation-runner.d.ts +2 -0
- package/dist/jobs/background-automation-runner.js +20 -1
- package/dist/jobs/sweep-hooks.d.ts +6 -2
- package/dist/jobs/sweep-hooks.js +17 -9
- package/dist/localization/core-messages/ar-SA.js +2 -2
- package/dist/localization/core-messages/de-DE.js +2 -2
- package/dist/localization/core-messages/en-US.d.ts +2 -2
- package/dist/localization/core-messages/en-US.js +2 -2
- package/dist/localization/core-messages/es-ES.js +2 -2
- package/dist/localization/core-messages/fr-FR.js +2 -2
- package/dist/localization/core-messages/hi-IN.js +2 -2
- package/dist/localization/core-messages/ja-JP.js +2 -2
- package/dist/localization/core-messages/ko-KR.js +2 -2
- package/dist/localization/core-messages/pt-BR.js +2 -2
- package/dist/localization/core-messages/zh-CN.js +2 -2
- package/dist/localization/core-messages/zh-TW.js +2 -2
- package/dist/localization/core-messages.d.ts +2 -2
- package/dist/notifications/routes.d.ts +5 -5
- package/dist/observability/routes.d.ts +6 -6
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
- package/dist/provider-api/actions/provider-api.d.ts +7 -7
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +4 -4
- package/dist/server/agent-chat-plugin.js +6 -2
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/triggers/dispatcher.js +167 -36
- package/dist/triggers/event-queue.d.ts +18 -1
- package/dist/triggers/event-queue.js +143 -10
- 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 {
|
|
15
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
43
|
+
const MIN_DATABASE_TIME_MS = 0;
|
|
33
44
|
let skippingLogged = false;
|
|
34
45
|
|
|
35
|
-
|
|
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
|
|
38
|
-
|
|
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
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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]
|
|
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={
|
|
2133
|
+
modelStatusChecksEnabled={agentEngineConfigured}
|
|
2134
2134
|
open={showNewDeckPrompt}
|
|
2135
2135
|
active={isHome}
|
|
2136
2136
|
onOpenChange={setNewDeckPromptOpen}
|
|
@@ -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,
|