@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.
- 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/client/auth/AuthPage.js +3 -2
- package/dist/client/org/OrgSwitcher.js +37 -25
- 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/collab/struct-routes.d.ts +1 -1
- 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/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +6 -6
- package/dist/org/handlers.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-ollama-models-route.d.ts +2 -2
- package/dist/server/index.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
189
|
-
|
|
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
|
|
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/
|
|
537
|
-
"@agent-native/
|
|
538
|
-
"@agent-native/toolkit": "^0.23.0
|
|
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",
|