fix(dashboard): populate cron delivery dropdown from configured platforms (#40218)
* fix: respect disabled auto-compaction on context overflow Port from anomalyco/opencode#30749. When compression.enabled is false, NO automatic compaction trigger may fire. The proactive token-threshold paths (preflight + post-response should_compress gate) already honoured the setting, but the three provider-overflow recovery paths in the agent loop — long-context-tier 429, 413 payload-too-large, and context-overflow — called _compress_context() unconditionally, silently compressing and rotating the session against the user's explicit choice. Add a single guard at the top of the overflow-recovery dispatch: when compression is disabled and the error is one of those three overflow classes, surface a terminal error (compaction_disabled: True) telling the user to /compress manually, /new, switch to a larger-context model, or reduce attachments. Manual /compress (force=True) is unaffected — it never enters this loop. Tests: new TestOverflowWithCompactionDisabled (413 + 400 overflow don't compress when disabled; control case still compresses when enabled). Existing overflow-recovery tests updated to enable compaction explicitly (they verify the recovery fires); fixture defaults flipped to True to match production (compression.enabled defaults to True). * fix(dashboard): populate cron delivery dropdown from configured platforms The dashboard cron-create/edit dropdown hardcoded five delivery options (local, telegram, discord, slack, email), so users on Matrix — or any other backend-supported platform — had no way to pick their channel even though the cron scheduler delivers to all of them. It also offered Telegram/Discord/etc. to users who never set those up. - cron/scheduler.py: add cron_delivery_targets() — the single source of truth. Intersects gateway-configured platforms with cron-deliverable ones and reports whether each platform's home channel is set. - web_server.py: GET /api/cron/delivery-targets exposes that list (+ the implicit local option) to the dashboard. - CronPage.tsx: both modals render options from the endpoint. Configured platforms missing a home channel still appear, annotated "set a home channel first" (option B), so the user knows what to fix. Edit modal preserves a job's current target even if it's no longer configured. Local-only state shows a "configure a platform under Channels" hint. Validation: scheduler + endpoint E2E'd with a Matrix gateway (home set and unset); 5 new tests; tests/cron + tests/hermes_cli/test_web_server green (366 passed).
This commit is contained in:
@@ -279,6 +279,9 @@ export const en: Translations = {
|
||||
discord: "Discord",
|
||||
slack: "Slack",
|
||||
email: "Email",
|
||||
needsHomeChannel: "set a home channel first",
|
||||
noneConfigured:
|
||||
"No messaging platforms configured. Set one up under Channels to deliver reports.",
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -297,6 +297,8 @@ export interface Translations {
|
||||
discord: string;
|
||||
slack: string;
|
||||
email: string;
|
||||
needsHomeChannel?: string;
|
||||
noneConfigured?: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -390,6 +390,8 @@ export const api = {
|
||||
// Cron jobs
|
||||
getCronJobs: (profile = "all") =>
|
||||
fetchJSON<CronJob[]>(`/api/cron/jobs?profile=${encodeURIComponent(profile)}`),
|
||||
getCronDeliveryTargets: () =>
|
||||
fetchJSON<{ targets: CronDeliveryTarget[] }>("/api/cron/delivery-targets"),
|
||||
createCronJob: (job: { prompt: string; schedule: string; name?: string; deliver?: string }, profile = "default") =>
|
||||
fetchJSON<CronJob>(`/api/cron/jobs?profile=${encodeURIComponent(profile)}`, {
|
||||
method: "POST",
|
||||
@@ -1513,6 +1515,13 @@ export interface CronJob {
|
||||
last_error?: string | null;
|
||||
}
|
||||
|
||||
export interface CronDeliveryTarget {
|
||||
id: string;
|
||||
name: string;
|
||||
home_target_set: boolean;
|
||||
home_env_var: string | null;
|
||||
}
|
||||
|
||||
export interface SkillInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
|
||||
+76
-31
@@ -6,7 +6,7 @@ import { Select, SelectOption } from "@nous-research/ui/ui/components/select";
|
||||
import { Spinner } from "@nous-research/ui/ui/components/spinner";
|
||||
import { H2 } from "@nous-research/ui/ui/components/typography/h2";
|
||||
import { api } from "@/lib/api";
|
||||
import type { CronJob, ProfileInfo } from "@/lib/api";
|
||||
import type { CronJob, CronDeliveryTarget, ProfileInfo } from "@/lib/api";
|
||||
import { DeleteConfirmDialog } from "@/components/DeleteConfirmDialog";
|
||||
import {
|
||||
DEFAULT_SCHEDULE_STATE,
|
||||
@@ -157,6 +157,9 @@ export default function CronPage() {
|
||||
onClose: closeCreateModal,
|
||||
});
|
||||
const [deliver, setDeliver] = useState("local");
|
||||
const [deliveryTargets, setDeliveryTargets] = useState<CronDeliveryTarget[]>([
|
||||
{ id: "local", name: "Local", home_target_set: true, home_env_var: null },
|
||||
]);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const createProfile = selectedProfile === "all" ? "default" : selectedProfile;
|
||||
|
||||
@@ -198,12 +201,76 @@ export default function CronPage() {
|
||||
.catch(() => setProfiles([]));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.getCronDeliveryTargets()
|
||||
.then((res) => setDeliveryTargets(res.targets))
|
||||
.catch(() =>
|
||||
// Fall back to local-only so the modal still works if the endpoint fails.
|
||||
setDeliveryTargets([
|
||||
{ id: "local", name: "Local", home_target_set: true, home_env_var: null },
|
||||
]),
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadJobs();
|
||||
}, [loadJobs]);
|
||||
|
||||
const scheduleString = buildScheduleString(scheduleState);
|
||||
|
||||
// Label for a delivery option. Configured platforms missing their cron home
|
||||
// channel are still offered (option B), annotated so the user knows what to
|
||||
// fix rather than wondering why delivery silently no-ops.
|
||||
const deliverLabel = useCallback(
|
||||
(target: CronDeliveryTarget): string => {
|
||||
const base = target.id === "local" ? t.cron.delivery.local : target.name;
|
||||
if (target.id !== "local" && !target.home_target_set) {
|
||||
const hint = t.cron.delivery.needsHomeChannel ?? "set a home channel first";
|
||||
return `${base} — ${hint}`;
|
||||
}
|
||||
return base;
|
||||
},
|
||||
[t.cron.delivery],
|
||||
);
|
||||
|
||||
const renderDeliverOptions = useCallback(
|
||||
() =>
|
||||
deliveryTargets.map((target) => (
|
||||
<SelectOption key={target.id} value={target.id}>
|
||||
{deliverLabel(target)}
|
||||
</SelectOption>
|
||||
)),
|
||||
[deliveryTargets, deliverLabel],
|
||||
);
|
||||
|
||||
// The edit modal must always show the job's current target, even if that
|
||||
// platform is no longer configured (e.g. job created via CLI, or the
|
||||
// gateway was later removed) — otherwise the value would silently vanish
|
||||
// from the dropdown and saving would drop it.
|
||||
const renderEditDeliverOptions = useCallback(
|
||||
(current: string) => {
|
||||
const known = new Set(deliveryTargets.map((target) => target.id));
|
||||
const options = deliveryTargets.map((target) => (
|
||||
<SelectOption key={target.id} value={target.id}>
|
||||
{deliverLabel(target)}
|
||||
</SelectOption>
|
||||
));
|
||||
if (current && !known.has(current)) {
|
||||
options.push(
|
||||
<SelectOption key={current} value={current}>
|
||||
{current}
|
||||
</SelectOption>,
|
||||
);
|
||||
}
|
||||
return options;
|
||||
},
|
||||
[deliveryTargets, deliverLabel],
|
||||
);
|
||||
|
||||
const onlyLocalAvailable =
|
||||
deliveryTargets.filter((target) => target.id !== "local").length === 0;
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!prompt.trim() || !scheduleString) {
|
||||
showToast(`${t.cron.prompt} & ${t.cron.schedule} required`, "error");
|
||||
@@ -447,22 +514,14 @@ export default function CronPage() {
|
||||
value={deliver}
|
||||
onValueChange={(v) => setDeliver(v)}
|
||||
>
|
||||
<SelectOption value="local">
|
||||
{t.cron.delivery.local}
|
||||
</SelectOption>
|
||||
<SelectOption value="telegram">
|
||||
{t.cron.delivery.telegram}
|
||||
</SelectOption>
|
||||
<SelectOption value="discord">
|
||||
{t.cron.delivery.discord}
|
||||
</SelectOption>
|
||||
<SelectOption value="slack">
|
||||
{t.cron.delivery.slack}
|
||||
</SelectOption>
|
||||
<SelectOption value="email">
|
||||
{t.cron.delivery.email}
|
||||
</SelectOption>
|
||||
{renderDeliverOptions()}
|
||||
</Select>
|
||||
{onlyLocalAvailable && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t.cron.delivery.noneConfigured ??
|
||||
"No messaging platforms configured. Set one up under Channels to deliver reports."}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
@@ -552,21 +611,7 @@ export default function CronPage() {
|
||||
value={editDeliver}
|
||||
onValueChange={(v) => setEditDeliver(v)}
|
||||
>
|
||||
<SelectOption value="local">
|
||||
{t.cron.delivery.local}
|
||||
</SelectOption>
|
||||
<SelectOption value="telegram">
|
||||
{t.cron.delivery.telegram}
|
||||
</SelectOption>
|
||||
<SelectOption value="discord">
|
||||
{t.cron.delivery.discord}
|
||||
</SelectOption>
|
||||
<SelectOption value="slack">
|
||||
{t.cron.delivery.slack}
|
||||
</SelectOption>
|
||||
<SelectOption value="email">
|
||||
{t.cron.delivery.email}
|
||||
</SelectOption>
|
||||
{renderEditDeliverOptions(editDeliver)}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user