@agent-native/core 0.0.0-beta-20260820214042 → 0.0.0-beta-20260820220930

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 (36) hide show
  1. package/dist/client/agent-page/AgentJobsTab.js +39 -6
  2. package/dist/client/agent-page/AutomationScheduleDialog.d.ts +10 -1
  3. package/dist/client/agent-page/AutomationScheduleDialog.js +3 -2
  4. package/dist/client/agent-page/ScheduledTriggerNotice.d.ts +22 -0
  5. package/dist/client/agent-page/ScheduledTriggerNotice.js +84 -0
  6. package/dist/client/agent-page/scheduled-trigger-state.d.ts +28 -0
  7. package/dist/client/agent-page/scheduled-trigger-state.js +10 -0
  8. package/dist/client/agent-page/use-jobs.d.ts +7 -0
  9. package/dist/client/agent-page/use-jobs.js +14 -0
  10. package/dist/collab/routes.d.ts +1 -1
  11. package/dist/collab/struct-routes.d.ts +1 -1
  12. package/dist/deploy/build.d.ts +5 -0
  13. package/dist/deploy/build.js +12 -1
  14. package/dist/framework-tools.js +1 -0
  15. package/dist/jobs/actions/get-scheduled-trigger-status.d.ts +13 -0
  16. package/dist/jobs/actions/get-scheduled-trigger-status.js +21 -0
  17. package/dist/localization/default-messages.d.ts +14 -0
  18. package/dist/localization/default-messages.js +14 -0
  19. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  20. package/dist/notifications/routes.d.ts +3 -3
  21. package/dist/observability/routes.d.ts +3 -3
  22. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  23. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  24. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  25. package/dist/resources/handlers.d.ts +2 -2
  26. package/dist/server/action-discovery.js +4 -0
  27. package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +42 -1
  28. package/dist/server/agent-chat/recurring-jobs-runtime.js +102 -9
  29. package/dist/server/agent-chat-plugin.d.ts +3 -1
  30. package/dist/server/agent-chat-plugin.js +3 -1
  31. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  32. package/dist/server/realtime-token.d.ts +1 -1
  33. package/dist/server/transcribe-voice.d.ts +1 -1
  34. package/dist/vite/client.js +10 -0
  35. package/docs/content/recurring-jobs.mdx +6 -0
  36. package/package.json +3 -3
@@ -11,7 +11,9 @@ import { AgentEmptyState } from "./AgentEmptyState.js";
11
11
  import { AgentTabFrame } from "./AgentTabFrame.js";
12
12
  import { AutomationDetailsDialog, } from "./AutomationDetailsDialog.js";
13
13
  import { AutomationScheduleDialog } from "./AutomationScheduleDialog.js";
14
- import { useAutomations, useManageAutomation, useManageRecurringJob, useRunAutomationNow, useRecurringJobs, } from "./use-jobs.js";
14
+ import { scheduleFiringFor, } from "./scheduled-trigger-state.js";
15
+ import { ScheduledTriggerNotice } from "./ScheduledTriggerNotice.js";
16
+ import { useAutomations, useManageAutomation, useManageRecurringJob, useRunAutomationNow, useRecurringJobs, useScheduledTriggerState, } from "./use-jobs.js";
15
17
  function listRecurringJobs(jobs) {
16
18
  return jobs.map((resource) => ({
17
19
  kind: "recurring",
@@ -37,7 +39,33 @@ function describeTrigger(entry, t) {
37
39
  entry.resource.schedule ||
38
40
  t("jobs.scheduledTrigger", { defaultValue: "Scheduled" }));
39
41
  }
40
- function detailsFields(entry, t, formatDateTime) {
42
+ /**
43
+ * `nextRun` is computed from the cron expression, not from anything that
44
+ * promises to run it, so it must never be presented with more confidence than
45
+ * the scheduler check earned. Three answers, three phrasings: a known-dead
46
+ * scheduler replaces the date, an unverified one qualifies it, and a confirmed
47
+ * one shows it plainly.
48
+ */
49
+ function nextRunValue(entry, t, formatDateTime, scheduleFiring, unset) {
50
+ const formatted = formatDateTime(entry.resource.nextRun);
51
+ if (entry.triggerType !== "schedule" || scheduleFiring === "fires") {
52
+ return formatted ?? unset;
53
+ }
54
+ if (scheduleFiring === "never") {
55
+ return t("jobs.nextRunNeverScheduler", {
56
+ defaultValue: "Never — no scheduler in this deploy",
57
+ });
58
+ }
59
+ return formatted
60
+ ? t("jobs.nextRunSchedulerUnknown", {
61
+ defaultValue: "{{date}} — unconfirmed, the scheduler check failed",
62
+ date: formatted,
63
+ })
64
+ : t("jobs.nextRunSchedulerUnknownNoDate", {
65
+ defaultValue: "Unknown — the scheduler check failed",
66
+ });
67
+ }
68
+ function detailsFields(entry, t, formatDateTime, scheduleFiring) {
41
69
  const resource = entry.resource;
42
70
  const unset = t("jobs.notSet", { defaultValue: "—" });
43
71
  const fields = [
@@ -66,7 +94,7 @@ function detailsFields(entry, t, formatDateTime) {
66
94
  }
67
95
  fields.push({
68
96
  label: t("jobs.nextRun", { defaultValue: "Next run" }),
69
- value: formatDateTime(resource.nextRun) ?? unset,
97
+ value: nextRunValue(entry, t, formatDateTime, scheduleFiring, unset),
70
98
  }, {
71
99
  label: t("jobs.lastRun", { defaultValue: "Last run" }),
72
100
  value: formatDateTime(resource.lastRun) ??
@@ -104,6 +132,11 @@ export function AgentJobsTab({ canManageOrg = false, hideHeader = false, }) {
104
132
  const personalAutomationsQuery = useAutomations("user");
105
133
  const organizationJobsQuery = useRecurringJobs("org");
106
134
  const organizationAutomationsQuery = useAutomations("org");
135
+ const scheduledTriggerState = useScheduledTriggerState();
136
+ // Three states, not `!== false`: that expression also swallowed a FAILED
137
+ // status query, which then licensed a confident "Next run" forever. See
138
+ // `scheduleFiringFor` for which way each state leans and why.
139
+ const scheduleFiring = scheduleFiringFor(scheduledTriggerState);
107
140
  const personalJobsMutation = useManageRecurringJob("user");
108
141
  const personalAutomationsMutation = useManageAutomation("user");
109
142
  const organizationJobsMutation = useManageRecurringJob("org");
@@ -240,7 +273,7 @@ export function AgentJobsTab({ canManageOrg = false, hideHeader = false, }) {
240
273
  defaultValue: "Create an automation",
241
274
  }), label: t("jobs.newAutomation", {
242
275
  defaultValue: "New automation",
243
- }) }), children: [_jsxs("div", { className: "space-y-7", children: [hideHeader ? (_jsx("div", { className: "flex justify-end", children: _jsx(AgentAskPopover, { context: automationCreationContext(), prompt: t("jobs.automationPrompt", {
276
+ }) }), children: [_jsxs("div", { className: "space-y-7", children: [_jsx(ScheduledTriggerNotice, { state: scheduledTriggerState }), hideHeader ? (_jsx("div", { className: "flex justify-end", children: _jsx(AgentAskPopover, { context: automationCreationContext(), prompt: t("jobs.automationPrompt", {
244
277
  defaultValue: "Create an automation that does this: ",
245
278
  }), title: t("jobs.automationsCreateTitle", {
246
279
  defaultValue: "Create an automation",
@@ -296,9 +329,9 @@ export function AgentJobsTab({ canManageOrg = false, hideHeader = false, }) {
296
329
  name: runTarget.resource.name,
297
330
  scope: runTarget.resource.scope,
298
331
  }, { onSuccess: () => setRunTarget(null) });
299
- }, children: [runAutomationMutation.isPending ? (_jsx(IconLoader2, { className: "size-4 animate-spin" })) : (_jsx(IconPlayerPlay, { className: "size-4" })), t("jobs.runNow", { defaultValue: "Run now" })] })] })] }) }), detailsTarget ? (_jsx(AutomationDetailsDialog, { open: true, name: detailsTarget.resource.name, scope: detailsTarget.resource.scope === "organization" ? "org" : "user", triggerSummary: describeTrigger(detailsTarget, t), fields: detailsFields(detailsTarget, t, formatDateTime), condition: detailsTarget.kind === "automation"
332
+ }, children: [runAutomationMutation.isPending ? (_jsx(IconLoader2, { className: "size-4 animate-spin" })) : (_jsx(IconPlayerPlay, { className: "size-4" })), t("jobs.runNow", { defaultValue: "Run now" })] })] })] }) }), detailsTarget ? (_jsx(AutomationDetailsDialog, { open: true, name: detailsTarget.resource.name, scope: detailsTarget.resource.scope === "organization" ? "org" : "user", triggerSummary: describeTrigger(detailsTarget, t), fields: detailsFields(detailsTarget, t, formatDateTime, scheduleFiring), condition: detailsTarget.kind === "automation"
300
333
  ? detailsTarget.resource.condition
301
334
  : null, instructions: detailsTarget.kind === "automation"
302
335
  ? detailsTarget.resource.body
303
- : detailsTarget.resource.instructions, mcpTools: detailsTarget.resource.mcpTools ?? [], lastError: detailsTarget.resource.lastError, formatTimestamp: (value) => formatDateTime(new Date(value).toISOString()) ?? String(value), onClose: () => setDetailsTarget(null) })) : null, scheduleTarget ? (_jsx(AutomationScheduleDialog, { open: true, name: scheduleTarget.resource.name, schedule: scheduleTarget.resource.schedule ?? "", timezone: scheduleTarget.resource.timezone ?? null, saving: mutationPending, error: mutationError ? mutationError.message : null, onCancel: () => setScheduleTarget(null), onSave: (next) => mutateEntry(scheduleTarget, "update", next, () => setScheduleTarget(null)) })) : null] }));
336
+ : detailsTarget.resource.instructions, mcpTools: detailsTarget.resource.mcpTools ?? [], lastError: detailsTarget.resource.lastError, formatTimestamp: (value) => formatDateTime(new Date(value).toISOString()) ?? String(value), onClose: () => setDetailsTarget(null) })) : null, scheduleTarget ? (_jsx(AutomationScheduleDialog, { open: true, name: scheduleTarget.resource.name, schedule: scheduleTarget.resource.schedule ?? "", timezone: scheduleTarget.resource.timezone ?? null, saving: mutationPending, error: mutationError ? mutationError.message : null, scheduledTriggerState: scheduledTriggerState, onCancel: () => setScheduleTarget(null), onSave: (next) => mutateEntry(scheduleTarget, "update", next, () => setScheduleTarget(null)) })) : null] }));
304
337
  }
@@ -1,3 +1,4 @@
1
+ import type { ScheduledTriggerState } from "./scheduled-trigger-state.js";
1
2
  export interface AutomationScheduleDialogProps {
2
3
  open: boolean;
3
4
  name: string;
@@ -5,10 +6,18 @@ export interface AutomationScheduleDialogProps {
5
6
  timezone: string | null;
6
7
  saving: boolean;
7
8
  error?: string | null;
9
+ /**
10
+ * Passed in rather than queried here so this dialog stays presentational, the
11
+ * way the rest of its props already are. Carries the whole query state so a
12
+ * failed check is distinguishable from a healthy one — this dialog is where
13
+ * someone is actively picking a cadence, so an unverified scheduler is exactly
14
+ * what they need told.
15
+ */
16
+ scheduledTriggerState: ScheduledTriggerState;
8
17
  onCancel: () => void;
9
18
  onSave: (next: {
10
19
  schedule: string;
11
20
  timezone: string;
12
21
  }) => void;
13
22
  }
14
- export declare function AutomationScheduleDialog({ open, name, schedule, timezone, saving, error, onCancel, onSave, }: AutomationScheduleDialogProps): import("react").JSX.Element;
23
+ export declare function AutomationScheduleDialog({ open, name, schedule, timezone, saving, error, scheduledTriggerState, onCancel, onSave, }: AutomationScheduleDialogProps): import("react").JSX.Element;
@@ -5,6 +5,7 @@ import { IconLoader2 } from "@tabler/icons-react";
5
5
  import { useEffect, useState } from "react";
6
6
  import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../components/ui/dialog.js";
7
7
  import { useT } from "../i18n.js";
8
+ import { ScheduledTriggerNotice } from "./ScheduledTriggerNotice.js";
8
9
  import { TimezoneSelect, browserTimezone } from "./TimezoneSelect.js";
9
10
  const PRESETS = [
10
11
  { label: "Every hour", cron: "0 * * * *" },
@@ -16,7 +17,7 @@ const CRON_FIELD_COUNT = 5;
16
17
  function looksLikeCron(value) {
17
18
  return value.trim().split(/\s+/).length === CRON_FIELD_COUNT;
18
19
  }
19
- export function AutomationScheduleDialog({ open, name, schedule, timezone, saving, error, onCancel, onSave, }) {
20
+ export function AutomationScheduleDialog({ open, name, schedule, timezone, saving, error, scheduledTriggerState, onCancel, onSave, }) {
20
21
  const t = useT();
21
22
  const [value, setValue] = useState(schedule);
22
23
  const [zone, setZone] = useState(timezone || browserTimezone());
@@ -37,7 +38,7 @@ export function AutomationScheduleDialog({ open, name, schedule, timezone, savin
37
38
  name: name.replace(/-/g, " "),
38
39
  }) }), _jsx(DialogDescription, { children: t("jobs.editScheduleDescription", {
39
40
  defaultValue: "The clock time below is read in the timezone you pick, so 8:00 means 8:00 there.",
40
- }) })] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { children: [_jsx("label", { className: "text-xs font-medium text-foreground", htmlFor: "automation-schedule", children: t("jobs.cronExpression", { defaultValue: "Cron expression" }) }), _jsx(Input, { id: "automation-schedule", className: "mt-1 font-mono text-sm", value: value, spellCheck: false, autoComplete: "off", disabled: saving, onChange: (event) => setValue(event.target.value) }), _jsx("p", { className: "mt-1 text-[11px] text-muted-foreground", children: t("jobs.cronFormatHint", {
41
+ }) })] }), _jsxs("div", { className: "space-y-3", children: [_jsx(ScheduledTriggerNotice, { state: scheduledTriggerState, variant: "inline" }), _jsxs("div", { children: [_jsx("label", { className: "text-xs font-medium text-foreground", htmlFor: "automation-schedule", children: t("jobs.cronExpression", { defaultValue: "Cron expression" }) }), _jsx(Input, { id: "automation-schedule", className: "mt-1 font-mono text-sm", value: value, spellCheck: false, autoComplete: "off", disabled: saving, onChange: (event) => setValue(event.target.value) }), _jsx("p", { className: "mt-1 text-[11px] text-muted-foreground", children: t("jobs.cronFormatHint", {
41
42
  defaultValue: "minute hour day-of-month month day-of-week",
42
43
  }) })] }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: PRESETS.map((preset) => (_jsx(Button, { type: "button", variant: "outline", size: "sm", className: "h-7 cursor-pointer text-[11px]", disabled: saving, onClick: () => setValue(preset.cron), children: preset.label }, preset.cron))) }), _jsxs("div", { children: [_jsx("label", { className: "text-xs font-medium text-foreground", htmlFor: "automation-timezone", children: t("jobs.timezone", { defaultValue: "Timezone" }) }), _jsx("div", { className: "mt-1", children: _jsx(TimezoneSelect, { id: "automation-timezone", value: zone, disabled: saving, onChange: setZone, suggested: [browserTimezone()] }) })] }), trimmed && !valid ? (_jsx("p", { className: "text-xs text-destructive", children: t("jobs.cronFieldCount", {
43
44
  defaultValue: "A cron expression needs exactly 5 fields.",
@@ -0,0 +1,22 @@
1
+ import type { ScheduledTriggerState } from "./scheduled-trigger-state.js";
2
+ export interface ScheduledTriggerNoticeProps {
3
+ state: ScheduledTriggerState;
4
+ /** `banner` heads the page; `inline` sits inside the schedule dialog. */
5
+ variant?: "banner" | "inline";
6
+ }
7
+ /**
8
+ * Warns that schedule-triggered automations will not fire in this deploy.
9
+ *
10
+ * Worth a persistent surface rather than a one-time toast: whether a schedule
11
+ * fires is decided by the BUILD, while automations are created at runtime, so
12
+ * the mismatch is invisible at the moment it bites. Without this, a scheduled
13
+ * automation reports "Enabled" with a plausible "Next run" and silently never
14
+ * runs. Shown on the Automations page and again inside the schedule editor,
15
+ * which is the one place a user is actively choosing a cadence.
16
+ *
17
+ * Takes the whole query state, not just a resolved status, because a status
18
+ * check that FAILED has to look different from one that came back healthy: the
19
+ * failure is why the next-run dates alongside it are unverified, and silently
20
+ * rendering nothing would present them as confirmed.
21
+ */
22
+ export declare function ScheduledTriggerNotice({ state, variant, }: ScheduledTriggerNoticeProps): import("react").JSX.Element | null;
@@ -0,0 +1,84 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { IconAlertCircle, IconAlertTriangle, IconChevronRight, } from "@tabler/icons-react";
3
+ import { useT } from "../i18n.js";
4
+ /**
5
+ * Warns that schedule-triggered automations will not fire in this deploy.
6
+ *
7
+ * Worth a persistent surface rather than a one-time toast: whether a schedule
8
+ * fires is decided by the BUILD, while automations are created at runtime, so
9
+ * the mismatch is invisible at the moment it bites. Without this, a scheduled
10
+ * automation reports "Enabled" with a plausible "Next run" and silently never
11
+ * runs. Shown on the Automations page and again inside the schedule editor,
12
+ * which is the one place a user is actively choosing a cadence.
13
+ *
14
+ * Takes the whole query state, not just a resolved status, because a status
15
+ * check that FAILED has to look different from one that came back healthy: the
16
+ * failure is why the next-run dates alongside it are unverified, and silently
17
+ * rendering nothing would present them as confirmed.
18
+ */
19
+ export function ScheduledTriggerNotice({ state, variant = "banner", }) {
20
+ const t = useT();
21
+ // Still in flight: resolves in milliseconds, so say nothing rather than
22
+ // flash a caveat at every reader on a perfectly healthy deploy.
23
+ if (state.kind === "loading")
24
+ return null;
25
+ const shell = variant === "banner"
26
+ ? "flex items-start gap-2.5 rounded-xl border px-4 py-3"
27
+ : "flex items-start gap-2 rounded-lg border px-3 py-2";
28
+ // Muted rather than amber: "we could not check" is a weaker claim than "this
29
+ // will not run", and dressing it identically would teach readers to discount
30
+ // the one that means something.
31
+ if (state.kind === "unknown") {
32
+ return (_jsxs("div", { role: "status", "data-testid": "scheduled-trigger-notice", "data-reason": "check-failed", className: `${shell} border-border/70 bg-muted/50`, children: [_jsx(IconAlertCircle, { className: "mt-px size-4 shrink-0 text-muted-foreground" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-xs font-medium text-foreground", children: t("jobs.scheduleUnknownTitle", {
33
+ defaultValue: "Couldn't check whether schedules run here",
34
+ }) }), _jsx("p", { className: "mt-0.5 text-[11px] leading-4 text-muted-foreground", children: t("jobs.scheduleUnknownDetail", {
35
+ defaultValue: "The scheduler status check failed, so the next run times below " +
36
+ "are unconfirmed. Reload to check again.",
37
+ }) })] })] }));
38
+ }
39
+ const status = state.status;
40
+ if (status.available)
41
+ return null;
42
+ const headline = status.reason === "local-development"
43
+ ? t("jobs.scheduleUnavailableLocalTitle", {
44
+ defaultValue: "Schedules don't run in local development",
45
+ })
46
+ : t("jobs.scheduleUnavailableTitle", {
47
+ defaultValue: "Schedules won't run in this deploy",
48
+ });
49
+ const detail = status.reason === "disabled-by-env"
50
+ ? t("jobs.scheduleUnavailableDisabled", {
51
+ defaultValue: "This app was built with recurring jobs turned off, so no scheduled " +
52
+ "automation will fire. Event-triggered automations and Run now still work.",
53
+ })
54
+ : status.reason === "no-platform-scheduler"
55
+ ? t("jobs.scheduleUnavailableNoScheduler", {
56
+ defaultValue: "This hosting target has no durable scheduler, so no scheduled automation will fire. Event-triggered automations and Run now still work.",
57
+ })
58
+ : t("jobs.scheduleUnavailableLocal", {
59
+ defaultValue: "Schedules stay off on a dev machine unless you opt in. " +
60
+ "Event-triggered automations and Run now still work.",
61
+ });
62
+ /**
63
+ * Recovery steps, collapsed by default: the notice has to say what is wrong
64
+ * before it says how to change it, and the how is longer than the what. Null
65
+ * where nothing is actually toggleable — `no-platform-scheduler` means the
66
+ * host has no scheduler to enable, so an empty disclosure would promise a fix
67
+ * that does not exist.
68
+ */
69
+ const fix = status.reason === "disabled-by-env"
70
+ ? t("jobs.scheduleUnavailableDisabledFix", {
71
+ defaultValue: "To enable recurring jobs, set AGENT_NATIVE_DISABLE_RECURRING_JOBS=false in the build environment.",
72
+ })
73
+ : status.reason === "local-development"
74
+ ? t("jobs.scheduleUnavailableLocalFix", {
75
+ defaultValue: "Set AGENT_NATIVE_ENABLE_LOCAL_RECURRING_JOBS=true to run " +
76
+ "schedules on this machine.",
77
+ })
78
+ : null;
79
+ return (_jsxs("div", { role: "status", "data-testid": "scheduled-trigger-notice", "data-reason": status.reason, className: `${shell} border-amber-500/40 bg-amber-500/10`, children: [_jsx(IconAlertTriangle, { className: "mt-px size-4 shrink-0 text-amber-600 dark:text-amber-400" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-xs font-medium text-amber-700 dark:text-amber-300", children: headline }), _jsx("p", { className: "mt-0.5 text-[11px] leading-4 text-amber-700/90 dark:text-amber-300/90", children: detail }), fix ? (_jsxs("details", { className: "group mt-1", children: [_jsxs("summary", { className: "-ml-1.5 inline-flex w-fit cursor-pointer select-none list-none items-center gap-1 rounded-md px-1.5 py-1 text-[11px] font-medium text-amber-700 transition-colors hover:bg-amber-500/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/40 dark:text-amber-300 dark:hover:bg-amber-400/15 [&::-webkit-details-marker]:hidden", children: [_jsx(IconChevronRight, { className: "size-3 shrink-0 transition-transform duration-150 group-open:rotate-90" }), _jsx("span", { className: "group-open:hidden", children: t("jobs.scheduleUnavailableFixLabel", {
80
+ defaultValue: "Show more",
81
+ }) }), _jsx("span", { className: "hidden group-open:inline", children: t("jobs.scheduleUnavailableFixLabelOpen", {
82
+ defaultValue: "Show less",
83
+ }) })] }), _jsx("p", { className: "mt-1 whitespace-pre-line text-[11px] leading-4 text-amber-700/90 dark:text-amber-300/90", children: fix })] })) : null] })] }));
84
+ }
@@ -0,0 +1,28 @@
1
+ import type { ScheduledTriggerStatus } from "../../jobs/actions/get-scheduled-trigger-status.js";
2
+ export type { ScheduledTriggerStatus };
3
+ /**
4
+ * Three-state view of the scheduler status query.
5
+ *
6
+ * `unknown` exists so a FAILED check cannot pass for a healthy one. The status
7
+ * action can 404 on an older deploy, 403 behind an auth boundary, or fail
8
+ * discovery — none of which say anything about whether schedules fire, and all
9
+ * of which read as "fine" under a plain `data?.available !== false`. `loading`
10
+ * stays separate because it resolves on its own in milliseconds; an error does
11
+ * not.
12
+ *
13
+ * Deliberately kept in its own module, free of the query hook: the pure
14
+ * state→consequence mapping below is what the UI branches on, and it stays real
15
+ * in tests that mock the data layer.
16
+ */
17
+ export type ScheduledTriggerState = {
18
+ kind: "loading";
19
+ } | {
20
+ kind: "unknown";
21
+ error: Error | null;
22
+ } | {
23
+ kind: "resolved";
24
+ status: ScheduledTriggerStatus;
25
+ };
26
+ /** Whether a schedule-triggered automation will fire, as far as the UI knows. */
27
+ export type ScheduleFiring = "fires" | "never" | "unknown";
28
+ export declare function scheduleFiringFor(state: ScheduledTriggerState): ScheduleFiring;
@@ -0,0 +1,10 @@
1
+ export function scheduleFiringFor(state) {
2
+ if (state.kind === "resolved") {
3
+ return state.status.available ? "fires" : "never";
4
+ }
5
+ // Loading leans optimistic on purpose: accusing a working deploy of being
6
+ // broken mid-flight is worse than showing the warning a beat late. An errored
7
+ // check gets no such benefit — it never resolves, so the optimism would be
8
+ // permanent and would license a confident "Next run" forever.
9
+ return state.kind === "unknown" ? "unknown" : "fires";
10
+ }
@@ -1,3 +1,4 @@
1
+ import type { ScheduledTriggerState, ScheduledTriggerStatus } from "./scheduled-trigger-state.js";
1
2
  export type JobsScope = "user" | "org";
2
3
  export interface RecurringJob {
3
4
  id: string;
@@ -75,6 +76,12 @@ export interface AutomationRun {
75
76
  finishedAt: number | null;
76
77
  error: string | null;
77
78
  }
79
+ export type { ScheduledTriggerState, ScheduledTriggerStatus };
80
+ /**
81
+ * Deploy-scoped and fixed for the life of the process, so it is cached far
82
+ * longer than the automation lists.
83
+ */
84
+ export declare function useScheduledTriggerState(): ScheduledTriggerState;
78
85
  export declare function useRecurringJobs(scope: JobsScope): import("@tanstack/react-query").UseQueryResult<NoInfer<RecurringJob[]>, Error>;
79
86
  export declare function useAutomations(scope: JobsScope): import("@tanstack/react-query").UseQueryResult<NoInfer<Automation[]>, Error>;
80
87
  export declare function useManageRecurringJob(scope: JobsScope): import("@tanstack/react-query").UseMutationResult<{
@@ -6,6 +6,20 @@ function recurringParams(scope) {
6
6
  function automationParams(scope) {
7
7
  return { scope: scope === "org" ? "organization" : "personal" };
8
8
  }
9
+ /**
10
+ * Deploy-scoped and fixed for the life of the process, so it is cached far
11
+ * longer than the automation lists.
12
+ */
13
+ export function useScheduledTriggerState() {
14
+ const query = useActionQuery("get-scheduled-trigger-status", {}, { staleTime: 5 * 60_000 });
15
+ // Data first: a cached answer with a failing background refetch is still an
16
+ // answer, and downgrading it to `unknown` would flicker the warning off.
17
+ if (query.data)
18
+ return { kind: "resolved", status: query.data };
19
+ if (query.error)
20
+ return { kind: "unknown", error: query.error };
21
+ return { kind: "loading" };
22
+ }
9
23
  export function useRecurringJobs(scope) {
10
24
  return useActionQuery("list-recurring-jobs", recurringParams(scope), { staleTime: 5_000 });
11
25
  }
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- error: string;
30
29
  ok?: undefined;
30
+ error: string;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- error: string;
17
16
  ok?: undefined;
17
+ error: string;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -101,6 +101,11 @@ export declare function isDurableBackgroundDeployEnabled(): boolean;
101
101
  export declare const NETLIFY_INTEGRATION_RECOVERY_FUNCTION_NAME = "server-integration-recovery";
102
102
  export declare function isIntegrationDurableDispatchDeployEnabled(): boolean;
103
103
  export declare const NETLIFY_RECURRING_JOBS_FUNCTION_NAME = "agent-native-recurring-jobs";
104
+ /**
105
+ * Routed through the same resolver that produces the runtime marker, so the gate
106
+ * that emits the scheduled function and the value the deployed app reports
107
+ * cannot drift: a build that skips the emit always ships `"disabled"`.
108
+ */
104
109
  export declare function isRecurringJobsDeployEnabled(): boolean;
105
110
  /**
106
111
  * Keep-warm is opt-in because a once-a-minute wake prevents autosuspend on
@@ -24,6 +24,7 @@ import { INTEGRATION_RECOVERY_RUNTIME_MARKER, INTEGRATION_RETRY_SWEEP_PATH, INTE
24
24
  import { isValidCron } from "../jobs/cron.js";
25
25
  import { RECURRING_JOBS_SWEEP_PATH, RECURRING_JOBS_SWEEP_TOKEN_SUBJECT, } from "../jobs/scheduler-dispatch.js";
26
26
  import { findWorkspaceRoot as findAgentNativeWorkspaceRoot } from "../scripts/utils.js";
27
+ import { RECURRING_JOBS_BUILD_MARKER_ENV_VAR, resolveRecurringJobsBuildMarker, } from "../server/agent-chat/recurring-jobs-runtime.js";
27
28
  import { normalizeAppBasePath } from "../server/app-base-path.js";
28
29
  import { DEFAULT_SPECULATION_RULES_PATH, resolveSsrCacheHeaders, resolveSsrCacheKeyHeaders, SSR_QUERY_CACHE_KEY_HEADER, } from "../shared/cache-control.js";
29
30
  import { mcpEmbedStaticAssetRouteRules } from "../shared/mcp-embed-headers.js";
@@ -2407,8 +2408,13 @@ function isTruthyEnv(name) {
2407
2408
  function isDisabledByEnv(name) {
2408
2409
  return isTruthyEnv(name);
2409
2410
  }
2411
+ /**
2412
+ * Routed through the same resolver that produces the runtime marker, so the gate
2413
+ * that emits the scheduled function and the value the deployed app reports
2414
+ * cannot drift: a build that skips the emit always ships `"disabled"`.
2415
+ */
2410
2416
  export function isRecurringJobsDeployEnabled() {
2411
- return !isDisabledByEnv("AGENT_NATIVE_DISABLE_RECURRING_JOBS");
2417
+ return resolveRecurringJobsBuildMarker(process.env) === "enabled";
2412
2418
  }
2413
2419
  /**
2414
2420
  * Keep-warm is opt-in because a once-a-minute wake prevents autosuspend on
@@ -3887,6 +3893,11 @@ export function resolveNitroBuildReplacements(env = process.env, deploymentEnvir
3887
3893
  // well as in the Vite server bundle.
3888
3894
  "process.env.AGENT_NATIVE_RELEASE_MIGRATIONS": JSON.stringify(env.AGENT_NATIVE_RELEASE_MIGRATIONS?.trim() || ""),
3889
3895
  "process.env.AGENT_NATIVE_BUILD_DEPLOY_CONTEXT": JSON.stringify(env.CONTEXT?.trim() || env.NETLIFY_CONTEXT?.trim() || ""),
3896
+ // Whether the recurring-jobs scheduled function exists is decided HERE, by
3897
+ // the build env. `scheduledTriggerAvailability` cannot re-derive it later —
3898
+ // a pipeline that sets the kill switch only for the build leaves no runtime
3899
+ // trace of it — so hand the decision to the runtime explicitly.
3900
+ [`process.env.${RECURRING_JOBS_BUILD_MARKER_ENV_VAR}`]: JSON.stringify(resolveRecurringJobsBuildMarker(env)),
3890
3901
  ...(configuredDeploymentEnvironment
3891
3902
  ? {
3892
3903
  "process.env.AGENT_NATIVE_DEPLOYMENT_ENVIRONMENT": JSON.stringify(configuredDeploymentEnvironment),
@@ -126,6 +126,7 @@ export const CORE_ACTION_GROUPS = {
126
126
  "manage-recurring-job": "automation",
127
127
  "run-automation-now": "automation",
128
128
  "list-automation-runs": "automation",
129
+ "get-scheduled-trigger-status": "automation",
129
130
  "list-automations": "automation",
130
131
  "manage-automation": "automation",
131
132
  "get-usage-alerts": "automation",
@@ -0,0 +1,13 @@
1
+ import { type ScheduledTriggerAvailability } from "../../server/agent-chat/recurring-jobs-runtime.js";
2
+ export type ScheduledTriggerStatus = ScheduledTriggerAvailability;
3
+ /**
4
+ * Read surface for "will a schedule-triggered automation actually fire here".
5
+ *
6
+ * A separate action rather than a field on `list-automations` / `list-recurring-jobs`:
7
+ * both of those return bare arrays that several callers map over directly, so
8
+ * adding a sibling field means an envelope and a breaking change at every call
9
+ * site. This is deploy-scoped rather than per-automation data anyway — it never
10
+ * varies by row — so it caches independently and for much longer.
11
+ */
12
+ declare const _default: import("../../action.js").ActionDefinition<Record<string, never>, ScheduledTriggerAvailability>;
13
+ export default _default;
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../action.js";
3
+ import { scheduledTriggerAvailability, } from "../../server/agent-chat/recurring-jobs-runtime.js";
4
+ /**
5
+ * Read surface for "will a schedule-triggered automation actually fire here".
6
+ *
7
+ * A separate action rather than a field on `list-automations` / `list-recurring-jobs`:
8
+ * both of those return bare arrays that several callers map over directly, so
9
+ * adding a sibling field means an envelope and a breaking change at every call
10
+ * site. This is deploy-scoped rather than per-automation data anyway — it never
11
+ * varies by row — so it caches independently and for much longer.
12
+ */
13
+ export default defineAction({
14
+ description: "Report whether schedule-triggered automations can actually fire in this deploy, and which driver (or missing driver) decides that. Used by the Agent Automations page to warn that a schedule will never run.",
15
+ agentTool: false,
16
+ schema: z.object({}),
17
+ http: { method: "GET" },
18
+ readOnly: true,
19
+ parallelSafe: true,
20
+ run: async () => scheduledTriggerAvailability(),
21
+ });
@@ -667,6 +667,20 @@ declare const messages: {
667
667
  organization: string;
668
668
  settingsSummary: string;
669
669
  openAutomations: string;
670
+ nextRunNeverScheduler: string;
671
+ nextRunSchedulerUnknown: string;
672
+ nextRunSchedulerUnknownNoDate: string;
673
+ scheduleUnknownTitle: string;
674
+ scheduleUnknownDetail: string;
675
+ scheduleUnavailableTitle: string;
676
+ scheduleUnavailableLocalTitle: string;
677
+ scheduleUnavailableDisabled: string;
678
+ scheduleUnavailableNoScheduler: string;
679
+ scheduleUnavailableLocal: string;
680
+ scheduleUnavailableDisabledFix: string;
681
+ scheduleUnavailableLocalFix: string;
682
+ scheduleUnavailableFixLabel: string;
683
+ scheduleUnavailableFixLabelOpen: string;
670
684
  };
671
685
  share: {
672
686
  titleWithResource: string;
@@ -673,6 +673,20 @@ const messages = {
673
673
  organization: "Organization",
674
674
  settingsSummary: "Manage scheduled and event-triggered agent tasks together from the Automations page.",
675
675
  openAutomations: "Open Automations",
676
+ nextRunNeverScheduler: "Never — no scheduler in this deploy",
677
+ nextRunSchedulerUnknown: "{{date}} — unconfirmed, the scheduler check failed",
678
+ nextRunSchedulerUnknownNoDate: "Unknown — the scheduler check failed",
679
+ scheduleUnknownTitle: "Couldn't check whether schedules run here",
680
+ scheduleUnknownDetail: "The scheduler status check failed, so the next run times below are unconfirmed. Reload to check again.",
681
+ scheduleUnavailableTitle: "Schedules won't run in this deploy",
682
+ scheduleUnavailableLocalTitle: "Schedules don't run in local development",
683
+ scheduleUnavailableDisabled: "This app was built with recurring jobs turned off, so no scheduled automation will fire. Event-triggered automations and Run now still work.",
684
+ scheduleUnavailableNoScheduler: "This hosting target has no durable scheduler, so no scheduled automation will fire. Event-triggered automations and Run now still work.",
685
+ scheduleUnavailableLocal: "Schedules stay off on a dev machine unless you opt in. Event-triggered automations and Run now still work.",
686
+ scheduleUnavailableDisabledFix: "To enable recurring jobs, set AGENT_NATIVE_DISABLE_RECURRING_JOBS=false in the build environment.",
687
+ scheduleUnavailableLocalFix: "Set AGENT_NATIVE_ENABLE_LOCAL_RECURRING_JOBS=true to run schedules on this machine.",
688
+ scheduleUnavailableFixLabel: "Show more",
689
+ scheduleUnavailableFixLabelOpen: "Show less",
676
690
  },
677
691
  share: {
678
692
  titleWithResource: 'Share "{{title}}"',
@@ -137,13 +137,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
137
137
  inputSchema: {
138
138
  type: string;
139
139
  properties: {
140
- count?: undefined;
141
140
  query?: undefined;
142
141
  minutes?: undefined;
143
142
  limit?: undefined;
144
143
  clientHint?: undefined;
145
144
  timestamp?: undefined;
146
145
  chapterId?: undefined;
146
+ count?: undefined;
147
147
  startAt?: undefined;
148
148
  endAt?: undefined;
149
149
  reason?: undefined;
@@ -159,7 +159,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
159
159
  inputSchema: {
160
160
  type: string;
161
161
  properties: {
162
- count?: undefined;
163
162
  query: {
164
163
  type: string;
165
164
  description: string;
@@ -175,6 +174,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
175
174
  clientHint?: undefined;
176
175
  timestamp?: undefined;
177
176
  chapterId?: undefined;
177
+ count?: undefined;
178
178
  startAt?: undefined;
179
179
  endAt?: undefined;
180
180
  reason?: undefined;
@@ -190,7 +190,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
190
190
  inputSchema: {
191
191
  type: string;
192
192
  properties: {
193
- count?: undefined;
194
193
  minutes: {
195
194
  type: string;
196
195
  description: string;
@@ -200,6 +199,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
200
199
  clientHint?: undefined;
201
200
  timestamp?: undefined;
202
201
  chapterId?: undefined;
202
+ count?: undefined;
203
203
  startAt?: undefined;
204
204
  endAt?: undefined;
205
205
  reason?: undefined;
@@ -216,7 +216,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
216
216
  type: string;
217
217
  required: string[];
218
218
  properties: {
219
- count?: undefined;
220
219
  query: {
221
220
  type: string;
222
221
  description: string;
@@ -235,6 +234,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
235
234
  };
236
235
  timestamp?: undefined;
237
236
  chapterId?: undefined;
237
+ count?: undefined;
238
238
  startAt?: undefined;
239
239
  endAt?: undefined;
240
240
  reason?: undefined;
@@ -250,7 +250,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
250
250
  type: string;
251
251
  required: string[];
252
252
  properties: {
253
- count?: undefined;
254
253
  query?: undefined;
255
254
  minutes?: undefined;
256
255
  limit?: undefined;
@@ -264,6 +263,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
264
263
  description: string;
265
264
  };
266
265
  chapterId?: undefined;
266
+ count?: undefined;
267
267
  startAt?: undefined;
268
268
  endAt?: undefined;
269
269
  includeMicrophone?: undefined;
@@ -314,13 +314,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
314
314
  type: string;
315
315
  required: string[];
316
316
  properties: {
317
- count?: undefined;
318
317
  query?: undefined;
319
318
  minutes?: undefined;
320
319
  limit?: undefined;
321
320
  clientHint?: undefined;
322
321
  timestamp?: undefined;
323
322
  chapterId?: undefined;
323
+ count?: undefined;
324
324
  startAt: {
325
325
  type: string;
326
326
  description: string;
@@ -349,13 +349,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
349
349
  type: string;
350
350
  required: string[];
351
351
  properties: {
352
- count?: undefined;
353
352
  query?: undefined;
354
353
  minutes?: undefined;
355
354
  limit?: undefined;
356
355
  clientHint?: undefined;
357
356
  timestamp?: undefined;
358
357
  chapterId?: undefined;
358
+ count?: undefined;
359
359
  startAt?: undefined;
360
360
  endAt?: undefined;
361
361
  reason?: undefined;
@@ -13,13 +13,13 @@
13
13
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
14
14
  count: number;
15
15
  updated?: undefined;
16
- error?: undefined;
17
16
  ok?: undefined;
17
+ error?: undefined;
18
18
  } | {
19
19
  count?: undefined;
20
20
  updated: number;
21
- error?: undefined;
22
21
  ok?: undefined;
22
+ error?: undefined;
23
23
  } | {
24
24
  count?: undefined;
25
25
  updated?: undefined;
@@ -28,6 +28,6 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
28
28
  } | {
29
29
  count?: undefined;
30
30
  updated?: undefined;
31
- error?: undefined;
32
31
  ok: boolean;
32
+ error?: undefined;
33
33
  }>>;
@@ -41,16 +41,16 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
41
41
  thumbsUpRate: number;
42
42
  avgEvalScore: number;
43
43
  } | {
44
- error?: undefined;
45
44
  summary: import("./types.js").TraceSummary;
46
45
  spans: import("./types.js").TraceSpan[];
47
46
  id?: undefined;
47
+ error?: undefined;
48
48
  ok?: undefined;
49
49
  } | {
50
- error?: undefined;
51
50
  summary?: undefined;
52
51
  spans?: undefined;
53
52
  id: string;
53
+ error?: undefined;
54
54
  ok?: undefined;
55
55
  } | {
56
56
  summary?: undefined;
@@ -59,9 +59,9 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
59
59
  error: any;
60
60
  ok?: undefined;
61
61
  } | {
62
- error?: undefined;
63
62
  summary?: undefined;
64
63
  spans?: undefined;
65
64
  id?: undefined;
65
+ error?: undefined;
66
66
  ok: boolean;
67
67
  }>>;
@@ -94,28 +94,28 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
94
94
  message?: undefined;
95
95
  } | {
96
96
  id?: undefined;
97
- count?: undefined;
98
97
  deleted?: undefined;
99
98
  providers?: undefined;
99
+ count?: undefined;
100
100
  found: boolean;
101
101
  provider: import("../custom-registry.js").CustomProviderConfig;
102
102
  registered?: undefined;
103
103
  label?: undefined;
104
104
  message?: undefined;
105
105
  } | {
106
- count?: undefined;
107
106
  provider?: undefined;
108
107
  deleted?: undefined;
109
108
  providers?: undefined;
109
+ count?: undefined;
110
110
  found: boolean;
111
111
  id: string;
112
112
  registered?: undefined;
113
113
  label?: undefined;
114
114
  message?: undefined;
115
115
  } | {
116
- count?: undefined;
117
116
  provider?: undefined;
118
117
  providers?: undefined;
118
+ count?: undefined;
119
119
  found?: undefined;
120
120
  deleted: boolean;
121
121
  id: string;
@@ -123,10 +123,10 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
123
123
  label?: undefined;
124
124
  message?: undefined;
125
125
  } | {
126
- count?: undefined;
127
126
  provider?: undefined;
128
127
  deleted?: undefined;
129
128
  providers?: undefined;
129
+ count?: undefined;
130
130
  found?: undefined;
131
131
  registered: boolean;
132
132
  id: string;
@@ -311,9 +311,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
311
311
  notes?: string | undefined;
312
312
  scope?: "org" | "user" | undefined;
313
313
  }, {
314
- message?: undefined;
315
- deleted?: undefined;
316
314
  id?: undefined;
315
+ provider?: undefined;
316
+ deleted?: undefined;
317
317
  providers: {
318
318
  id: string;
319
319
  label: string;
@@ -324,45 +324,45 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
324
324
  updatedAt: number;
325
325
  }[];
326
326
  count: number;
327
- provider?: undefined;
328
327
  found?: undefined;
329
328
  registered?: undefined;
330
329
  label?: undefined;
331
- } | {
332
330
  message?: undefined;
333
- deleted?: undefined;
331
+ } | {
332
+ count?: undefined;
334
333
  id?: undefined;
334
+ deleted?: undefined;
335
335
  providers?: undefined;
336
- count?: undefined;
337
336
  found: boolean;
338
337
  provider: import("../custom-registry.js").CustomProviderConfig;
339
338
  registered?: undefined;
340
339
  label?: undefined;
341
- } | {
342
340
  message?: undefined;
343
- deleted?: undefined;
344
- providers?: undefined;
341
+ } | {
345
342
  count?: undefined;
346
343
  provider?: undefined;
344
+ deleted?: undefined;
345
+ providers?: undefined;
347
346
  found: boolean;
348
347
  id: string;
349
348
  registered?: undefined;
350
349
  label?: undefined;
351
- } | {
352
350
  message?: undefined;
353
- providers?: undefined;
351
+ } | {
354
352
  count?: undefined;
355
353
  provider?: undefined;
354
+ providers?: undefined;
356
355
  found?: undefined;
357
356
  deleted: boolean;
358
357
  id: string;
359
358
  registered?: undefined;
360
359
  label?: undefined;
360
+ message?: undefined;
361
361
  } | {
362
- deleted?: undefined;
363
- providers?: undefined;
364
362
  count?: undefined;
365
363
  provider?: undefined;
364
+ deleted?: undefined;
365
+ providers?: undefined;
366
366
  found?: undefined;
367
367
  registered: boolean;
368
368
  id: string;
@@ -73,7 +73,6 @@ export declare function createProviderCorpusJobAction(options: CreateProviderCor
73
73
  offset?: unknown;
74
74
  limit?: unknown;
75
75
  }, {
76
- deleted?: undefined;
77
76
  jobs: {
78
77
  id: string;
79
78
  name: string;
@@ -84,6 +83,7 @@ export declare function createProviderCorpusJobAction(options: CreateProviderCor
84
83
  updatedAt: string;
85
84
  }[];
86
85
  total: number;
86
+ deleted?: undefined;
87
87
  jobId?: undefined;
88
88
  } | {
89
89
  jobs?: undefined;
@@ -91,9 +91,9 @@ export declare function createProviderCorpusJobAction(options: CreateProviderCor
91
91
  deleted: boolean;
92
92
  jobId: string;
93
93
  } | {
94
- deleted?: undefined;
95
94
  jobs?: undefined;
96
95
  total?: undefined;
96
+ deleted?: undefined;
97
97
  jobId?: undefined;
98
98
  hits: Record<string, unknown>[];
99
99
  offset: number;
@@ -51,8 +51,8 @@ export declare function handleDeleteResource(event: any): Promise<{
51
51
  error: string;
52
52
  ok?: undefined;
53
53
  } | {
54
- ok: boolean;
55
54
  error?: undefined;
55
+ ok: boolean;
56
56
  }>;
57
57
  /** POST /_agent-native/resources/upload — upload a file as a resource */
58
58
  export declare function handleUploadResource(event: any): Promise<import("./store.js").Resource | {
@@ -73,10 +73,10 @@ export declare function handleUploadResource(event: any): Promise<import("./stor
73
73
  runId: string | null;
74
74
  expiresAt: number | null;
75
75
  metadata: string | null;
76
+ error?: undefined;
76
77
  url: string;
77
78
  provider: string;
78
79
  storageSetupRequired?: undefined;
79
- error?: undefined;
80
80
  } | {
81
81
  error: string;
82
82
  storageSetupRequired: boolean;
@@ -626,6 +626,10 @@ export async function mergeCoreSharingActions(registry) {
626
626
  "list-automation-runs",
627
627
  () => import("../jobs/actions/list-automation-runs.js"),
628
628
  ],
629
+ [
630
+ "get-scheduled-trigger-status",
631
+ () => import("../jobs/actions/get-scheduled-trigger-status.js"),
632
+ ],
629
633
  [
630
634
  "list-automations",
631
635
  () => import("../triggers/actions/list-automations.js"),
@@ -1,4 +1,4 @@
1
- type RecurringJobsRuntimeEnvKey = "AGENT_NATIVE_DISABLE_RECURRING_JOBS" | "AGENT_NATIVE_ENABLE_LOCAL_RECURRING_JOBS" | "APP_URL" | "BETTER_AUTH_URL" | "CF_PAGES" | "DEPLOY_URL" | "AWS_EXECUTION_ENV" | "AWS_LAMBDA_FUNCTION_NAME" | "NETLIFY" | "NETLIFY_LOCAL" | "NITRO_PRESET" | "NODE_ENV" | "SITE_ID" | "URL" | "VERCEL" | "VITE_APP_URL" | "VITE_WORKSPACE_GATEWAY_URL" | "WORKSPACE_GATEWAY_URL";
1
+ type RecurringJobsRuntimeEnvKey = "AGENT_NATIVE_BUILD_RECURRING_JOBS" | "AGENT_NATIVE_DISABLE_RECURRING_JOBS" | "AGENT_NATIVE_ENABLE_LOCAL_RECURRING_JOBS" | "APP_URL" | "BETTER_AUTH_URL" | "CF_PAGES" | "DEPLOY_URL" | "AWS_EXECUTION_ENV" | "AWS_LAMBDA_FUNCTION_NAME" | "NETLIFY" | "NETLIFY_LOCAL" | "NITRO_PRESET" | "NODE_ENV" | "SITE_ID" | "URL" | "VERCEL" | "VITE_APP_URL" | "VITE_WORKSPACE_GATEWAY_URL" | "WORKSPACE_GATEWAY_URL";
2
2
  type RecurringJobsRuntimeEnv = Partial<Record<RecurringJobsRuntimeEnvKey, string | undefined>>;
3
3
  export declare function shouldDisableRecurringJobsRuntime(env?: RecurringJobsRuntimeEnv): boolean;
4
4
  /**
@@ -7,4 +7,45 @@ export declare function shouldDisableRecurringJobsRuntime(env?: RecurringJobsRun
7
7
  * that timer, which is exactly the failure mode the emitted sweep fixes.
8
8
  */
9
9
  export declare function isNetlifyRecurringJobsRuntime(env?: RecurringJobsRuntimeEnv): boolean;
10
+ /** What the BUILD decided about recurring jobs, as carried into the runtime. */
11
+ export type RecurringJobsBuildMarker = "enabled" | "disabled";
12
+ /**
13
+ * Env name the build embeds its recurring-jobs decision under.
14
+ *
15
+ * Netlify's scheduled function is emitted — or omitted — while the build runs,
16
+ * and a deployed serverless runtime never sees the build environment. So the
17
+ * build has to hand the decision over explicitly; nothing observable at runtime
18
+ * can reconstruct it. Consumed below as a LITERAL `process.env.<name>` member
19
+ * expression because that is the only form Vite's `define` and Nitro's
20
+ * `replace` inline at build time.
21
+ */
22
+ export declare const RECURRING_JOBS_BUILD_MARKER_ENV_VAR = "AGENT_NATIVE_BUILD_RECURRING_JOBS";
23
+ /**
24
+ * Build-side resolver: what the build environment decided. Shared with
25
+ * `isRecurringJobsDeployEnabled` in the deploy build so the gate that emits the
26
+ * scheduled function and the marker that reports it cannot disagree.
27
+ */
28
+ export declare function resolveRecurringJobsBuildMarker(env?: RecurringJobsRuntimeEnv): RecurringJobsBuildMarker;
29
+ export type ScheduledTriggerAvailability = {
30
+ available: true;
31
+ driver: "netlify-scheduled-function" | "in-process";
32
+ } | {
33
+ available: false;
34
+ reason: "disabled-by-env" | "no-platform-scheduler" | "local-development";
35
+ };
36
+ /**
37
+ * Whether ANY driver will actually fire a schedule-triggered automation in this
38
+ * deploy. Distinct from `shouldDisableRecurringJobsRuntime`, which answers the
39
+ * narrower "should THIS process run an in-process timer" and is therefore `true`
40
+ * on hosted Netlify even though schedules do fire there via the emitted
41
+ * scheduled function — reporting that value to a user would call the one
42
+ * working production runtime broken.
43
+ *
44
+ * Each branch reads the scope that actually decides its driver. The Netlify
45
+ * driver is a BUILD artifact, so it is answered by the build marker; every other
46
+ * driver is an in-process timer started from the RUNTIME env, so it is answered
47
+ * by the runtime flag. Reading runtime-only platform markers for the Netlify
48
+ * branch would report `available` off a build that emitted no trigger at all.
49
+ */
50
+ export declare function scheduledTriggerAvailability(env?: RecurringJobsRuntimeEnv): ScheduledTriggerAvailability;
10
51
  export {};
@@ -30,20 +30,27 @@ function isLoopbackAppUrl(value) {
30
30
  }
31
31
  return false;
32
32
  }
33
- export function shouldDisableRecurringJobsRuntime(env = process.env) {
34
- if (isTruthyEnv(env.AGENT_NATIVE_DISABLE_RECURRING_JOBS))
35
- return true;
36
- // A serverless isolate is not a durable scheduler. Keep this check separate
37
- // from the platform-specific scheduler branch below so a new sweep cannot
38
- // accidentally start an in-process timer before its platform trigger exists.
39
- const isServerlessRuntime = env.NETLIFY_LOCAL !== "true" &&
33
+ /**
34
+ * A serverless isolate is not a durable scheduler. Shared so
35
+ * `shouldDisableRecurringJobsRuntime` and `scheduledTriggerAvailability` cannot
36
+ * drift on what counts as serverless.
37
+ */
38
+ function isServerlessRecurringJobsRuntime(env) {
39
+ return (env.NETLIFY_LOCAL !== "true" &&
40
40
  (isTruthyEnv(env.NETLIFY) ||
41
41
  env.NITRO_PRESET === "netlify" ||
42
42
  Boolean(env.AWS_LAMBDA_FUNCTION_NAME) ||
43
43
  env.AWS_EXECUTION_ENV?.startsWith("AWS_Lambda") === true ||
44
44
  isTruthyEnv(env.CF_PAGES) ||
45
- isTruthyEnv(env.VERCEL));
46
- if (isServerlessRuntime)
45
+ isTruthyEnv(env.VERCEL)));
46
+ }
47
+ export function shouldDisableRecurringJobsRuntime(env = process.env) {
48
+ if (isTruthyEnv(env.AGENT_NATIVE_DISABLE_RECURRING_JOBS))
49
+ return true;
50
+ // Keep this check separate from the platform-specific scheduler branch below
51
+ // so a new sweep cannot accidentally start an in-process timer before its
52
+ // platform trigger exists.
53
+ if (isServerlessRecurringJobsRuntime(env))
47
54
  return true;
48
55
  const isLocalRuntime = env.NODE_ENV === "development" ||
49
56
  env.NODE_ENV === "test" ||
@@ -74,3 +81,89 @@ export function isNetlifyRecurringJobsRuntime(env = process.env) {
74
81
  return false;
75
82
  return Boolean((env.NETLIFY && env.NETLIFY !== "false") || env.SITE_ID);
76
83
  }
84
+ /**
85
+ * Env name the build embeds its recurring-jobs decision under.
86
+ *
87
+ * Netlify's scheduled function is emitted — or omitted — while the build runs,
88
+ * and a deployed serverless runtime never sees the build environment. So the
89
+ * build has to hand the decision over explicitly; nothing observable at runtime
90
+ * can reconstruct it. Consumed below as a LITERAL `process.env.<name>` member
91
+ * expression because that is the only form Vite's `define` and Nitro's
92
+ * `replace` inline at build time.
93
+ */
94
+ export const RECURRING_JOBS_BUILD_MARKER_ENV_VAR = "AGENT_NATIVE_BUILD_RECURRING_JOBS";
95
+ /**
96
+ * Build-side resolver: what the build environment decided. Shared with
97
+ * `isRecurringJobsDeployEnabled` in the deploy build so the gate that emits the
98
+ * scheduled function and the marker that reports it cannot disagree.
99
+ */
100
+ export function resolveRecurringJobsBuildMarker(env = process.env) {
101
+ return isTruthyEnv(env.AGENT_NATIVE_DISABLE_RECURRING_JOBS)
102
+ ? "disabled"
103
+ : "enabled";
104
+ }
105
+ function readRecurringJobsBuildMarker(env) {
106
+ const raw = env.AGENT_NATIVE_BUILD_RECURRING_JOBS ??
107
+ // config-ok: this value is INLINED at build time by Vite's `define` /
108
+ // Nitro's `replace`, which rewrite the literal `process.env.<NAME>` member
109
+ // expression and nothing else. A declared app-config field is read at
110
+ // runtime from the deployed environment, which is precisely the scope that
111
+ // cannot see the build's decision — the bug this marker exists to fix.
112
+ // Reading through the aliased `env` parameter would also survive the build
113
+ // unreplaced, so the literal form is load-bearing.
114
+ process.env.AGENT_NATIVE_BUILD_RECURRING_JOBS;
115
+ const value = raw?.trim();
116
+ return value === "enabled" || value === "disabled" ? value : undefined;
117
+ }
118
+ /**
119
+ * Whether ANY driver will actually fire a schedule-triggered automation in this
120
+ * deploy. Distinct from `shouldDisableRecurringJobsRuntime`, which answers the
121
+ * narrower "should THIS process run an in-process timer" and is therefore `true`
122
+ * on hosted Netlify even though schedules do fire there via the emitted
123
+ * scheduled function — reporting that value to a user would call the one
124
+ * working production runtime broken.
125
+ *
126
+ * Each branch reads the scope that actually decides its driver. The Netlify
127
+ * driver is a BUILD artifact, so it is answered by the build marker; every other
128
+ * driver is an in-process timer started from the RUNTIME env, so it is answered
129
+ * by the runtime flag. Reading runtime-only platform markers for the Netlify
130
+ * branch would report `available` off a build that emitted no trigger at all.
131
+ */
132
+ export function scheduledTriggerAvailability(env = process.env) {
133
+ const buildMarker = readRecurringJobsBuildMarker(env);
134
+ // Netlify: the driver IS the emitted scheduled function. It fires on the
135
+ // platform's clock without consulting the deployed env, and no runtime value
136
+ // can conjure it back once the build omitted it — so the build marker is the
137
+ // only honest input here, in both directions.
138
+ if (isNetlifyRecurringJobsRuntime(env)) {
139
+ if (buildMarker === "disabled") {
140
+ return { available: false, reason: "disabled-by-env" };
141
+ }
142
+ if (buildMarker === "enabled") {
143
+ return { available: true, driver: "netlify-scheduled-function" };
144
+ }
145
+ // No marker: a build predating it, or a caller passing a synthetic env.
146
+ // Fall back to the runtime flag, which only agrees with the build when the
147
+ // pipeline sets the same value in both scopes.
148
+ return isTruthyEnv(env.AGENT_NATIVE_DISABLE_RECURRING_JOBS)
149
+ ? { available: false, reason: "disabled-by-env" }
150
+ : { available: true, driver: "netlify-scheduled-function" };
151
+ }
152
+ // Everything below is driven by an in-process timer, and the runtime flag is
153
+ // what `shouldDisableRecurringJobsRuntime` reads before starting it. A build
154
+ // marker cannot speak for this branch: a build with recurring jobs turned off
155
+ // still starts the timer if the deployed env does not repeat the switch.
156
+ if (isTruthyEnv(env.AGENT_NATIVE_DISABLE_RECURRING_JOBS)) {
157
+ return { available: false, reason: "disabled-by-env" };
158
+ }
159
+ // Every other serverless host: the build emits a scheduled trigger only for
160
+ // Netlify, and a frozen isolate cannot hold a timer.
161
+ if (isServerlessRecurringJobsRuntime(env)) {
162
+ return { available: false, reason: "no-platform-scheduler" };
163
+ }
164
+ // Long-lived host. Whatever is left of the runtime gate here is the
165
+ // local/loopback branch, which needs AGENT_NATIVE_ENABLE_LOCAL_RECURRING_JOBS.
166
+ return shouldDisableRecurringJobsRuntime(env)
167
+ ? { available: false, reason: "local-development" }
168
+ : { available: true, driver: "in-process" };
169
+ }
@@ -9,7 +9,7 @@ import { _agentChatPromptSectionsForTests } from "./agent-chat/framework-prompts
9
9
  import { type AgentChatPluginOptions, type NitroPluginDef } from "./agent-chat/plugin-options.js";
10
10
  import { finalizeClaimedAgentChatProcessRunFailure } from "./agent-chat/process-run-failure.js";
11
11
  import { loadResourcesForPrompt } from "./agent-chat/prompt-resources.js";
12
- import { isNetlifyRecurringJobsRuntime, shouldDisableRecurringJobsRuntime } from "./agent-chat/recurring-jobs-runtime.js";
12
+ import { isNetlifyRecurringJobsRuntime, resolveRecurringJobsBuildMarker, scheduledTriggerAvailability, shouldDisableRecurringJobsRuntime } from "./agent-chat/recurring-jobs-runtime.js";
13
13
  import { shouldBlockInProductCodeEditingSurface } from "./agent-chat/request-surface.js";
14
14
  import { loadRunCodeToolEntries } from "./agent-chat/run-code-tools.js";
15
15
  export { loadResourcesForPrompt };
@@ -34,6 +34,8 @@ export { DEFAULT_DELEGATED_MAX_ITERATIONS, DEFAULT_DELEGATED_MAX_RUN_INPUT_TOKEN
34
34
  export { shouldBlockInProductCodeEditingSurface };
35
35
  export { loadRunCodeToolEntries };
36
36
  export { isNetlifyRecurringJobsRuntime };
37
+ export { resolveRecurringJobsBuildMarker };
38
+ export { scheduledTriggerAvailability };
37
39
  export { shouldDisableRecurringJobsRuntime };
38
40
  export { finalizeClaimedAgentChatProcessRunFailure };
39
41
  export declare function resolveInteractiveAgentRunOptions(options?: Pick<AgentChatPluginOptions, "runSoftTimeoutMs" | "runNoProgressTimeoutMs" | "durableBackgroundRuns">): {
@@ -120,7 +120,7 @@ import { resolveAgentChatMcpOptions } from "./agent-chat/mcp-options.js";
120
120
  import { resolveA2AAgentDelegationEnabled, } from "./agent-chat/plugin-options.js";
121
121
  import { finalizeClaimedAgentChatProcessRunFailure } from "./agent-chat/process-run-failure.js";
122
122
  import { loadResourcesForPrompt, promptResourceManifestSections, resourceScopeForOwner, } from "./agent-chat/prompt-resources.js";
123
- import { isNetlifyRecurringJobsRuntime, shouldDisableRecurringJobsRuntime, } from "./agent-chat/recurring-jobs-runtime.js";
123
+ import { isNetlifyRecurringJobsRuntime, resolveRecurringJobsBuildMarker, scheduledTriggerAvailability, shouldDisableRecurringJobsRuntime, } from "./agent-chat/recurring-jobs-runtime.js";
124
124
  import { isLocalhost, shouldBlockInProductCodeEditingSurface, } from "./agent-chat/request-surface.js";
125
125
  import { loadRunCodeToolEntries } from "./agent-chat/run-code-tools.js";
126
126
  import { createAgentEngineScriptEntries, createAgentLoopSettingsScriptEntries, createCallAgentScriptEntry, createChatScriptEntries, createDbScriptEntries, createDocsScriptEntries, createResourceScriptEntries, } from "./agent-chat/script-entries.js";
@@ -147,6 +147,8 @@ export { DEFAULT_DELEGATED_MAX_ITERATIONS, DEFAULT_DELEGATED_MAX_RUN_INPUT_TOKEN
147
147
  export { shouldBlockInProductCodeEditingSurface };
148
148
  export { loadRunCodeToolEntries };
149
149
  export { isNetlifyRecurringJobsRuntime };
150
+ export { resolveRecurringJobsBuildMarker };
151
+ export { scheduledTriggerAvailability };
150
152
  export { shouldDisableRecurringJobsRuntime };
151
153
  export { finalizeClaimedAgentChatProcessRunFailure };
152
154
  export function resolveInteractiveAgentRunOptions(options) {
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
- error?: undefined;
31
30
  ok: boolean;
32
31
  key: string;
33
32
  baseUrlKey?: string;
34
33
  scope: AgentEngineApiKeyScope;
34
+ error?: undefined;
35
35
  }>>;
36
36
  export {};
@@ -26,8 +26,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
26
26
  expiresAt?: undefined;
27
27
  ttlSeconds?: undefined;
28
28
  } | {
29
- error?: undefined;
30
29
  token: string;
31
30
  expiresAt: string;
32
31
  ttlSeconds: number;
32
+ error?: undefined;
33
33
  }>>;
@@ -20,6 +20,6 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- error?: undefined;
24
23
  text: string;
24
+ error?: undefined;
25
25
  }>>;
@@ -10,6 +10,7 @@ import { getViteDevRecoveryScript } from "../client/vite-dev-recovery-script.js"
10
10
  import { inferAgentNativeDeploymentEnvironment, mergeAgentNativeConfigs, readAgentNativeConfigEnv, resolveAgentNativeConfig, } from "../config.js";
11
11
  import { writeAgentNativeNitroPresetMarker } from "../deploy/nitro-preset.js";
12
12
  import { findWorkspaceRoot } from "../scripts/utils.js";
13
+ import { RECURRING_JOBS_BUILD_MARKER_ENV_VAR, resolveRecurringJobsBuildMarker, } from "../server/agent-chat/recurring-jobs-runtime.js";
13
14
  import { verifyEmbedSessionToken } from "../server/embed-session.js";
14
15
  import { EMBED_SESSION_COOKIE, EMBED_TOKEN_QUERY_PARAM, MCP_APP_CHAT_BRIDGE_QUERY_PARAM, } from "../shared/embed-auth.js";
15
16
  import { isMcpEmbedCorsOrigin, MCP_EMBED_CORS_ALLOW_HEADERS, MCP_EMBED_STATIC_ASSET_HEADERS, mcpEmbedStaticAssetRouteRules, shouldAllowMcpEmbedCredentials, } from "../shared/mcp-embed-headers.js";
@@ -2561,6 +2562,10 @@ function createNitroDevPlugin(options, appBasePath) {
2561
2562
  // injected into deployed Functions. Nitro is a separate server build,
2562
2563
  // so it needs the release ownership decision in its own replacement map.
2563
2564
  "process.env.AGENT_NATIVE_RELEASE_MIGRATIONS": JSON.stringify(process.env.AGENT_NATIVE_RELEASE_MIGRATIONS?.trim() || ""),
2565
+ // Same reason as the release owner above: the recurring-jobs decision
2566
+ // belongs to the build env, and Nitro is a separate server build with its
2567
+ // own replacement map.
2568
+ [`process.env.${RECURRING_JOBS_BUILD_MARKER_ENV_VAR}`]: JSON.stringify(resolveRecurringJobsBuildMarker(process.env)),
2564
2569
  },
2565
2570
  // Never auto-load test files as server handlers/plugins/middleware.
2566
2571
  // Nitro scans server/{plugins,middleware,routes,api}/*; a co-located
@@ -2913,6 +2918,11 @@ function createAgentNativeConfig(options = {}, command, userConfig = {}, mode =
2913
2918
  // netlify.toml environment is available to the build but not injected
2914
2919
  // into deployed Functions, so embed the decision in the server bundle.
2915
2920
  "process.env.AGENT_NATIVE_RELEASE_MIGRATIONS": JSON.stringify(process.env.AGENT_NATIVE_RELEASE_MIGRATIONS?.trim() || ""),
2921
+ // Recurring jobs are turned off (and their platform scheduled trigger
2922
+ // omitted) by the BUILD environment, which a deployed serverless runtime
2923
+ // never sees. Embed the decision so the Automations page reports what
2924
+ // will actually fire instead of inferring it from runtime-only markers.
2925
+ [`process.env.${RECURRING_JOBS_BUILD_MARKER_ENV_VAR}`]: JSON.stringify(resolveRecurringJobsBuildMarker(process.env)),
2916
2926
  ...(resolvedAppConfig.deployment?.environment
2917
2927
  ? {
2918
2928
  "process.env.AGENT_NATIVE_DEPLOYMENT_ENVIRONMENT": JSON.stringify(resolvedAppConfig.deployment.environment),
@@ -233,6 +233,12 @@ The scheduler is a framework plugin (the internal `processRecurringJobs()` routi
233
233
 
234
234
  </Callout>
235
235
 
236
+ <Callout id="doc-block-rj3" tone="warning">
237
+
238
+ **`AGENT_NATIVE_DISABLE_RECURRING_JOBS` is read in two scopes.** In the **build** environment it decides whether the platform's scheduled trigger is emitted at all — on Netlify that emitted function is the only durable driver, so a build with the switch on ships an app where no schedule can fire, whatever the deployed environment says. In the **runtime** environment it gates the in-process `setInterval` loop. Set it in the build environment to turn recurring jobs off for a deploy; the build records its decision so the **Automations** page can say schedules will never run instead of showing a next-run date nothing will honour.
239
+
240
+ </Callout>
241
+
236
242
  ## Debugging a job {#debugging}
237
243
 
238
244
  - Open the automation's details in **Automations**, or inspect
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.0.0-beta-20260820214042",
3
+ "version": "0.0.0-beta-20260820220930",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -427,8 +427,8 @@
427
427
  "y-protocols": "^1.0.7",
428
428
  "yjs": "^13.6.32",
429
429
  "zod": "^4.3.6",
430
- "@agent-native/toolkit": "^0.16.9",
431
- "@agent-native/recap-cli": "0.5.6"
430
+ "@agent-native/recap-cli": "0.0.0-beta-20260820220930",
431
+ "@agent-native/toolkit": "^0.16.9"
432
432
  },
433
433
  "devDependencies": {
434
434
  "@ai-sdk/anthropic": "^3.0.71",