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:
Teknium
2026-06-05 20:23:54 -07:00
committed by GitHub
parent 150687447b
commit 50f9ad70fc
8 changed files with 259 additions and 31 deletions
+3
View File
@@ -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.",
},
},
+2
View File
@@ -297,6 +297,8 @@ export interface Translations {
discord: string;
slack: string;
email: string;
needsHomeChannel?: string;
noneConfigured?: string;
};
};
+9
View File
@@ -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
View File
@@ -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>