@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
@@ -314,6 +314,7 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
314
314
  provider?: undefined;
315
315
  id?: undefined;
316
316
  deleted?: undefined;
317
+ found?: undefined;
317
318
  providers: {
318
319
  id: string;
319
320
  label: string;
@@ -324,15 +325,14 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
324
325
  updatedAt: number;
325
326
  }[];
326
327
  count: number;
327
- found?: undefined;
328
328
  registered?: undefined;
329
329
  label?: undefined;
330
330
  message?: undefined;
331
331
  } | {
332
332
  id?: undefined;
333
- count?: undefined;
334
333
  deleted?: undefined;
335
334
  providers?: undefined;
335
+ count?: undefined;
336
336
  found: boolean;
337
337
  provider: import("../custom-registry.js").CustomProviderConfig;
338
338
  registered?: undefined;
@@ -340,9 +340,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
340
340
  message?: undefined;
341
341
  } | {
342
342
  provider?: undefined;
343
- count?: undefined;
344
343
  deleted?: undefined;
345
344
  providers?: undefined;
345
+ count?: undefined;
346
346
  found: boolean;
347
347
  id: string;
348
348
  registered?: undefined;
@@ -350,9 +350,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
350
350
  message?: undefined;
351
351
  } | {
352
352
  provider?: undefined;
353
- count?: undefined;
354
- providers?: undefined;
355
353
  found?: undefined;
354
+ providers?: undefined;
355
+ count?: undefined;
356
356
  deleted: boolean;
357
357
  id: string;
358
358
  registered?: undefined;
@@ -360,10 +360,10 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
360
360
  message?: undefined;
361
361
  } | {
362
362
  provider?: undefined;
363
- count?: undefined;
364
363
  deleted?: undefined;
365
- providers?: undefined;
366
364
  found?: undefined;
365
+ providers?: undefined;
366
+ count?: undefined;
367
367
  registered: boolean;
368
368
  id: string;
369
369
  label: string;
@@ -39,8 +39,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
39
39
  error: string;
40
40
  }>;
41
41
  export declare function handleDeleteResource(event: any): Promise<{
42
- ok?: undefined;
43
42
  error: string;
43
+ ok?: undefined;
44
44
  } | {
45
45
  error?: undefined;
46
46
  ok: boolean;
@@ -55,31 +55,31 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
55
55
  } | {
56
56
  error: string;
57
57
  errorCode?: undefined;
58
- status?: undefined;
59
58
  ok?: undefined;
59
+ status?: undefined;
60
60
  } | {
61
61
  error: string;
62
62
  errorCode: string;
63
- status?: undefined;
64
63
  ok?: undefined;
64
+ status?: undefined;
65
65
  } | {
66
66
  errorCode?: undefined;
67
67
  ok: boolean;
68
68
  status: string;
69
69
  error?: undefined;
70
70
  } | {
71
+ ok?: undefined;
71
72
  error: string;
72
73
  removed?: undefined;
73
- ok?: undefined;
74
74
  } | {
75
75
  ok: boolean;
76
76
  removed: boolean;
77
77
  error?: undefined;
78
78
  }>>;
79
79
  export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
80
+ ok?: undefined;
80
81
  error: string;
81
82
  note?: undefined;
82
- ok?: undefined;
83
83
  } | {
84
84
  ok: boolean;
85
85
  note?: undefined;
@@ -33,6 +33,7 @@ import { isProductionServerlessFunctionRuntime, isTransientDatabaseError, } from
33
33
  import { filterFrameworkToolGroups, resolveFrameworkTools, } from "../framework-tools.js";
34
34
  import { verifyInternalToken, extractBearerToken, } from "../integrations/internal-token.js";
35
35
  import { RECURRING_JOBS_SWEEP_PATH, RECURRING_JOBS_SWEEP_TOKEN_SUBJECT, } from "../jobs/scheduler-dispatch.js";
36
+ import { RECURRING_SWEEP_BUDGET_MS } from "../jobs/sweep-hooks.js";
36
37
  import { McpClientManager, mcpToolsToActionEntries, syncMcpActionEntries, mountMcpServersRoutes, mountMcpHubRoutes, buildMergedConfig, startMcpConfigRefresh, getHubStatus, isHubServeEnabled, } from "../mcp-client/index.js";
37
38
  import { declaredMcpToolNames } from "../mcp/build-server.js";
38
39
  import { setProgressPreListHook } from "../progress/store.js";
@@ -5915,6 +5916,9 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
5915
5916
  error: "Recurring-job sweep reached the synchronous server instead of the durable background worker.",
5916
5917
  };
5917
5918
  }
5919
+ const sweepContext = {
5920
+ deadlineAt: Date.now() + RECURRING_SWEEP_BUDGET_MS,
5921
+ };
5918
5922
  // Stale reaping runs FIRST and site-wide, before the open-ended job
5919
5923
  // sweep can spend the platform wall. It is the durable driver the
5920
5924
  // in-process fast sweep below cannot be on serverless: that timer is
@@ -5941,6 +5945,8 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
5941
5945
  console.error("[agent-chat] durable stale-run reap failed:", error);
5942
5946
  return null;
5943
5947
  });
5948
+ const { runRecurringSweepHandlers } = await import("../jobs/sweep-hooks.js");
5949
+ const appSweepHandlers = await runRecurringSweepHandlers(sweepContext);
5944
5950
  // Rides the same site-tick as the reap above, for the same reason:
5945
5951
  // it is the only durable driver on serverless. Never fatal to the
5946
5952
  // job sweep, and its own failure is a distinguishable outcome
@@ -5961,8 +5967,6 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
5961
5967
  console.error("[agent-chat] durable unclaimed-run sweep failed:", error);
5962
5968
  return null;
5963
5969
  });
5964
- const { runRecurringSweepHandlers } = await import("../jobs/sweep-hooks.js");
5965
- const appSweepHandlers = await runRecurringSweepHandlers();
5966
5970
  const triggerAvailability = scheduledTriggerAvailability();
5967
5971
  if (unclaimedBackgroundRuns === null) {
5968
5972
  setResponseStatus(event, 500);
@@ -65,6 +65,7 @@ export declare function createAgentEngineApiKeyHandler(): import("h3").EventHand
65
65
  error: any;
66
66
  code: any;
67
67
  } | {
68
+ error?: undefined;
68
69
  errorCode?: undefined;
69
70
  code?: undefined;
70
71
  ok: boolean;
@@ -72,6 +73,5 @@ export declare function createAgentEngineApiKeyHandler(): import("h3").EventHand
72
73
  baseUrlKey?: string;
73
74
  scope: AgentEngineApiKeyScope;
74
75
  defaultModel?: import("./agent-engine-default-model-route.js").SavedKeyDefaultModelOutcome;
75
- error?: undefined;
76
76
  }>>;
77
77
  export {};
@@ -8,8 +8,8 @@ export declare const AGENT_ENGINE_API_KEY_ACTION_NAME = "agent-engine-api-key";
8
8
  * resolveEngine's credential detection.
9
9
  */
10
10
  export declare function createAgentEngineDisconnectHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
11
- error: string;
12
11
  ok?: undefined;
12
+ error: string;
13
13
  scope?: undefined;
14
14
  } | {
15
15
  ok: boolean;
@@ -1,7 +1,7 @@
1
1
  export declare function createAgentEngineOllamaModelsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
2
2
  error: string;
3
- ok?: undefined;
4
3
  models?: undefined;
4
+ ok?: undefined;
5
5
  } | {
6
6
  ok: boolean;
7
7
  models: string[];
@@ -97,7 +97,7 @@ export { sendEmail, isEmailConfigured, getEmailReadiness, getEmailProvider, type
97
97
  export { defineTransactionalEmail, defineTransactionalEmails, replaceTransactionalEmails, listTransactionalEmails, getTransactionalEmail, renderTransactionalEmailPreview, type TransactionalEmailDefinition, type RegisteredTransactionalEmail, } from "../email-catalog/registry.js";
98
98
  export { notifyActivity, runActivityNotification, resolveActivityRecipients, type ActivityDeliveryFailure, type ActivityNotificationResult, type ActivityNotificationStatus, type NotifyActivityInput, type ResolveActivityRecipientsInput, } from "./activity-notifications.js";
99
99
  export { renderEmail, emailStrong, emailQuote, emailLink, type RenderEmailArgs, type RenderedEmail, type EmailCta, } from "./email-template.js";
100
- export { hasRecurringSweepHandler, registerRecurringSweepHandler, runRecurringSweepHandlers, type RecurringSweepHandler, } from "../jobs/sweep-hooks.js";
100
+ export { hasRecurringSweepHandler, registerRecurringSweepHandler, runRecurringSweepHandlers, type RecurringSweepContext, type RecurringSweepHandler, } from "../jobs/sweep-hooks.js";
101
101
  export { scheduledTriggerAvailability, type ScheduledTriggerAvailability, } from "./agent-chat/recurring-jobs-runtime.js";
102
102
  export { getAppProductionUrl, getFirstPartyProdUrl, resolveAppRuntimeUrl, } from "./app-url.js";
103
103
  export { getConfiguredAppBasePath, normalizeAppBasePath, withConfiguredAppBasePath, } from "./app-base-path.js";
@@ -1,12 +1,14 @@
1
1
  import { getOwnerActiveApiKey } from "../agent/production-agent.js";
2
2
  import { automationMatchesEventOwner, resolveAutomationExecutionIdentity, } from "../automations/service.js";
3
+ import { isProductionServerlessFunctionRuntime } from "../db/client.js";
3
4
  import { subscribe, unsubscribe } from "../event-bus/index.js";
4
5
  import { isBackgroundAutomationRunActive, runBackgroundAutomation, } from "../jobs/background-automation-runner.js";
5
6
  import { buildJobResourceContent, jobBelongsToApp, parseJobResource, patchJobFrontmatterFields, } from "../jobs/frontmatter.js";
7
+ import { registerRecurringSweepHandler, } from "../jobs/sweep-hooks.js";
6
8
  import { resourceGetByPath, resourceListAllOwners, resourcePutIfCurrent, } from "../resources/store.js";
7
9
  import { startIntervalJob } from "../server/interval-job.js";
8
10
  import { evaluateCondition } from "./condition-evaluator.js";
9
- import { AUTOMATION_TRIGGER_EVENT_PURGE_BATCH_SIZE, MAX_AUTOMATION_TRIGGER_EVENT_FAILURES, claimNextAutomationTriggerEvent, completeAutomationTriggerEvent, enqueueAutomationTriggerEvent, ensureAutomationTriggerEventQueue, failAutomationTriggerEvent, listReadyAutomationTriggerIds, purgeExpiredAutomationTriggerEvents, retryAutomationTriggerEvent, } from "./event-queue.js";
11
+ import { AUTOMATION_TRIGGER_EVENT_EXPIRY_BATCH_SIZE, AUTOMATION_TRIGGER_EVENT_PURGE_BATCH_SIZE, MAX_AUTOMATION_TRIGGER_EVENT_FAILURES, claimNextAutomationTriggerEvent, completeAutomationTriggerEvent, enqueueAutomationTriggerEvent, ensureAutomationTriggerEventQueue, expireAutomationTriggerEvent, expireStaleAutomationTriggerEvents, failAutomationTriggerEvent, getAutomationTriggerSweepCursor, listReadyAutomationTriggerIds, purgeExpiredAutomationTriggerEvents, reserveAutomationTriggerEventPurge, retryAutomationTriggerEvent, scheduleAutomationTriggerEventPurge, setAutomationTriggerSweepCursor, } from "./event-queue.js";
10
12
  export function parseTriggerFrontmatter(content) {
11
13
  const { meta, body } = parseJobResource(content);
12
14
  return {
@@ -26,9 +28,16 @@ const _dispatchingTriggers = new Set();
26
28
  const _drainingTriggers = new Map();
27
29
  const MAX_TRIGGER_PAYLOAD_PROMPT_CHARS = 4_000;
28
30
  const MAX_TRIGGER_META_CHARS = 200;
31
+ const DURABLE_TRIGGER_MAX_EVENT_RUN_MS = 60_000;
32
+ const DURABLE_TRIGGER_RUN_CLEANUP_RESERVE_MS = 30_000;
33
+ const DURABLE_TRIGGER_DRAIN_CONCURRENCY = 4;
34
+ const DURABLE_TRIGGER_READY_PAGE_SIZE = 100;
35
+ const MIN_DURABLE_TRIGGER_RUN_MS = 1_000;
36
+ const DB_QUERY_TIMEOUT_MS = 15_000;
37
+ const MAX_MAIL_TRIGGER_EVENT_AGE_MS = 60 * 60_000;
38
+ const MAIL_RECEIVED_EVENT = "mail.message.received";
29
39
  let _deps = null;
30
40
  let _triggerQueueWorkerStarted = false;
31
- let _nextTriggerQueueCleanupAt = 0;
32
41
  export function buildAutomationTriggerPrompt(input) {
33
42
  let payloadStr;
34
43
  try {
@@ -109,27 +118,21 @@ export async function initTriggerDispatcher(deps) {
109
118
  _deps = deps;
110
119
  await ensureAutomationTriggerEventQueue();
111
120
  await refreshEventSubscriptions();
121
+ registerRecurringSweepHandler("automation-trigger-queue", async (context) => {
122
+ await drainReadyTriggerQueue(context);
123
+ });
112
124
  startTriggerQueueWorker();
113
125
  }
114
126
  function startTriggerQueueWorker() {
115
- if (_triggerQueueWorkerStarted)
127
+ if (_triggerQueueWorkerStarted || isProductionServerlessFunctionRuntime()) {
116
128
  return;
129
+ }
117
130
  _triggerQueueWorkerStarted = true;
118
131
  startIntervalJob(async (signal) => {
119
132
  const deps = _deps;
120
133
  if (!deps || signal.aborted)
121
134
  return;
122
- const triggerIds = await listReadyAutomationTriggerIds(deps.appId, 100);
123
- for (const triggerId of triggerIds)
124
- startTriggerDrain(triggerId);
125
- if (Date.now() >= _nextTriggerQueueCleanupAt) {
126
- const purged = await purgeExpiredAutomationTriggerEvents();
127
- _nextTriggerQueueCleanupAt =
128
- Date.now() +
129
- (purged === AUTOMATION_TRIGGER_EVENT_PURGE_BATCH_SIZE
130
- ? 60_000
131
- : 24 * 60 * 60_000);
132
- }
135
+ await drainReadyTriggerQueue();
133
136
  }, {
134
137
  intervalMs: 10_000,
135
138
  timeoutMs: 10_000,
@@ -137,6 +140,106 @@ function startTriggerQueueWorker() {
137
140
  onError: (error) => console.error("[triggers] Event queue recovery scan failed:", error),
138
141
  });
139
142
  }
143
+ async function drainReadyTriggerQueue(context) {
144
+ const deps = _deps;
145
+ if (!deps)
146
+ return;
147
+ if (!context) {
148
+ const triggerIds = await listReadyAutomationTriggerIds(deps.appId, DURABLE_TRIGGER_READY_PAGE_SIZE);
149
+ for (const triggerId of triggerIds)
150
+ void startTriggerDrain(triggerId);
151
+ return;
152
+ }
153
+ const deadline = context.deadlineAt;
154
+ let expiredCount = 0;
155
+ if (deps.appId === "mail") {
156
+ while (Date.now() + DB_QUERY_TIMEOUT_MS < deadline) {
157
+ const expired = await expireStaleAutomationTriggerEvents({
158
+ appId: deps.appId,
159
+ eventName: MAIL_RECEIVED_EVENT,
160
+ emittedBefore: new Date(Date.now() - MAX_MAIL_TRIGGER_EVENT_AGE_MS).toISOString(),
161
+ reason: "Expired because the mail event was older than 60 minutes.",
162
+ });
163
+ expiredCount += expired;
164
+ if (expired < AUTOMATION_TRIGGER_EVENT_EXPIRY_BATCH_SIZE)
165
+ break;
166
+ }
167
+ }
168
+ if (expiredCount > 0) {
169
+ console.info(`[triggers] Expired ${expiredCount} stale ${MAIL_RECEIVED_EVENT} events.`);
170
+ }
171
+ let reclaimedExpiredCount = 0;
172
+ let cycleStart = await getAutomationTriggerSweepCursor(deps.appId);
173
+ let cursor = cycleStart;
174
+ let wrapped = false;
175
+ let madeProgress = false;
176
+ try {
177
+ while (Date.now() +
178
+ DURABLE_TRIGGER_RUN_CLEANUP_RESERVE_MS +
179
+ MIN_DURABLE_TRIGGER_RUN_MS <
180
+ deadline) {
181
+ const ready = await listReadyAutomationTriggerIds(deps.appId, DURABLE_TRIGGER_READY_PAGE_SIZE, {
182
+ ...(cursor === null ? {} : { afterTriggerId: cursor }),
183
+ ...(wrapped && cycleStart !== null
184
+ ? { throughTriggerId: cycleStart }
185
+ : {}),
186
+ });
187
+ if (ready.length === 0) {
188
+ if (!wrapped && cycleStart !== null) {
189
+ wrapped = true;
190
+ cursor = null;
191
+ continue;
192
+ }
193
+ if (madeProgress) {
194
+ cycleStart = await getAutomationTriggerSweepCursor(deps.appId);
195
+ cursor = cycleStart;
196
+ wrapped = false;
197
+ madeProgress = false;
198
+ continue;
199
+ }
200
+ break;
201
+ }
202
+ const batch = ready.slice(0, DURABLE_TRIGGER_DRAIN_CONCURRENCY);
203
+ cursor = batch.at(-1) ?? cursor;
204
+ if (cursor !== null) {
205
+ await setAutomationTriggerSweepCursor(deps.appId, cursor);
206
+ }
207
+ const outcomes = await Promise.allSettled(batch.map((triggerId) => startTriggerDrain(triggerId, {
208
+ maxEvents: 1,
209
+ skipExisting: true,
210
+ expireStaleMailEvents: true,
211
+ deadline,
212
+ onStaleEventExpired: () => {
213
+ reclaimedExpiredCount += 1;
214
+ },
215
+ })));
216
+ const failures = outcomes.flatMap((outcome) => outcome.status === "rejected" ? [outcome.reason] : []);
217
+ if (failures.length === 1)
218
+ throw failures[0];
219
+ if (failures.length > 1) {
220
+ throw new AggregateError(failures, "Failed to drain queued automation trigger events.");
221
+ }
222
+ madeProgress ||= outcomes.some((outcome) => outcome.status === "fulfilled" && outcome.value);
223
+ }
224
+ }
225
+ finally {
226
+ if (reclaimedExpiredCount > 0) {
227
+ console.info(`[triggers] Expired ${reclaimedExpiredCount} stale ${MAIL_RECEIVED_EVENT} events from reclaimed claims.`);
228
+ }
229
+ }
230
+ if (Date.now() + DB_QUERY_TIMEOUT_MS * 3 < deadline) {
231
+ const reserved = await reserveAutomationTriggerEventPurge(deps.appId);
232
+ if (reserved && Date.now() + DB_QUERY_TIMEOUT_MS * 2 < deadline) {
233
+ const purged = await purgeExpiredAutomationTriggerEvents();
234
+ if (Date.now() + DB_QUERY_TIMEOUT_MS < deadline) {
235
+ await scheduleAutomationTriggerEventPurge(deps.appId, Date.now() +
236
+ (purged === AUTOMATION_TRIGGER_EVENT_PURGE_BATCH_SIZE
237
+ ? 60_000
238
+ : 24 * 60 * 60_000));
239
+ }
240
+ }
241
+ }
242
+ }
140
243
  export async function refreshEventSubscriptions() {
141
244
  try {
142
245
  const jobResources = await resourceListAllOwners("jobs/");
@@ -198,7 +301,9 @@ async function handleEvent(eventName, payload, eventMeta) {
198
301
  eventOwner: eventMeta.owner,
199
302
  emittedAt: eventMeta.emittedAt,
200
303
  });
201
- startTriggerDrain(resource.id);
304
+ if (!isProductionServerlessFunctionRuntime()) {
305
+ void startTriggerDrain(resource.id);
306
+ }
202
307
  }
203
308
  }
204
309
  catch (err) {
@@ -206,37 +311,57 @@ async function handleEvent(eventName, payload, eventMeta) {
206
311
  throw err;
207
312
  }
208
313
  }
209
- function startTriggerDrain(triggerId) {
210
- if (_drainingTriggers.has(triggerId))
211
- return;
212
- const drain = drainTriggerQueue(triggerId)
213
- .catch((error) => {
214
- console.error(`[triggers] Failed to drain queued events for trigger ${triggerId}:`, error);
215
- })
216
- .finally(() => {
217
- if (_drainingTriggers.get(triggerId) === drain) {
314
+ function startTriggerDrain(triggerId, options) {
315
+ const existing = _drainingTriggers.get(triggerId);
316
+ if (existing)
317
+ return options?.skipExisting ? Promise.resolve(false) : existing;
318
+ const drain = drainTriggerQueue(triggerId, options?.maxEvents, options?.expireStaleMailEvents, options?.deadline, options?.onStaleEventExpired).finally(() => {
319
+ if (_drainingTriggers.get(triggerId) === drain)
218
320
  _drainingTriggers.delete(triggerId);
219
- }
321
+ });
322
+ void drain.catch((error) => {
323
+ console.error(`[triggers] Failed to drain queued events for trigger ${triggerId}:`, error);
220
324
  });
221
325
  _drainingTriggers.set(triggerId, drain);
326
+ return drain;
222
327
  }
223
- async function drainTriggerQueue(triggerId) {
224
- for (;;) {
328
+ async function drainTriggerQueue(triggerId, maxEvents = Number.POSITIVE_INFINITY, expireStaleMailEvents = false, deadline, onStaleEventExpired) {
329
+ let processedEvents = 0;
330
+ while (processedEvents < maxEvents) {
331
+ if (deadline !== undefined &&
332
+ Date.now() +
333
+ DURABLE_TRIGGER_RUN_CLEANUP_RESERVE_MS +
334
+ MIN_DURABLE_TRIGGER_RUN_MS >=
335
+ deadline) {
336
+ return processedEvents > 0;
337
+ }
225
338
  const deps = _deps;
226
339
  if (!deps)
227
- return;
340
+ return processedEvents > 0;
228
341
  const queued = await claimNextAutomationTriggerEvent(triggerId, deps.appId);
229
342
  if (!queued)
230
- return;
343
+ return processedEvents > 0;
344
+ processedEvents += 1;
345
+ if (expireStaleMailEvents &&
346
+ deps.appId === "mail" &&
347
+ queued.eventName === MAIL_RECEIVED_EVENT &&
348
+ Date.parse(queued.emittedAt) < Date.now() - MAX_MAIL_TRIGGER_EVENT_AGE_MS) {
349
+ await expireAutomationTriggerEvent(queued.id, queued.claimedAt, queued.attempts, "Expired because the mail event was older than 60 minutes.");
350
+ onStaleEventExpired?.();
351
+ continue;
352
+ }
231
353
  if (queued.failureAttempts >= MAX_AUTOMATION_TRIGGER_EVENT_FAILURES) {
232
354
  await failAutomationTriggerEvent(queued.id, queued.claimedAt, queued.attempts, queued.failureAttempts, new Error("Automation event exceeded its retry limit after worker crashes."));
233
- return;
355
+ return true;
234
356
  }
357
+ const hardDeadlineAt = deadline === undefined
358
+ ? undefined
359
+ : Math.min(Date.now() + DURABLE_TRIGGER_MAX_EVENT_RUN_MS, deadline - DURABLE_TRIGGER_RUN_CLEANUP_RESERVE_MS);
235
360
  try {
236
- const result = await dispatchQueuedAutomationEvent(queued, deps);
361
+ const result = await dispatchQueuedAutomationEvent(queued, deps, hardDeadlineAt);
237
362
  if (result === "retry") {
238
363
  await retryAutomationTriggerEvent(queued.id, queued.claimedAt, queued.attempts, queued.failureAttempts, "Automation trigger is busy; the event remains queued.", { delayMs: 5_000, countFailure: false });
239
- return;
364
+ return true;
240
365
  }
241
366
  await completeAutomationTriggerEvent(queued.id, queued.claimedAt, queued.attempts);
242
367
  }
@@ -250,11 +375,12 @@ async function drainTriggerQueue(triggerId) {
250
375
  await retryAutomationTriggerEvent(queued.id, queued.claimedAt, queued.attempts, queued.failureAttempts, error);
251
376
  console.error(`[triggers] Queued event ${queued.eventId} will be retried:`, error);
252
377
  }
253
- return;
378
+ return true;
254
379
  }
255
380
  }
381
+ return processedEvents > 0;
256
382
  }
257
- async function dispatchQueuedAutomationEvent(queued, deps) {
383
+ async function dispatchQueuedAutomationEvent(queued, deps, hardDeadlineAt) {
258
384
  const resource = await resourceGetByPath(queued.triggerOwner, queued.triggerPath);
259
385
  if (!resource || resource.id !== queued.triggerId) {
260
386
  return "completed";
@@ -321,13 +447,17 @@ async function dispatchQueuedAutomationEvent(queued, deps) {
321
447
  const dispatchKey = `${resource.owner}:${resource.path}`;
322
448
  if (_dispatchingTriggers.has(dispatchKey))
323
449
  return "retry";
450
+ if (hardDeadlineAt !== undefined &&
451
+ hardDeadlineAt - Date.now() < MIN_DURABLE_TRIGGER_RUN_MS) {
452
+ return "retry";
453
+ }
324
454
  _dispatchingTriggers.add(dispatchKey);
325
455
  try {
326
456
  const dispatched = await dispatchAgentic(resource, queued.payload, {
327
457
  eventId: queued.eventId,
328
458
  emittedAt: queued.emittedAt,
329
459
  owner: queued.eventOwner,
330
- }, identity);
460
+ }, identity, hardDeadlineAt);
331
461
  return dispatched ? "completed" : "retry";
332
462
  }
333
463
  finally {
@@ -399,7 +529,7 @@ export async function dispatchAutomationWebhookTask(task) {
399
529
  }
400
530
  return "completed";
401
531
  }
402
- async function dispatchAgentic(resource, payload, eventMeta, identity) {
532
+ async function dispatchAgentic(resource, payload, eventMeta, identity, hardDeadlineAt) {
403
533
  if (!_deps)
404
534
  return false;
405
535
  const triggerName = resource.path.replace(/^jobs\//, "").replace(/\.md$/, "");
@@ -494,6 +624,7 @@ async function dispatchAgentic(resource, payload, eventMeta, identity) {
494
624
  triggerName,
495
625
  policyId: runningMeta.delegatedPolicyId,
496
626
  },
627
+ ...(hardDeadlineAt === undefined ? {} : { hardDeadlineAt }),
497
628
  }, _deps);
498
629
  await recordTriggerExecutionOutcome(latest, {
499
630
  lastStatus: "success",
@@ -1,6 +1,7 @@
1
1
  import { type MigrationEntry } from "../db/migrations.js";
2
2
  export declare const AUTOMATION_TRIGGER_EVENT_DEDUPE_RETENTION_MS: number;
3
3
  export declare const AUTOMATION_TRIGGER_EVENT_PURGE_BATCH_SIZE = 1000;
4
+ export declare const AUTOMATION_TRIGGER_EVENT_EXPIRY_BATCH_SIZE = 1000;
4
5
  export declare const MAX_AUTOMATION_TRIGGER_EVENT_FAILURES = 8;
5
6
  declare const CLAIM_LEASE_MS: () => number;
6
7
  export declare const AUTOMATION_TRIGGER_EVENT_MIGRATIONS: MigrationEntry[];
@@ -37,9 +38,24 @@ export declare function enqueueAutomationTriggerEvent(input: EnqueueAutomationTr
37
38
  id: string;
38
39
  inserted: boolean;
39
40
  }>;
40
- export declare function listReadyAutomationTriggerIds(appId?: string | null, limit?: number): Promise<string[]>;
41
+ export declare function listReadyAutomationTriggerIds(appId?: string | null, limit?: number, cursor?: {
42
+ afterTriggerId?: string;
43
+ throughTriggerId?: string;
44
+ }): Promise<string[]>;
45
+ export declare function reserveAutomationTriggerEventPurge(appId?: string | null, now?: number): Promise<boolean>;
46
+ export declare function scheduleAutomationTriggerEventPurge(appId: string | null | undefined, nextAt: number): Promise<void>;
47
+ export declare function getAutomationTriggerSweepCursor(appId?: string | null): Promise<string | null>;
48
+ export declare function setAutomationTriggerSweepCursor(appId: string | null | undefined, triggerId: string | null): Promise<void>;
41
49
  export declare function claimNextAutomationTriggerEvent(triggerId: string, appId?: string | null): Promise<QueuedAutomationTriggerEvent | null>;
42
50
  export declare function completeAutomationTriggerEvent(id: string, claimedAt: number, attempts: number): Promise<void>;
51
+ export declare function expireAutomationTriggerEvent(id: string, claimedAt: number, attempts: number, reason: string): Promise<void>;
52
+ export declare function expireStaleAutomationTriggerEvents(input: {
53
+ appId: string;
54
+ eventName: string;
55
+ emittedBefore: string;
56
+ reason: string;
57
+ limit?: number;
58
+ }): Promise<number>;
43
59
  export declare function retryAutomationTriggerEvent(id: string, claimedAt: number, attempts: number, failureAttempts: number, error: unknown, options?: {
44
60
  delayMs?: number;
45
61
  countFailure?: boolean;
@@ -48,6 +64,7 @@ export declare function failAutomationTriggerEvent(id: string, claimedAt: number
48
64
  export declare function purgeExpiredAutomationTriggerEvents(now?: number): Promise<number>;
49
65
  export declare const __automationTriggerEventQueue: {
50
66
  table: string;
67
+ sweepStateTable: string;
51
68
  claimLeaseMs: typeof CLAIM_LEASE_MS;
52
69
  };
53
70
  export {};