@agent-native/core 0.196.0-nightly-20260928165326 → 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 (87) 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/client/auth/AuthPage.js +3 -2
  47. package/dist/client/org/OrgSwitcher.js +37 -25
  48. package/dist/client/settings/UsageSection.js +3 -3
  49. package/dist/client/use-action.js +1 -1
  50. package/dist/collab/awareness.d.ts +2 -2
  51. package/dist/collab/routes.d.ts +1 -1
  52. package/dist/collab/struct-routes.d.ts +1 -1
  53. package/dist/jobs/background-automation-runner.d.ts +2 -0
  54. package/dist/jobs/background-automation-runner.js +20 -1
  55. package/dist/jobs/sweep-hooks.d.ts +6 -2
  56. package/dist/jobs/sweep-hooks.js +17 -9
  57. package/dist/localization/core-messages/ar-SA.js +2 -2
  58. package/dist/localization/core-messages/de-DE.js +2 -2
  59. package/dist/localization/core-messages/en-US.d.ts +2 -2
  60. package/dist/localization/core-messages/en-US.js +2 -2
  61. package/dist/localization/core-messages/es-ES.js +2 -2
  62. package/dist/localization/core-messages/fr-FR.js +2 -2
  63. package/dist/localization/core-messages/hi-IN.js +2 -2
  64. package/dist/localization/core-messages/ja-JP.js +2 -2
  65. package/dist/localization/core-messages/ko-KR.js +2 -2
  66. package/dist/localization/core-messages/pt-BR.js +2 -2
  67. package/dist/localization/core-messages/zh-CN.js +2 -2
  68. package/dist/localization/core-messages/zh-TW.js +2 -2
  69. package/dist/localization/core-messages.d.ts +2 -2
  70. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  71. package/dist/notifications/routes.d.ts +1 -1
  72. package/dist/observability/routes.d.ts +6 -6
  73. package/dist/org/handlers.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-ollama-models-route.d.ts +2 -2
  81. package/dist/server/index.d.ts +1 -1
  82. package/dist/server/realtime-token.d.ts +1 -1
  83. package/dist/server/transcribe-voice.d.ts +1 -1
  84. package/dist/triggers/dispatcher.js +167 -36
  85. package/dist/triggers/event-queue.d.ts +18 -1
  86. package/dist/triggers/event-queue.js +143 -10
  87. package/package.json +4 -4
@@ -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 {};
@@ -4,14 +4,17 @@ import { getDbExec } from "../db/client.js";
4
4
  import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db/ddl-guard.js";
5
5
  import { runMigrations } from "../db/migrations.js";
6
6
  const TABLE = "automation_trigger_event_queue";
7
+ const SWEEP_STATE_TABLE = "automation_trigger_event_queue_sweep_state";
7
8
  const COMPLETED_PAYLOAD = '{"kind":"completed"}';
8
9
  const UNIQUE_INDEX = "idx_automation_trigger_event_queue_dedupe";
9
10
  const ORDER_INDEX = "idx_automation_trigger_event_queue_order";
10
11
  const READY_INDEX = "idx_automation_trigger_event_queue_ready";
11
12
  const COMPLETED_INDEX = "idx_automation_trigger_event_queue_completed";
12
13
  const FAILED_INDEX = "idx_automation_trigger_event_queue_failed";
14
+ const STALE_EVENT_INDEX = "idx_automation_trigger_event_queue_stale_event";
13
15
  export const AUTOMATION_TRIGGER_EVENT_DEDUPE_RETENTION_MS = 7 * 24 * 60 * 60_000;
14
16
  export const AUTOMATION_TRIGGER_EVENT_PURGE_BATCH_SIZE = 1_000;
17
+ export const AUTOMATION_TRIGGER_EVENT_EXPIRY_BATCH_SIZE = 1_000;
15
18
  export const MAX_AUTOMATION_TRIGGER_EVENT_FAILURES = 8;
16
19
  const CLAIM_LEASE_MS = () => Math.ceil(resolveBackgroundRunHardTimeoutMs() * 1.5);
17
20
  const CREATE_TABLE_SQL = `CREATE TABLE IF NOT EXISTS ${TABLE} (
@@ -35,6 +38,12 @@ const CREATE_TABLE_SQL = `CREATE TABLE IF NOT EXISTS ${TABLE} (
35
38
  completed_at BIGINT,
36
39
  last_error TEXT
37
40
  )`;
41
+ const CREATE_SWEEP_STATE_TABLE_SQL = `CREATE TABLE IF NOT EXISTS ${SWEEP_STATE_TABLE} (
42
+ scope_key TEXT PRIMARY KEY,
43
+ last_trigger_id TEXT,
44
+ purge_after BIGINT NOT NULL DEFAULT 0,
45
+ updated_at BIGINT NOT NULL
46
+ )`;
38
47
  export const AUTOMATION_TRIGGER_EVENT_MIGRATIONS = [
39
48
  {
40
49
  version: 1,
@@ -59,6 +68,18 @@ export const AUTOMATION_TRIGGER_EVENT_MIGRATIONS = [
59
68
  CREATE INDEX IF NOT EXISTS ${FAILED_INDEX}
60
69
  ON ${TABLE} (completed_at) WHERE status = 'failed'`,
61
70
  },
71
+ {
72
+ version: 3,
73
+ name: "automation-trigger-event-stale-mail-index",
74
+ sql: `CREATE INDEX IF NOT EXISTS ${STALE_EVENT_INDEX}
75
+ ON ${TABLE} (app_id, event_name, emitted_at)
76
+ WHERE status = 'pending'`,
77
+ },
78
+ {
79
+ version: 4,
80
+ name: "automation-trigger-event-queue-sweep-state",
81
+ sql: CREATE_SWEEP_STATE_TABLE_SQL,
82
+ },
62
83
  ];
63
84
  export async function runAutomationTriggerEventMigrations(nitroApp) {
64
85
  await runMigrations(AUTOMATION_TRIGGER_EVENT_MIGRATIONS, {
@@ -70,6 +91,7 @@ export async function ensureAutomationTriggerEventQueue() {
70
91
  if (!_initPromise) {
71
92
  _initPromise = (async () => {
72
93
  await ensureTableExists(TABLE, CREATE_TABLE_SQL);
94
+ await ensureTableExists(SWEEP_STATE_TABLE, CREATE_SWEEP_STATE_TABLE_SQL);
73
95
  await ensureColumnExists(TABLE, "failure_attempts", `ALTER TABLE ${TABLE}
74
96
  ADD COLUMN IF NOT EXISTS failure_attempts BIGINT NOT NULL DEFAULT 0`);
75
97
  await ensureIndexExists(UNIQUE_INDEX, `CREATE UNIQUE INDEX IF NOT EXISTS ${UNIQUE_INDEX}
@@ -80,6 +102,9 @@ export async function ensureAutomationTriggerEventQueue() {
80
102
  ON ${TABLE} (app_id, status, available_at, claimed_at)`);
81
103
  await ensureIndexExists(FAILED_INDEX, `CREATE INDEX IF NOT EXISTS ${FAILED_INDEX}
82
104
  ON ${TABLE} (completed_at) WHERE status = 'failed'`);
105
+ await ensureIndexExists(STALE_EVENT_INDEX, `CREATE INDEX IF NOT EXISTS ${STALE_EVENT_INDEX}
106
+ ON ${TABLE} (app_id, event_name, emitted_at)
107
+ WHERE status = 'pending'`);
83
108
  await ensureIndexExists(COMPLETED_INDEX, `CREATE INDEX IF NOT EXISTS ${COMPLETED_INDEX}
84
109
  ON ${TABLE} (completed_at) WHERE status = 'completed'`);
85
110
  })().catch((error) => {
@@ -174,29 +199,89 @@ function appScopePredicate(appId) {
174
199
  ? { sql: "app_id = ?", args: [appId.trim()] }
175
200
  : { sql: "app_id IS NULL", args: [] };
176
201
  }
177
- export async function listReadyAutomationTriggerIds(appId, limit = 100) {
202
+ export async function listReadyAutomationTriggerIds(appId, limit = 100, cursor = {}) {
178
203
  await ensureAutomationTriggerEventQueue();
179
204
  const now = Date.now();
180
205
  const scope = appScopePredicate(appId);
206
+ const afterClause = cursor.afterTriggerId === undefined ? "" : "AND trigger_id > ?";
207
+ const throughClause = cursor.throughTriggerId === undefined ? "" : "AND trigger_id <= ?";
208
+ const args = [
209
+ ...scope.args,
210
+ now,
211
+ now - CLAIM_LEASE_MS(),
212
+ ...(cursor.afterTriggerId === undefined ? [] : [cursor.afterTriggerId]),
213
+ ...(cursor.throughTriggerId === undefined ? [] : [cursor.throughTriggerId]),
214
+ Math.max(1, Math.min(limit, 500)),
215
+ ];
181
216
  const { rows } = await getDbExec().execute({
182
- sql: `SELECT trigger_id, MIN(sequence_id) AS first_sequence
217
+ sql: `SELECT DISTINCT trigger_id
183
218
  FROM ${TABLE}
184
219
  WHERE ${scope.sql}
185
220
  AND ((status = 'pending' AND available_at <= ?)
186
221
  OR (status = 'processing' AND
187
222
  (claimed_at IS NULL OR claimed_at <= ?)))
188
- GROUP BY trigger_id
189
- ORDER BY first_sequence ASC
223
+ ${afterClause}
224
+ ${throughClause}
225
+ ORDER BY trigger_id ASC
190
226
  LIMIT ?`,
191
- args: [
192
- ...scope.args,
193
- now,
194
- now - CLAIM_LEASE_MS(),
195
- Math.max(1, Math.min(limit, 500)),
196
- ],
227
+ args,
197
228
  });
198
229
  return rows.map((row) => String(row.trigger_id));
199
230
  }
231
+ export async function reserveAutomationTriggerEventPurge(appId, now = Date.now()) {
232
+ await ensureAutomationTriggerEventQueue();
233
+ const { rows } = await getDbExec().execute({
234
+ sql: `INSERT INTO ${SWEEP_STATE_TABLE}
235
+ (scope_key, last_trigger_id, purge_after, updated_at)
236
+ VALUES (?, NULL, ?, ?)
237
+ ON CONFLICT (scope_key) DO UPDATE
238
+ SET purge_after = excluded.purge_after,
239
+ updated_at = excluded.updated_at
240
+ WHERE ${SWEEP_STATE_TABLE}.purge_after <= ?
241
+ RETURNING scope_key`,
242
+ args: [appId?.trim() ?? "", now + 60_000, now, now],
243
+ });
244
+ return rows.length > 0;
245
+ }
246
+ export async function scheduleAutomationTriggerEventPurge(appId, nextAt) {
247
+ await ensureAutomationTriggerEventQueue();
248
+ await getDbExec().execute({
249
+ sql: `UPDATE ${SWEEP_STATE_TABLE}
250
+ SET purge_after = ?, updated_at = ?
251
+ WHERE scope_key = ?`,
252
+ args: [nextAt, Date.now(), appId?.trim() ?? ""],
253
+ });
254
+ }
255
+ export async function getAutomationTriggerSweepCursor(appId) {
256
+ await ensureAutomationTriggerEventQueue();
257
+ const { rows } = await getDbExec().execute({
258
+ sql: `SELECT last_trigger_id FROM ${SWEEP_STATE_TABLE} WHERE scope_key = ?`,
259
+ args: [appId?.trim() ?? ""],
260
+ });
261
+ const cursor = rows[0]
262
+ ?.last_trigger_id;
263
+ return cursor == null ? null : String(cursor);
264
+ }
265
+ export async function setAutomationTriggerSweepCursor(appId, triggerId) {
266
+ await ensureAutomationTriggerEventQueue();
267
+ const scopeKey = appId?.trim() ?? "";
268
+ if (triggerId === null) {
269
+ await getDbExec().execute({
270
+ sql: `DELETE FROM ${SWEEP_STATE_TABLE} WHERE scope_key = ?`,
271
+ args: [scopeKey],
272
+ });
273
+ return;
274
+ }
275
+ await getDbExec().execute({
276
+ sql: `INSERT INTO ${SWEEP_STATE_TABLE}
277
+ (scope_key, last_trigger_id, updated_at)
278
+ VALUES (?, ?, ?)
279
+ ON CONFLICT (scope_key) DO UPDATE
280
+ SET last_trigger_id = excluded.last_trigger_id,
281
+ updated_at = excluded.updated_at`,
282
+ args: [scopeKey, triggerId, Date.now()],
283
+ });
284
+ }
200
285
  export async function claimNextAutomationTriggerEvent(triggerId, appId) {
201
286
  await ensureAutomationTriggerEventQueue();
202
287
  const now = Date.now();
@@ -247,6 +332,53 @@ export async function completeAutomationTriggerEvent(id, claimedAt, attempts) {
247
332
  args: [COMPLETED_PAYLOAD, Date.now(), id, claimedAt, attempts],
248
333
  });
249
334
  }
335
+ export async function expireAutomationTriggerEvent(id, claimedAt, attempts, reason) {
336
+ await ensureAutomationTriggerEventQueue();
337
+ await getDbExec().execute({
338
+ sql: `UPDATE ${TABLE}
339
+ SET status = 'completed', payload = ?, claimed_at = NULL,
340
+ completed_at = ?, last_error = ?
341
+ WHERE id = ? AND status = 'processing'
342
+ AND claimed_at = ? AND attempts = ?`,
343
+ args: [
344
+ COMPLETED_PAYLOAD,
345
+ Date.now(),
346
+ reason.slice(0, 500),
347
+ id,
348
+ claimedAt,
349
+ attempts,
350
+ ],
351
+ });
352
+ }
353
+ export async function expireStaleAutomationTriggerEvents(input) {
354
+ await ensureAutomationTriggerEventQueue();
355
+ const limit = Math.max(1, Math.min(input.limit ?? AUTOMATION_TRIGGER_EVENT_EXPIRY_BATCH_SIZE, 10_000));
356
+ const { rowsAffected } = await getDbExec().execute({
357
+ sql: `WITH expired AS (
358
+ SELECT id FROM ${TABLE}
359
+ WHERE app_id = ? AND event_name = ? AND status = 'pending'
360
+ AND emitted_at < ?
361
+ ORDER BY emitted_at ASC
362
+ LIMIT ?
363
+ FOR UPDATE SKIP LOCKED
364
+ )
365
+ UPDATE ${TABLE} AS queued
366
+ SET status = 'completed', payload = ?, claimed_at = NULL,
367
+ completed_at = ?, last_error = ?
368
+ FROM expired
369
+ WHERE queued.id = expired.id`,
370
+ args: [
371
+ input.appId,
372
+ input.eventName,
373
+ input.emittedBefore,
374
+ limit,
375
+ COMPLETED_PAYLOAD,
376
+ Date.now(),
377
+ input.reason.slice(0, 500),
378
+ ],
379
+ });
380
+ return rowsAffected;
381
+ }
250
382
  export async function retryAutomationTriggerEvent(id, claimedAt, attempts, failureAttempts, error, options = {}) {
251
383
  await ensureAutomationTriggerEventQueue();
252
384
  const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
@@ -313,5 +445,6 @@ export async function purgeExpiredAutomationTriggerEvents(now = Date.now()) {
313
445
  }
314
446
  export const __automationTriggerEventQueue = {
315
447
  table: TABLE,
448
+ sweepStateTable: SWEEP_STATE_TABLE,
316
449
  claimLeaseMs: CLAIM_LEASE_MS,
317
450
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.196.0-nightly-20260928165326",
3
+ "version": "0.196.0",
4
4
  "description": "The agentic application framework for building autonomous agents with intuitive UIs",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -533,9 +533,9 @@
533
533
  "yaml": "^2.9.0",
534
534
  "yjs": "^13.6.32",
535
535
  "zod": "^4.3.6",
536
- "@agent-native/recap-cli": "0.5.47",
537
- "@agent-native/agentkit": "^0.5.0-nightly-20260928165326",
538
- "@agent-native/toolkit": "^0.23.0-nightly-20260928165326"
536
+ "@agent-native/agentkit": "^0.5.0",
537
+ "@agent-native/recap-cli": "0.5.48",
538
+ "@agent-native/toolkit": "^0.23.0"
539
539
  },
540
540
  "devDependencies": {
541
541
  "@ai-sdk/anthropic": "^3.0.71",