@agent-native/core 0.204.0 → 0.204.1-nightly-20261008034100
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/dist/action.js +5 -6
- package/dist/agent/auto-continue.d.ts +35 -0
- package/dist/agent/auto-continue.js +38 -0
- package/dist/agent/connection-required-note.d.ts +52 -0
- package/dist/agent/connection-required-note.js +187 -0
- package/dist/agent/jev-tool-prefetch.d.ts +5 -1
- package/dist/agent/jev-tool-prefetch.js +19 -6
- package/dist/agent/previous-turn-note.d.ts +9 -0
- package/dist/agent/previous-turn-note.js +48 -0
- package/dist/agent/production-agent.d.ts +17 -13
- package/dist/agent/production-agent.js +185 -43
- package/dist/agent/reaped-run-thread.d.ts +13 -0
- package/dist/agent/reaped-run-thread.js +45 -0
- package/dist/agent/run-store.d.ts +29 -5
- package/dist/agent/run-store.js +144 -30
- package/dist/agent/thread-data-builder.d.ts +16 -0
- package/dist/agent/thread-data-builder.js +30 -8
- package/dist/agent/thread-message-projection.js +5 -3
- package/dist/agent/tool-search.js +5 -1
- package/dist/agent/types.d.ts +3 -0
- package/dist/audit/config.d.ts +6 -0
- package/dist/audit/config.js +12 -1
- package/dist/audit/index.d.ts +1 -1
- package/dist/audit/index.js +1 -1
- package/dist/audit/store.js +39 -4
- package/dist/cli/skills.d.ts +1 -0
- package/dist/cli/skills.js +1 -1
- package/dist/client/chat/agentkit-agent-native.js +83 -7
- package/dist/client/chat/agentkit-protocol.js +140 -67
- package/dist/client/chat/runtime.js +5 -3
- package/dist/client/use-run-stuck-detection.d.ts +18 -1
- package/dist/client/use-run-stuck-detection.js +115 -75
- package/dist/db/request-telemetry.d.ts +7 -1
- package/dist/db/request-telemetry.js +20 -1
- package/dist/deploy/build.d.ts +25 -1
- package/dist/deploy/build.js +176 -3
- package/dist/deploy/workspace-deploy.js +8 -3
- package/dist/extensions/fetch-tool.js +50 -14
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/integrations/adapters/email.js +11 -3
- package/dist/integrations/adapters/telegram.js +4 -2
- package/dist/jobs/actions/automation-no-op.d.ts +12 -0
- package/dist/jobs/actions/automation-no-op.js +20 -0
- package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
- package/dist/jobs/background-automation-runner.d.ts +8 -1
- package/dist/jobs/background-automation-runner.js +102 -19
- package/dist/jobs/run-history.d.ts +1 -1
- package/dist/jobs/run-history.js +8 -4
- package/dist/jobs/scheduler.js +25 -12
- package/dist/localization/automation-outcome-messages.d.ts +22 -0
- package/dist/localization/automation-outcome-messages.js +90 -0
- package/dist/localization/default-messages.d.ts +2 -0
- package/dist/localization/default-messages.js +4 -2
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/metrics.d.ts +3 -0
- package/dist/observability/metrics.js +10 -0
- package/dist/observability/migrations.js +22 -0
- package/dist/observability/routes.d.ts +13 -8
- package/dist/observability/routes.js +88 -14
- package/dist/observability/sentiment.d.ts +5 -0
- package/dist/observability/sentiment.js +75 -25
- package/dist/observability/store.d.ts +28 -0
- package/dist/observability/store.js +58 -0
- package/dist/observability/traces.js +37 -5
- package/dist/org/index.d.ts +2 -2
- package/dist/progress/actions.js +1 -0
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.js +6 -2
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/provider-api/index.d.ts +1 -0
- package/dist/provider-api/index.js +18 -0
- package/dist/provider-api/result-outcome.d.ts +1 -0
- package/dist/provider-api/result-outcome.js +20 -0
- package/dist/provider-api/staging.js +10 -9
- package/dist/server/agent-chat/prompt-resources.d.ts +8 -2
- package/dist/server/agent-chat/prompt-resources.js +30 -7
- package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +19 -4
- package/dist/server/agent-chat/recurring-jobs-runtime.js +52 -16
- package/dist/server/agent-chat/sweep-trigger-auth.d.ts +18 -0
- package/dist/server/agent-chat/sweep-trigger-auth.js +36 -0
- package/dist/server/agent-chat-plugin.js +20 -13
- package/dist/server/agent-engine-default-model-route.d.ts +2 -2
- package/dist/server/auth.js +4 -4
- package/dist/server/framework-request-handler.js +19 -0
- package/dist/server/http-response-telemetry.js +4 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/request-context.d.ts +6 -0
- package/dist/shared/context-status.d.ts +21 -0
- package/dist/shared/context-status.js +54 -0
- package/dist/templates/chat/app/components/chat/ChatRouteContent.tsx +8 -1
- package/dist/templates/workspace-core/.agents/skills/automations/SKILL.md +8 -1
- package/dist/templates/workspace-core/.agents/skills/recurring-jobs/SKILL.md +3 -1
- package/dist/triggers/actions/manage-automation.d.ts +7 -7
- package/dist/triggers/dispatcher.js +10 -7
- package/docs/content/audit-log.mdx +3 -3
- package/docs/content/cloudflare.mdx +27 -0
- package/docs/content/deployment-environment-variables.mdx +2 -0
- package/docs/content/durable-resume.mdx +3 -1
- package/docs/content/environment-variables.mdx +2 -0
- package/docs/content/recurring-jobs.mdx +29 -9
- package/docs/content/vercel.mdx +30 -0
- package/package.json +3 -3
- package/src/templates/chat/app/components/chat/ChatRouteContent.tsx +8 -1
- package/src/templates/workspace-core/.agents/skills/automations/SKILL.md +8 -1
- package/src/templates/workspace-core/.agents/skills/recurring-jobs/SKILL.md +3 -1
|
@@ -9,10 +9,15 @@ const DEFAULT_POLL_INTERVAL_MS = 5_000;
|
|
|
9
9
|
const IDLE_BACKOFF_INTERVAL_MS = 15_000;
|
|
10
10
|
const MAX_POLL_ERROR_BACKOFF_MS = 30_000;
|
|
11
11
|
const FRESH_BACKGROUND_HEARTBEAT_MS = 30_000;
|
|
12
|
+
const SETTLED_CONFIRMATIONS = 2;
|
|
13
|
+
const UNREADABLE_AFTER_FAILURES = 2;
|
|
12
14
|
const POLL_ABORT_MIN_MS = 10_000;
|
|
13
15
|
function getPollAbortMs(interval) {
|
|
14
16
|
return Math.max(POLL_ABORT_MIN_MS, interval * 4);
|
|
15
17
|
}
|
|
18
|
+
function isActiveRunResponse(body) {
|
|
19
|
+
return typeof body?.active === "boolean";
|
|
20
|
+
}
|
|
16
21
|
const EMPTY_STATE = {
|
|
17
22
|
isStuck: false,
|
|
18
23
|
runId: null,
|
|
@@ -24,9 +29,27 @@ const EMPTY_STATE = {
|
|
|
24
29
|
heartbeatSinceMs: null,
|
|
25
30
|
dispatchMode: null,
|
|
26
31
|
hasInFlightWork: null,
|
|
32
|
+
serverSettled: false,
|
|
33
|
+
statusUnreadable: false,
|
|
27
34
|
};
|
|
28
|
-
export function useRunStuckDetection({ threadId, enabled = true, stuckThresholdMs = DEFAULT_STUCK_THRESHOLD_MS, backgroundStuckThresholdMs = DEFAULT_BACKGROUND_STUCK_THRESHOLD_MS, liveBackgroundStuckThresholdMs = DEFAULT_LIVE_BACKGROUND_STUCK_THRESHOLD_MS, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS, apiUrl, }) {
|
|
35
|
+
export function useRunStuckDetection({ threadId, enabled = true, awaitingResponse, stuckThresholdMs = DEFAULT_STUCK_THRESHOLD_MS, backgroundStuckThresholdMs = DEFAULT_BACKGROUND_STUCK_THRESHOLD_MS, liveBackgroundStuckThresholdMs = DEFAULT_LIVE_BACKGROUND_STUCK_THRESHOLD_MS, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS, apiUrl, }) {
|
|
29
36
|
const [state, setState] = useState(EMPTY_STATE);
|
|
37
|
+
// Reset in render, not in the effect: an effect-time reset still lets the
|
|
38
|
+
// consumer's own effects act once on the previous answer. The epoch also
|
|
39
|
+
// restarts polling, which drops any poll still in flight from before.
|
|
40
|
+
const awaiting = awaitingResponse === true;
|
|
41
|
+
const [scope, setScope] = useState({ threadId, awaiting, epoch: 0 });
|
|
42
|
+
if (scope.threadId !== threadId || scope.awaiting !== awaiting) {
|
|
43
|
+
const waitingStarted = awaiting && !scope.awaiting;
|
|
44
|
+
setScope({
|
|
45
|
+
threadId,
|
|
46
|
+
awaiting,
|
|
47
|
+
epoch: scope.epoch + (waitingStarted ? 1 : 0),
|
|
48
|
+
});
|
|
49
|
+
if (waitingStarted || scope.threadId !== threadId)
|
|
50
|
+
setState(EMPTY_STATE);
|
|
51
|
+
}
|
|
52
|
+
const awaitingEpoch = scope.epoch;
|
|
30
53
|
useEffect(() => {
|
|
31
54
|
setState(EMPTY_STATE);
|
|
32
55
|
if (!threadId || !enabled)
|
|
@@ -38,18 +61,24 @@ export function useRunStuckDetection({ threadId, enabled = true, stuckThresholdM
|
|
|
38
61
|
let lastObservedLocalProgressAt = null;
|
|
39
62
|
let lastObservedLocalProgressSeq = null;
|
|
40
63
|
let consecutivePollFailures = 0;
|
|
64
|
+
let consecutiveIdlePolls = 0;
|
|
41
65
|
const nextDelayRef = { current: 2_000 };
|
|
42
66
|
const pollFailureDelay = () => {
|
|
43
67
|
consecutivePollFailures += 1;
|
|
68
|
+
// A poll that got no answer breaks an idle streak still being confirmed:
|
|
69
|
+
// a run can start and be missed between two idle answers that surround
|
|
70
|
+
// it. A streak that already settled stands, or the verdict would flip
|
|
71
|
+
// off and on again across one outage.
|
|
72
|
+
if (consecutiveIdlePolls < SETTLED_CONFIRMATIONS) {
|
|
73
|
+
consecutiveIdlePolls = 0;
|
|
74
|
+
}
|
|
75
|
+
if (!cancelled && consecutivePollFailures >= UNREADABLE_AFTER_FAILURES) {
|
|
76
|
+
setState((current) => current.statusUnreadable
|
|
77
|
+
? current
|
|
78
|
+
: { ...current, statusUnreadable: true });
|
|
79
|
+
}
|
|
44
80
|
return Math.min(Math.max(pollIntervalMs, 1) * 2 ** consecutivePollFailures, MAX_POLL_ERROR_BACKOFF_MS);
|
|
45
81
|
};
|
|
46
|
-
const clearObservedRun = () => {
|
|
47
|
-
snapshotVersion += 1;
|
|
48
|
-
if (snapshotTransitionTimer)
|
|
49
|
-
clearTimeout(snapshotTransitionTimer);
|
|
50
|
-
snapshotTransitionTimer = null;
|
|
51
|
-
setState(EMPTY_STATE);
|
|
52
|
-
};
|
|
53
82
|
const effectiveThresholdFor = (dispatchMode, heartbeatSinceMs) => {
|
|
54
83
|
const liveBackgroundWorker = dispatchMode === "background-processing" &&
|
|
55
84
|
heartbeatSinceMs != null &&
|
|
@@ -180,77 +209,87 @@ export function useRunStuckDetection({ threadId, enabled = true, stuckThresholdM
|
|
|
180
209
|
const res = await fetch(`${base}/runs/active?threadId=${encodeURIComponent(threadId)}`, { credentials: "same-origin", signal });
|
|
181
210
|
if (cancelled)
|
|
182
211
|
return;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const heartbeatAt = data.heartbeatAt ?? null;
|
|
190
|
-
const heartbeatSinceMs = heartbeatAt != null ? nowMs - heartbeatAt : null;
|
|
191
|
-
const dispatchMode = typeof data.dispatchMode === "string" ? data.dispatchMode : null;
|
|
192
|
-
const runId = data.runId ?? null;
|
|
193
|
-
const activeRun = getActiveRun();
|
|
194
|
-
const localProgressObservedAt = activeRun?.threadId === threadId && activeRun.runId === runId
|
|
195
|
-
? (activeRun.lastProgressObservedAt ?? null)
|
|
196
|
-
: null;
|
|
197
|
-
const localProgressSinceMs = localProgressObservedAt == null
|
|
198
|
-
? null
|
|
199
|
-
: Math.max(0, Date.now() - localProgressObservedAt);
|
|
200
|
-
const effectiveThresholdMs = effectiveThresholdFor(dispatchMode, heartbeatSinceMs);
|
|
201
|
-
const isStuck = Boolean(data.active &&
|
|
202
|
-
data.status === "running" &&
|
|
203
|
-
stuckSinceMs != null &&
|
|
204
|
-
stuckSinceMs > effectiveThresholdMs &&
|
|
205
|
-
(localProgressSinceMs == null ||
|
|
206
|
-
localProgressSinceMs > effectiveThresholdMs));
|
|
207
|
-
const observedAtMs = Date.now();
|
|
208
|
-
const version = ++snapshotVersion;
|
|
209
|
-
scheduleSnapshotTransition({
|
|
210
|
-
active: data.active,
|
|
211
|
-
runId,
|
|
212
|
-
status: data.status ?? null,
|
|
213
|
-
lastProgressAt,
|
|
214
|
-
stuckSinceMs,
|
|
215
|
-
localProgressObservedAt,
|
|
216
|
-
lastProgressSeq: activeRun?.threadId === threadId && activeRun.runId === runId
|
|
217
|
-
? (activeRun.lastProgressSeq ?? null)
|
|
218
|
-
: null,
|
|
219
|
-
heartbeatAt,
|
|
220
|
-
heartbeatSinceMs,
|
|
221
|
-
dispatchMode,
|
|
222
|
-
}, observedAtMs, version);
|
|
223
|
-
setState({
|
|
224
|
-
isStuck,
|
|
225
|
-
runId,
|
|
226
|
-
status: data.status ?? null,
|
|
227
|
-
lastProgressAt,
|
|
228
|
-
stuckSinceMs: localProgressSinceMs != null &&
|
|
229
|
-
localProgressSinceMs <= effectiveThresholdMs
|
|
230
|
-
? null
|
|
231
|
-
: stuckSinceMs,
|
|
232
|
-
lastProgressSeq: activeRun?.threadId === threadId && activeRun.runId === runId
|
|
233
|
-
? (activeRun.lastProgressSeq ?? null)
|
|
234
|
-
: null,
|
|
235
|
-
heartbeatAt,
|
|
236
|
-
heartbeatSinceMs,
|
|
237
|
-
dispatchMode,
|
|
238
|
-
hasInFlightWork: typeof data.hasInFlightWork === "boolean"
|
|
239
|
-
? data.hasInFlightWork
|
|
240
|
-
: null,
|
|
241
|
-
});
|
|
242
|
-
if (!data.active || data.status !== "running") {
|
|
243
|
-
nextDelay = IDLE_BACKOFF_INTERVAL_MS;
|
|
244
|
-
}
|
|
212
|
+
// The endpoint answers "no run" with a 200 and a boolean `active`, so
|
|
213
|
+
// no other status or body is an idle answer: a 401/403/404 or a body
|
|
214
|
+
// without `active` leaves the run's state unknown, not gone.
|
|
215
|
+
if (!res.ok) {
|
|
216
|
+
nextDelay = pollFailureDelay();
|
|
217
|
+
return;
|
|
245
218
|
}
|
|
246
|
-
|
|
219
|
+
const data = await res.json();
|
|
220
|
+
if (cancelled)
|
|
221
|
+
return;
|
|
222
|
+
if (!isActiveRunResponse(data)) {
|
|
247
223
|
nextDelay = pollFailureDelay();
|
|
224
|
+
return;
|
|
248
225
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
226
|
+
consecutiveIdlePolls = data.active ? 0 : consecutiveIdlePolls + 1;
|
|
227
|
+
const serverSettled = consecutiveIdlePolls >= SETTLED_CONFIRMATIONS;
|
|
228
|
+
const lastProgressAt = data.lastProgressAt ?? null;
|
|
229
|
+
const nowMs = data.serverNow ?? Date.now();
|
|
230
|
+
const stuckSinceMs = lastProgressAt != null ? nowMs - lastProgressAt : null;
|
|
231
|
+
const heartbeatAt = data.heartbeatAt ?? null;
|
|
232
|
+
const heartbeatSinceMs = heartbeatAt != null ? nowMs - heartbeatAt : null;
|
|
233
|
+
const dispatchMode = typeof data.dispatchMode === "string" ? data.dispatchMode : null;
|
|
234
|
+
const runId = data.runId ?? null;
|
|
235
|
+
const activeRun = getActiveRun();
|
|
236
|
+
const localProgressObservedAt = activeRun?.threadId === threadId && activeRun.runId === runId
|
|
237
|
+
? (activeRun.lastProgressObservedAt ?? null)
|
|
238
|
+
: null;
|
|
239
|
+
const localProgressSinceMs = localProgressObservedAt == null
|
|
240
|
+
? null
|
|
241
|
+
: Math.max(0, Date.now() - localProgressObservedAt);
|
|
242
|
+
const effectiveThresholdMs = effectiveThresholdFor(dispatchMode, heartbeatSinceMs);
|
|
243
|
+
const isStuck = Boolean(data.active &&
|
|
244
|
+
data.status === "running" &&
|
|
245
|
+
stuckSinceMs != null &&
|
|
246
|
+
stuckSinceMs > effectiveThresholdMs &&
|
|
247
|
+
(localProgressSinceMs == null ||
|
|
248
|
+
localProgressSinceMs > effectiveThresholdMs));
|
|
249
|
+
const observedAtMs = Date.now();
|
|
250
|
+
const version = ++snapshotVersion;
|
|
251
|
+
scheduleSnapshotTransition({
|
|
252
|
+
active: data.active,
|
|
253
|
+
runId,
|
|
254
|
+
status: data.status ?? null,
|
|
255
|
+
lastProgressAt,
|
|
256
|
+
stuckSinceMs,
|
|
257
|
+
localProgressObservedAt,
|
|
258
|
+
lastProgressSeq: activeRun?.threadId === threadId && activeRun.runId === runId
|
|
259
|
+
? (activeRun.lastProgressSeq ?? null)
|
|
260
|
+
: null,
|
|
261
|
+
heartbeatAt,
|
|
262
|
+
heartbeatSinceMs,
|
|
263
|
+
dispatchMode,
|
|
264
|
+
}, observedAtMs, version);
|
|
265
|
+
setState({
|
|
266
|
+
isStuck,
|
|
267
|
+
runId,
|
|
268
|
+
status: data.status ?? null,
|
|
269
|
+
lastProgressAt,
|
|
270
|
+
stuckSinceMs: localProgressSinceMs != null &&
|
|
271
|
+
localProgressSinceMs <= effectiveThresholdMs
|
|
272
|
+
? null
|
|
273
|
+
: stuckSinceMs,
|
|
274
|
+
lastProgressSeq: activeRun?.threadId === threadId && activeRun.runId === runId
|
|
275
|
+
? (activeRun.lastProgressSeq ?? null)
|
|
276
|
+
: null,
|
|
277
|
+
heartbeatAt,
|
|
278
|
+
heartbeatSinceMs,
|
|
279
|
+
dispatchMode,
|
|
280
|
+
hasInFlightWork: typeof data.hasInFlightWork === "boolean"
|
|
281
|
+
? data.hasInFlightWork
|
|
282
|
+
: null,
|
|
283
|
+
serverSettled,
|
|
284
|
+
statusUnreadable: false,
|
|
285
|
+
});
|
|
286
|
+
// The first idle answer is confirmed at the normal cadence, so a
|
|
287
|
+
// run the server has not registered yet is not called settled.
|
|
288
|
+
const confirmingIdle = !data.active && !serverSettled;
|
|
289
|
+
if (!confirmingIdle && (!data.active || data.status !== "running")) {
|
|
290
|
+
nextDelay = IDLE_BACKOFF_INTERVAL_MS;
|
|
253
291
|
}
|
|
292
|
+
consecutivePollFailures = 0;
|
|
254
293
|
}
|
|
255
294
|
catch {
|
|
256
295
|
nextDelay = pollFailureDelay();
|
|
@@ -292,6 +331,7 @@ export function useRunStuckDetection({ threadId, enabled = true, stuckThresholdM
|
|
|
292
331
|
liveBackgroundStuckThresholdMs,
|
|
293
332
|
pollIntervalMs,
|
|
294
333
|
apiUrl,
|
|
334
|
+
awaitingEpoch,
|
|
295
335
|
]);
|
|
296
336
|
return state;
|
|
297
337
|
}
|
|
@@ -20,7 +20,13 @@ export interface DatabaseRequestTelemetry {
|
|
|
20
20
|
export declare function createDatabaseRequestTelemetry(): DatabaseRequestTelemetry;
|
|
21
21
|
export declare function claimStartupDatabaseTelemetry(): DatabaseRequestTelemetry | undefined;
|
|
22
22
|
export declare function runWithDatabaseRequestTelemetry<T>(telemetry: DatabaseRequestTelemetry, fn: () => T): T;
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Returns false when the runtime cannot scope telemetry to the rest of the
|
|
25
|
+
* request. workerd's AsyncLocalStorage implements `run()` but throws from
|
|
26
|
+
* `enterWith()`, and a throw here escapes the Nitro request hook and skips
|
|
27
|
+
* every hook after it, including the framework route readiness gate.
|
|
28
|
+
*/
|
|
29
|
+
export declare function enterDatabaseRequestTelemetry(telemetry: DatabaseRequestTelemetry): boolean;
|
|
24
30
|
export declare function beginDatabaseOperation(operation: DatabaseOperationKind): (outcome: "success" | "error" | "timeout") => void;
|
|
25
31
|
export declare function recordDatabaseRetry(): void;
|
|
26
32
|
export declare function recordDatabaseQueryResult(sql: string, rowsReturned: number): void;
|
|
@@ -62,8 +62,27 @@ export function claimStartupDatabaseTelemetry() {
|
|
|
62
62
|
export function runWithDatabaseRequestTelemetry(telemetry, fn) {
|
|
63
63
|
return getStorage().run(telemetry, fn);
|
|
64
64
|
}
|
|
65
|
+
const storagesWithoutEnterWith = new WeakSet();
|
|
66
|
+
/**
|
|
67
|
+
* Returns false when the runtime cannot scope telemetry to the rest of the
|
|
68
|
+
* request. workerd's AsyncLocalStorage implements `run()` but throws from
|
|
69
|
+
* `enterWith()`, and a throw here escapes the Nitro request hook and skips
|
|
70
|
+
* every hook after it, including the framework route readiness gate.
|
|
71
|
+
*/
|
|
65
72
|
export function enterDatabaseRequestTelemetry(telemetry) {
|
|
66
|
-
getStorage()
|
|
73
|
+
const storage = getStorage();
|
|
74
|
+
if (storage === NOOP_STORAGE || storagesWithoutEnterWith.has(storage)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
storage.enterWith(telemetry);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
storagesWithoutEnterWith.add(storage);
|
|
83
|
+
console.warn("[db] Per-request database telemetry is unavailable: AsyncLocalStorage.enterWith() failed on this runtime.", error);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
67
86
|
}
|
|
68
87
|
export function beginDatabaseOperation(operation) {
|
|
69
88
|
const telemetry = currentDatabaseTelemetry();
|
package/dist/deploy/build.d.ts
CHANGED
|
@@ -15,7 +15,15 @@ export declare const CLOUDFLARE_MODULE_WORKER_ENTRY = "worker.mjs";
|
|
|
15
15
|
export declare function configureAwsAmplifyRuntimeOutput(serverDir: string, appDir: string, env?: NodeJS.ProcessEnv): void;
|
|
16
16
|
export declare function configureAwsLambdaRuntimeOutput(serverDir: string, appDir: string, env?: NodeJS.ProcessEnv): void;
|
|
17
17
|
export declare function shimCloudflarePagesModuleTimers(code: string): string;
|
|
18
|
-
export declare
|
|
18
|
+
export declare const CLOUDFLARE_SWEEP_CRON = "* * * * *";
|
|
19
|
+
/**
|
|
20
|
+
* The URL path a platform scheduler requests. Under a custom framework route
|
|
21
|
+
* prefix the request boundary 404s `/_agent-native/*` before any route runs,
|
|
22
|
+
* and the server is mounted under the app base path, so the internal sweep
|
|
23
|
+
* path alone never reaches the sweep.
|
|
24
|
+
*/
|
|
25
|
+
export declare function publicRecurringJobsSweepPath(env?: NodeJS.ProcessEnv): string;
|
|
26
|
+
export declare function generateCloudflareModuleWorkerEntry(env?: NodeJS.ProcessEnv): string;
|
|
19
27
|
export declare function patchCloudflareModuleNitroEntry(code: string): string;
|
|
20
28
|
export declare function configureCloudflareModuleWorkerOutput(serverDir: string): void;
|
|
21
29
|
export declare const NITRO_RUNTIME_IGNORE_PATTERNS: string[];
|
|
@@ -65,6 +73,22 @@ export declare function isRecurringJobsDeployEnabled(): boolean;
|
|
|
65
73
|
export declare function isKeepWarmDeployEnabled(): boolean;
|
|
66
74
|
export declare function isKeepWarmBackgroundDeployEnabled(): boolean;
|
|
67
75
|
export declare function resolveKeepWarmSchedule(): string;
|
|
76
|
+
export declare function resolveVercelSweepCronSchedule(env?: NodeJS.ProcessEnv): string;
|
|
77
|
+
export declare function vercelSweepCrons(sweepPaths: readonly string[], env?: NodeJS.ProcessEnv): Array<{
|
|
78
|
+
path: string;
|
|
79
|
+
schedule: string;
|
|
80
|
+
}>;
|
|
81
|
+
export declare function addVercelSweepCron(outputDir: string, env?: NodeJS.ProcessEnv): void;
|
|
82
|
+
/**
|
|
83
|
+
* Reads back the sweep cron `addVercelSweepCron` wrote for an app mounted at
|
|
84
|
+
* `basePath`. A workspace deploy must reuse it rather than recompute it: the
|
|
85
|
+
* app's build resolved its framework route prefix from config files the
|
|
86
|
+
* workspace build never reads.
|
|
87
|
+
*/
|
|
88
|
+
export declare function readVercelSweepCron(outputDir: string, basePath: string): {
|
|
89
|
+
path: string;
|
|
90
|
+
schedule: string;
|
|
91
|
+
};
|
|
68
92
|
export declare function emitSingleTemplateNetlifyKeepWarmFunction(projectCwd: string): void;
|
|
69
93
|
export declare function emitSingleTemplateNetlifyRecurringJobsFunction(projectCwd: string): void;
|
|
70
94
|
/**
|
package/dist/deploy/build.js
CHANGED
|
@@ -19,7 +19,7 @@ import { frameworkSessionHintCookieName, resolveAuthCookieNamespace, } from "../
|
|
|
19
19
|
import { resolveAgentNativeBuildId } from "../shared/build-id.js";
|
|
20
20
|
import { DEFAULT_SPECULATION_RULES_PATH, resolveSsrCacheHeaders, resolveSsrCacheKeyHeaders, SSR_QUERY_CACHE_KEY_HEADER, } from "../shared/cache-control.js";
|
|
21
21
|
import { EMBED_TARGET_QUERY_PARAM, EMBED_TOKEN_QUERY_PARAM, } from "../shared/embed-auth.js";
|
|
22
|
-
import { normalizeFrameworkRoutePrefix } from "../shared/framework-route-prefix.js";
|
|
22
|
+
import { FRAMEWORK_INTERNAL_ROUTE_PREFIX, normalizeFrameworkRoutePrefix, toPublicFrameworkPath, } from "../shared/framework-route-prefix.js";
|
|
23
23
|
import { mcpEmbedStaticAssetRouteRules } from "../shared/mcp-embed-headers.js";
|
|
24
24
|
import { isTruthyRuntimeValue } from "../shared/runtime-config.js";
|
|
25
25
|
import { AGENT_NATIVE_SOCIAL_IMAGE_ALT, AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER, AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT, AGENT_NATIVE_SOCIAL_IMAGE_PATH, AGENT_NATIVE_SOCIAL_IMAGE_TYPE, AGENT_NATIVE_SOCIAL_IMAGE_WIDTH, } from "../shared/social-meta.js";
|
|
@@ -294,7 +294,79 @@ export function shimCloudflarePagesModuleTimers(code) {
|
|
|
294
294
|
}
|
|
295
295
|
return code;
|
|
296
296
|
}
|
|
297
|
-
export
|
|
297
|
+
export const CLOUDFLARE_SWEEP_CRON = "* * * * *";
|
|
298
|
+
/**
|
|
299
|
+
* The URL path a platform scheduler requests. Under a custom framework route
|
|
300
|
+
* prefix the request boundary 404s `/_agent-native/*` before any route runs,
|
|
301
|
+
* and the server is mounted under the app base path, so the internal sweep
|
|
302
|
+
* path alone never reaches the sweep.
|
|
303
|
+
*/
|
|
304
|
+
export function publicRecurringJobsSweepPath(env = process.env) {
|
|
305
|
+
const basePath = normalizeAppBasePath(env.VITE_APP_BASE_PATH || env.APP_BASE_PATH);
|
|
306
|
+
const publicPath = toPublicFrameworkPath(RECURRING_JOBS_SWEEP_PATH, {
|
|
307
|
+
publicPrefix: resolveBuildFrameworkRoutePrefix(env),
|
|
308
|
+
});
|
|
309
|
+
return `${basePath}${publicPath}`;
|
|
310
|
+
}
|
|
311
|
+
// The token format must match verifyInternalToken in
|
|
312
|
+
// integrations/internal-token.ts.
|
|
313
|
+
function cloudflareSweepTriggerScript(sweepPath) {
|
|
314
|
+
return `const SWEEP_CRON = ${JSON.stringify(CLOUDFLARE_SWEEP_CRON)};
|
|
315
|
+
const SWEEP_PATH = ${JSON.stringify(sweepPath)};
|
|
316
|
+
const SWEEP_TOKEN_SUBJECT = ${JSON.stringify(RECURRING_JOBS_SWEEP_TOKEN_SUBJECT)};
|
|
317
|
+
|
|
318
|
+
async function sweepToken(secret) {
|
|
319
|
+
const timestamp = Date.now();
|
|
320
|
+
const encoder = new TextEncoder();
|
|
321
|
+
const key = await crypto.subtle.importKey(
|
|
322
|
+
"raw",
|
|
323
|
+
encoder.encode(secret),
|
|
324
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
325
|
+
false,
|
|
326
|
+
["sign"],
|
|
327
|
+
);
|
|
328
|
+
const signature = await crypto.subtle.sign(
|
|
329
|
+
"HMAC",
|
|
330
|
+
key,
|
|
331
|
+
encoder.encode(SWEEP_TOKEN_SUBJECT + ":" + timestamp),
|
|
332
|
+
);
|
|
333
|
+
const hex = Array.from(new Uint8Array(signature), (byte) =>
|
|
334
|
+
byte.toString(16).padStart(2, "0"),
|
|
335
|
+
).join("");
|
|
336
|
+
return timestamp + "." + hex;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function runSweep(h, env, ctx) {
|
|
340
|
+
const secret = env?.A2A_SECRET;
|
|
341
|
+
if (!secret) {
|
|
342
|
+
throw new Error("[recurring-jobs] A2A_SECRET is required for the scheduled sweep");
|
|
343
|
+
}
|
|
344
|
+
const request = new Request(
|
|
345
|
+
new URL(SWEEP_PATH, env.APP_URL || "https://scheduled-sweep.invalid"),
|
|
346
|
+
{
|
|
347
|
+
method: "POST",
|
|
348
|
+
headers: {
|
|
349
|
+
Authorization: "Bearer " + (await sweepToken(secret)),
|
|
350
|
+
"Content-Type": "application/json",
|
|
351
|
+
"user-agent": "agent-native-cloudflare-cron",
|
|
352
|
+
},
|
|
353
|
+
body: "{}",
|
|
354
|
+
},
|
|
355
|
+
);
|
|
356
|
+
if (typeof ctx?.waitUntil === "function") {
|
|
357
|
+
request.waitUntil = ctx.waitUntil.bind(ctx);
|
|
358
|
+
}
|
|
359
|
+
const response = await h.fetch(request, env, ctx);
|
|
360
|
+
const body = await response.text();
|
|
361
|
+
if (!response.ok) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
"[recurring-jobs] Scheduled sweep failed (" + response.status + "): " + body.slice(0, 500),
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
console.log("[recurring-jobs] Scheduled sweep finished", body.slice(0, 500));
|
|
367
|
+
}`;
|
|
368
|
+
}
|
|
369
|
+
export function generateCloudflareModuleWorkerEntry(env = process.env) {
|
|
298
370
|
return `let handler;
|
|
299
371
|
|
|
300
372
|
async function loadHandler() {
|
|
@@ -306,6 +378,8 @@ ${cloudflareBindingsInitScript()}
|
|
|
306
378
|
|
|
307
379
|
${cloudflareModuleTimerRestoreScript()}
|
|
308
380
|
|
|
381
|
+
${cloudflareSweepTriggerScript(publicRecurringJobsSweepPath(env))}
|
|
382
|
+
|
|
309
383
|
export default {
|
|
310
384
|
async fetch(request, env, ctx) {
|
|
311
385
|
if (typeof ctx?.waitUntil === "function") {
|
|
@@ -320,7 +394,10 @@ export default {
|
|
|
320
394
|
initializeBindings(env);
|
|
321
395
|
const h = await loadHandler();
|
|
322
396
|
__cfRestoreModuleTimers();
|
|
323
|
-
|
|
397
|
+
await Promise.all([
|
|
398
|
+
h.scheduled?.(controller, env, ctx),
|
|
399
|
+
controller?.cron === SWEEP_CRON ? runSweep(h, env, ctx) : undefined,
|
|
400
|
+
]);
|
|
324
401
|
},
|
|
325
402
|
async email(message, env, ctx) {
|
|
326
403
|
initializeBindings(env);
|
|
@@ -386,6 +463,13 @@ export function configureCloudflareModuleWorkerOutput(serverDir) {
|
|
|
386
463
|
}
|
|
387
464
|
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
388
465
|
config.main = CLOUDFLARE_MODULE_WORKER_ENTRY;
|
|
466
|
+
const crons = Array.isArray(config.triggers?.crons)
|
|
467
|
+
? config.triggers.crons.filter((cron) => typeof cron === "string")
|
|
468
|
+
: [];
|
|
469
|
+
config.triggers = {
|
|
470
|
+
...config.triggers,
|
|
471
|
+
crons: [...new Set([...crons, CLOUDFLARE_SWEEP_CRON])],
|
|
472
|
+
};
|
|
389
473
|
const compatibilityFlags = Array.isArray(config.compatibility_flags)
|
|
390
474
|
? config.compatibility_flags.filter((flag) => typeof flag === "string")
|
|
391
475
|
: [];
|
|
@@ -2696,6 +2780,92 @@ export function resolveKeepWarmSchedule() {
|
|
|
2696
2780
|
}
|
|
2697
2781
|
return raw;
|
|
2698
2782
|
}
|
|
2783
|
+
const DEFAULT_VERCEL_SWEEP_CRON_SCHEDULE = "* * * * *";
|
|
2784
|
+
// Vercel accepts a narrower grammar than cron-parser, and a schedule outside
|
|
2785
|
+
// it fails the deployment rather than the build.
|
|
2786
|
+
function vercelCronScheduleProblem(raw) {
|
|
2787
|
+
const fields = raw.split(/\s+/);
|
|
2788
|
+
if (fields.length !== 5 || !isValidCron(raw)) {
|
|
2789
|
+
return `must be a 5-field cron expression (minute hour day month weekday); got "${raw}" (${fields.length} field(s))`;
|
|
2790
|
+
}
|
|
2791
|
+
if (!fields.every((field) => /^[\d*,/-]+$/.test(field))) {
|
|
2792
|
+
return `must use numbers, not names such as MON or JAN, on Vercel; got "${raw}"`;
|
|
2793
|
+
}
|
|
2794
|
+
const [, , dayOfMonth, , dayOfWeek] = fields;
|
|
2795
|
+
if (dayOfMonth !== "*" && dayOfWeek !== "*") {
|
|
2796
|
+
return `cannot set both a day of the month and a day of the week on Vercel, so one must be "*"; got "${raw}"`;
|
|
2797
|
+
}
|
|
2798
|
+
const weekdays = dayOfWeek
|
|
2799
|
+
.split(",")
|
|
2800
|
+
.flatMap((part) => part.split("/")[0].split("-"));
|
|
2801
|
+
if (weekdays.some((day) => day !== "*" && Number(day) > 6)) {
|
|
2802
|
+
return `must number weekdays 0-6 (Sunday to Saturday) on Vercel; got "${raw}"`;
|
|
2803
|
+
}
|
|
2804
|
+
return null;
|
|
2805
|
+
}
|
|
2806
|
+
// Vercel Hobby rejects a deployment whose cron runs more than once a day, and
|
|
2807
|
+
// the build cannot see the team's plan, so the schedule is the operator's call.
|
|
2808
|
+
export function resolveVercelSweepCronSchedule(env = process.env) {
|
|
2809
|
+
const raw = env.AGENT_NATIVE_VERCEL_CRON_SCHEDULE?.trim();
|
|
2810
|
+
if (!raw)
|
|
2811
|
+
return DEFAULT_VERCEL_SWEEP_CRON_SCHEDULE;
|
|
2812
|
+
const problem = vercelCronScheduleProblem(raw);
|
|
2813
|
+
if (problem) {
|
|
2814
|
+
throw new Error(`AGENT_NATIVE_VERCEL_CRON_SCHEDULE ${problem}. ` +
|
|
2815
|
+
`Example: "0 9 * * *" for once a day on Vercel Hobby.`);
|
|
2816
|
+
}
|
|
2817
|
+
return raw;
|
|
2818
|
+
}
|
|
2819
|
+
export function vercelSweepCrons(sweepPaths, env = process.env) {
|
|
2820
|
+
const schedule = resolveVercelSweepCronSchedule(env);
|
|
2821
|
+
return sweepPaths.map((sweepPath) => ({ path: sweepPath, schedule }));
|
|
2822
|
+
}
|
|
2823
|
+
export function addVercelSweepCron(outputDir, env = process.env) {
|
|
2824
|
+
const configPath = path.join(outputDir, "config.json");
|
|
2825
|
+
if (!fs.existsSync(configPath)) {
|
|
2826
|
+
throw new Error(`[deploy] Nitro did not generate ${configPath} for vercel`);
|
|
2827
|
+
}
|
|
2828
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
2829
|
+
const [sweepCron] = vercelSweepCrons([publicRecurringJobsSweepPath(env)], env);
|
|
2830
|
+
config.crons = [
|
|
2831
|
+
...(config.crons ?? []).filter((cron) => cron.path !== sweepCron.path),
|
|
2832
|
+
sweepCron,
|
|
2833
|
+
];
|
|
2834
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
2835
|
+
console.log(`[build] Added Vercel cron "${sweepCron.schedule}" for ${sweepCron.path}. ` +
|
|
2836
|
+
"Vercel sends CRON_SECRET as its bearer, so set CRON_SECRET in the project." +
|
|
2837
|
+
(env.AGENT_NATIVE_VERCEL_CRON_SCHEDULE?.trim()
|
|
2838
|
+
? ""
|
|
2839
|
+
: " Hobby only allows daily crons: set AGENT_NATIVE_VERCEL_CRON_SCHEDULE " +
|
|
2840
|
+
'to a daily expression such as "0 9 * * *" there.'));
|
|
2841
|
+
}
|
|
2842
|
+
const SWEEP_PATH_AFTER_PREFIX = RECURRING_JOBS_SWEEP_PATH.slice(FRAMEWORK_INTERNAL_ROUTE_PREFIX.length);
|
|
2843
|
+
/**
|
|
2844
|
+
* Reads back the sweep cron `addVercelSweepCron` wrote for an app mounted at
|
|
2845
|
+
* `basePath`. A workspace deploy must reuse it rather than recompute it: the
|
|
2846
|
+
* app's build resolved its framework route prefix from config files the
|
|
2847
|
+
* workspace build never reads.
|
|
2848
|
+
*/
|
|
2849
|
+
export function readVercelSweepCron(outputDir, basePath) {
|
|
2850
|
+
const configPath = path.join(outputDir, "config.json");
|
|
2851
|
+
if (!fs.existsSync(configPath)) {
|
|
2852
|
+
throw new Error(`[deploy] Expected Vercel build config at ${configPath}`);
|
|
2853
|
+
}
|
|
2854
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
2855
|
+
const sweepCrons = (config.crons ?? []).filter((cron) => {
|
|
2856
|
+
if (!cron.path.startsWith(`${basePath}/`) ||
|
|
2857
|
+
!cron.path.endsWith(SWEEP_PATH_AFTER_PREFIX)) {
|
|
2858
|
+
return false;
|
|
2859
|
+
}
|
|
2860
|
+
const prefix = cron.path.slice(basePath.length, -SWEEP_PATH_AFTER_PREFIX.length);
|
|
2861
|
+
return /^\/[^/]+$/.test(prefix);
|
|
2862
|
+
});
|
|
2863
|
+
if (sweepCrons.length !== 1) {
|
|
2864
|
+
throw new Error(`[deploy] Expected one recurring-jobs sweep cron under ${basePath} in ${configPath}, found ${sweepCrons.length}. ` +
|
|
2865
|
+
"Build the app with this version of @agent-native/core so its scheduled work runs on Vercel.");
|
|
2866
|
+
}
|
|
2867
|
+
return sweepCrons[0];
|
|
2868
|
+
}
|
|
2699
2869
|
export function emitSingleTemplateNetlifyKeepWarmFunction(projectCwd) {
|
|
2700
2870
|
if (!isKeepWarmDeployEnabled()) {
|
|
2701
2871
|
const reason = isDisabledByEnv("AGENT_NATIVE_DISABLE_KEEP_WARM")
|
|
@@ -4147,6 +4317,9 @@ export default bundle;
|
|
|
4147
4317
|
if (isCloudflareModulePreset(preset)) {
|
|
4148
4318
|
configureCloudflareModuleWorkerOutput(nitro.options.output.serverDir);
|
|
4149
4319
|
}
|
|
4320
|
+
if (preset === "vercel") {
|
|
4321
|
+
addVercelSweepCron(nitro.options.output.dir);
|
|
4322
|
+
}
|
|
4150
4323
|
if (preset === "netlify" ||
|
|
4151
4324
|
preset === "vercel" ||
|
|
4152
4325
|
isAwsLambdaPreset(preset) ||
|
|
@@ -12,7 +12,7 @@ import { DEFAULT_WORKSPACE_APP_AUDIENCE, normalizeWorkspaceAppHomePath, normaliz
|
|
|
12
12
|
import { DISPATCH_WORKSPACE_ROOT_REDIRECTS, isValidWorkspaceAppIdFormat, } from "../shared/workspace-app-id.js";
|
|
13
13
|
import { createAgentNativeConfigContext, loadResolvedAgentNativeConfig, } from "../vite/agent-native-config-loader.js";
|
|
14
14
|
import { inferWorkspaceAppRootHomePath, readConfiguredWorkspaceAppHomePath, } from "../workspace-app-config.js";
|
|
15
|
-
import { assertEmittedBackgroundFunctionOnDisk, isRecurringJobsDeployEnabled, } from "./build.js";
|
|
15
|
+
import { assertEmittedBackgroundFunctionOnDisk, isRecurringJobsDeployEnabled, readVercelSweepCron, } from "./build.js";
|
|
16
16
|
import { cloneServerBundleForFunction, pruneSsrIslandFromRewritingClone, } from "./function-bundle.js";
|
|
17
17
|
import { collectImmutableAssetPaths, IMMUTABLE_ASSET_CACHE_HEADERS, } from "./immutable-assets.js";
|
|
18
18
|
function workspaceFrameworkRoutePrefixEnv() {
|
|
@@ -211,9 +211,13 @@ export async function runWorkspaceDeploy(opts = {}) {
|
|
|
211
211
|
}
|
|
212
212
|
console.log(`[workspace-deploy] Building ${apps.length} app(s) for preset=${preset}`);
|
|
213
213
|
const execFile = opts.execFile ?? execFileSync;
|
|
214
|
+
const sweepCrons = [];
|
|
214
215
|
for (const app of apps) {
|
|
215
216
|
buildOneApp(workspaceRoot, appsDir, app, preset, execFile, workspaceApps, workspaceAuthMode);
|
|
216
217
|
moveAppBuildIntoWorkspaceOutput(workspaceRoot, appsDir, app, preset, distDir, vercelOutputDir, workspaceApps, workspaceAuthMode);
|
|
218
|
+
if (preset === "vercel") {
|
|
219
|
+
sweepCrons.push(readVercelSweepCron(path.join(appsDir, app, VERCEL_OUTPUT_DIR), `/${app}`));
|
|
220
|
+
}
|
|
217
221
|
}
|
|
218
222
|
writeWorkspaceAppManifests(workspaceRoot, apps, workspaceApps, preset);
|
|
219
223
|
if (workspaceRootPage === "directory") {
|
|
@@ -224,7 +228,7 @@ export async function runWorkspaceDeploy(opts = {}) {
|
|
|
224
228
|
writeNetlifyHeaders(distDir, apps);
|
|
225
229
|
}
|
|
226
230
|
else {
|
|
227
|
-
writeVercelBuildConfig(vercelOutputDir, apps, workspaceApps, workspaceRootPage);
|
|
231
|
+
writeVercelBuildConfig(vercelOutputDir, apps, workspaceApps, workspaceRootPage, sweepCrons);
|
|
228
232
|
}
|
|
229
233
|
if (buildOnly) {
|
|
230
234
|
const outputDir = preset === "vercel" ? vercelOutputDir : distDir;
|
|
@@ -442,7 +446,7 @@ function netlifyHeaderBlock(pathname) {
|
|
|
442
446
|
...Object.entries(IMMUTABLE_ASSET_CACHE_HEADERS).map(([name, value]) => ` ${name}: ${value}`),
|
|
443
447
|
].join("\n");
|
|
444
448
|
}
|
|
445
|
-
function writeVercelBuildConfig(outputDir, apps, workspaceApps, rootPage) {
|
|
449
|
+
function writeVercelBuildConfig(outputDir, apps, workspaceApps, rootPage, sweepCrons) {
|
|
446
450
|
const routes = [
|
|
447
451
|
...vercelImmutableAssetHeaderRoutes(outputDir, apps),
|
|
448
452
|
{ handle: "filesystem" },
|
|
@@ -496,6 +500,7 @@ function writeVercelBuildConfig(outputDir, apps, workspaceApps, rootPage) {
|
|
|
496
500
|
const config = {
|
|
497
501
|
version: 3,
|
|
498
502
|
routes,
|
|
503
|
+
crons: sweepCrons,
|
|
499
504
|
};
|
|
500
505
|
fs.writeFileSync(path.join(outputDir, "config.json"), JSON.stringify(config, null, 2) + "\n");
|
|
501
506
|
}
|