diff --git a/README.md b/README.md index 1eaf364..ace378a 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,9 @@ What the dashboard can now do (all state-changing actions require `ADMIN_TOKEN`) enqueues a `sync` command so the worker clones it. Manual mode (existing `root_dir`) still works; every project with a remote gets a **Pull now** button, and spawn always pulls first. -- **Schedules** — recurring agent spawns: a name prefix, a prompt, and an interval. The +- **Schedules** — recurring agent spawns: a name prefix, a prompt, an interval, and + optionally a **model backend** (the same dropdown the spawn form has — every fired run + spawns on it). The worker fires each due schedule as a normal queued `spawn` with a timestamped agent name (`nightly-20260710-090000`), so runs are fresh, stateless agents and show up in Activity. The canonical prompt keeps its state in the repo: *"Read @notes.md, continue diff --git a/docs/local-models.md b/docs/local-models.md index 3ef07fe..7afc7db 100644 --- a/docs/local-models.md +++ b/docs/local-models.md @@ -14,7 +14,8 @@ thing a **model backend** changes is the environment of that one agent's process | `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` | `1` (skip sidecar calls a local endpoint won't serve; override via the row's env map) | Register backends on the dashboard's **Claude → Models** tab (or `POST /claude/models`), -then pick one from the **Model** dropdown when spawning an agent. No selection = the +then pick one from the **Model** dropdown when spawning an agent — or on a **Schedule**, +so every fired run spawns on that backend. No selection = the worker's logged-in Claude subscription, exactly as before. The agent is *pinned* to its backend: resumes come back up on the same one, and deleting a backend makes resumes of its agents fail loudly rather than silently falling back to the subscription. diff --git a/frontend/components/sections/SchedulesSection.tsx b/frontend/components/sections/SchedulesSection.tsx index 18724ed..4f06cad 100644 --- a/frontend/components/sections/SchedulesSection.tsx +++ b/frontend/components/sections/SchedulesSection.tsx @@ -36,7 +36,7 @@ function intervalLabel(seconds: number): string { return `every ${seconds}s`; } -const emptyForm = { name_prefix: "", task: "", interval: "3600", role: "" }; +const emptyForm = { name_prefix: "", task: "", interval: "3600", role: "", model_id: "" }; export function SchedulesSection() { const s = useDashboard(); @@ -46,6 +46,21 @@ export function SchedulesSection() { () => s.projects.map((p) => ({ value: p.id, label: p.id })), [s.projects], ); + /* Same choice the spawn form offers: Claude subscription by default, plus every + * enabled backend from the Claude page's Models tab. Every fired run spawns on it. */ + const modelOpts = useMemo( + () => [ + { value: "", label: "Claude (subscription)" }, + ...s.claudeModels + .filter((m) => m.enabled) + .map((m) => ({ value: String(m.id), label: `${m.name} (${m.model})` })), + ], + [s.claudeModels], + ); + const modelName = useMemo(() => { + const byId = new Map(s.claudeModels.map((m) => [m.id, m.name])); + return (id: number | null | undefined) => (id == null ? null : byId.get(id) ?? `#${id}`); + }, [s.claudeModels]); const create = async () => { const ok = await s.createSchedule(s.selectedProjectId, { @@ -53,6 +68,7 @@ export function SchedulesSection() { task: form.task, interval_seconds: Number(form.interval), role: form.role, + model_id: form.model_id, }); if (ok) setForm(emptyForm); }; @@ -107,6 +123,12 @@ export function SchedulesSection() { onChange={(v) => setForm({ ...form, role: v })} options={ROLE_OPTS} /> +