* feat: add scheduled tasks MVP
* fix: harden scheduled task execution semantics
* feat(scheduled-tasks): preset-driven schedule form with timezone and live preview
Replace the raw cron input with a preset Select (hourly/daily/weekly/monthly/custom)
plus structured inputs (time picker, weekday toggles, day-of-month), datetime-local
for one-time tasks, a timezone selector defaulting to the browser timezone, and a
live human-readable preview. Reuses one ScheduledTaskScheduleInput for create and
edit; backend contract unchanged; zero new deps (pure Intl + DST-safe offset helpers).
* feat(scheduled-tasks): full-page i18n + recipe templates + E2E locale pin
Localize the rest of the scheduled-tasks page (filters, detail pane, actions,
edit form, run list, enum values) via t.scheduledTasks.* in en/zh. Add four
built-in recipe templates (GitHub Trending, news digest, issue triage, weekly
report) exposed as a chip row that pre-fills title + prompt + schedule. Pin
Playwright locale to en-US so E2E selectors stay stable against i18n. No backend
change, no new deps.
* fix(scheduled-tasks): idempotent 0003 migration, update head constants, future-date once test
Merge with main surfaced three CI failures:
- 0003_scheduled_tasks create_table collided with legacy test seeds that
build from full metadata; guard with inspector.has_table so the revision
no-ops when the table already exists (0004/0005 are already idempotent via
_helpers.py).
- persistence bootstrap concurrency/regression tests pinned HEAD to main's
0002_runs_token_usage; bump to the new head 0005_scheduled_task_thread_nullable.
- once-task router test used a fixed past run_at and tripped the
must-be-in-the-future validation; use a future date.
* address review: ok-check, 502 for trigger failure, mock fields, migration filename, doc fences
- fetchThreadScheduledTasks now checks response.ok like the other fetchers.
- trigger endpoint returns 502 (not 409) when dispatch fails outright, so
clients can distinguish a real conflict from a server-side failure.
- E2E mock normalizes scheduled-task objects with context_mode/last_thread_id
and nullable thread_id, matching the backend contract the UI renders against.
- Rename 0002_scheduled_tasks.py -> 0003_scheduled_tasks.py to match its
revision id (file was renamed in spirit already; filename now follows).
- CONFIGURATION.md: close the Tool Groups yaml fence and drop the stray fence
after the Scheduler notes so the sections render correctly.
* fix(scheduled-tasks): harden lease, poller, config, and frontend UX after review
* fix(scheduled-tasks): harden run lifecycle, overlap skip, non_interactive gating, and DST conversion after review
- defer a once task's terminal status to the run-completion hook; the task
stays running until the real outcome, and a startup sweep cancels once
tasks orphaned by a crash (launch-time 'completed' could stick forever)
- record interrupted runs as a distinct 'interrupted' run status with a
readable message; an interrupted once task ends 'cancelled', not 'failed'
- enforce overlap_policy=skip for fresh_thread_per_run via an active-run
pre-check (same-thread ConflictError can never fire across fresh threads)
- protect terminal run statuses from the late launch-path 'running' write
- honor context.non_interactive only for internally-authenticated callers;
arbitrary clients can no longer strip ask_clarification
- fix DST-stale timezone offset in zonedLocalToUtcIso by re-deriving the
offset at the resolved instant (once tasks fired an hour late around
spring-forward and the create->edit round-trip diverged)
- drop dead ScheduledTaskRunRepository.update_by_run_id; share one Gateway
API error helper between channels and scheduled-tasks frontends
* fix(scheduled-tasks): close review round-3 gaps in guards, concurrency, and API ergonomics
- scrub internal-only context keys (non_interactive) from the assembled run
config for non-internal callers: gating body.context alone left the same
key smuggle-able through the free-form body.config copied verbatim by
build_run_config
- guard update_after_launch with protect_terminal so the launch bookkeeping
write cannot clobber a once task already finalized by a fast-failing run's
completion hook (parent-row sibling of the run-row guard)
- reject a manual trigger while the task has an active run (409) instead of
launching a duplicate concurrent run on fresh_thread_per_run
- re-arm a terminal once task to enabled when PATCH pushes run_at into the
future; previously the endpoint returned 200 with a next_run_at that could
never be claimed
- make max_concurrent_runs a real global cap: each poll claims only into the
remaining budget of active (queued/running) scheduled runs
- paginate GET /scheduled-tasks/{id}/runs (limit<=200, offset) and push the
thread filter of /threads/{id}/scheduled-tasks into SQL
- stamp context.user_id on scheduler-launched runs, matching IM channels, so
user-scoped guardrail providers see the owning user
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
3.4 KiB
Scheduled-tasks page: full i18n + recipe templates — design
Follow-up to the preset-driven schedule form (commit 1ca27a73). Scope kept
front-end only and backend-contract-free so it stays reviewable inside PR #3898.
Background
The preset form i18n'd the schedule section but left the rest of the
scheduled-tasks page in hard-coded English (filters, detail pane, actions, edit
form, run list). A zh-CN user saw a half-English page right next to the
i18n'd schedule input. Separately, the page still required users to hand-write
prompts for the headline use cases (GitHub Trending, news digest, issue triage,
weekly report).
Goals
- Every visible string on
/workspace/scheduled-tasksgoes throught.scheduledTasks.*(en + zh). - One-click "quick create" via four built-in recipe templates that pre-fill title + prompt + schedule.
- No backend change. No new dependency.
Design
Full i18n
Added to the scheduledTasks i18n section (types + en-US + zh-CN):
create,context,filters,detail,actions,edit— UI labels.status,runTrigger,runStatus— enum maps so list/detail/run rows render localized values instead of rawenabled/manual/success.recipes— recipe labels (below).
The page maps raw enum values through small lookup helpers
(statusLabel, scheduleTypeLabel, contextModeLabel, runTriggerLabel,
runStatusLabel) that fall back to the raw value if unknown. E2E selectors
switched from exact English strings to case-insensitive regex so they survive
the i18n capitalization change (cron · enabled → Cron · Enabled).
Recipe templates
New frontend/src/core/scheduled-tasks/recipes.ts exports RECIPES: Recipe[]
with { id, icon, titleKey, prompt, schedule }. Four starters:
| id | icon | schedule | prompt gist |
|---|---|---|---|
trending |
🔥 | daily 09:00 | web_search GitHub Trending, summarize top 10 |
news |
📰 | daily 09:00 | web_search top tech news, 5-item digest |
issues |
🏷️ | daily 09:00 | triage open issues in {{repo}} (user fills placeholder) |
weekly |
📅 | weekly Mon 09:00 | weekly report |
schedule.timezone is intentionally "" so the ScheduledTaskScheduleInput
falls back to the browser timezone when applied. Recipe titles/descriptions live
in i18n (recipes.{id}.{title,desc}); the file only stores the prompt + schedule.
Wiring into the create form
- A
createNoncecounter state keys the create-formScheduledTaskScheduleInput(key={createNonce}). Bumping the nonce forces the component to remount so the recipe's schedule is re-initialized into itsuseState, not just emitted. applyRecipe(recipe)sets title + prompt + createSchedule + contextMode + bumps the nonce.- A chip row (
data-testid="schedule-recipes") renders above the form fields.
Verification
pnpm check0 errors;pnpm testall green (403 unit tests).- Real browser (chrome-devtools, zh-CN): 4 recipe chips render, clicking
「每周周报」pre-fills title + prompt + preset (weekly) + preview
每周 周一 09:00 (Asia/Shanghai). - Playwright
scheduled-taskssuite re-verified with regex selectors.
Non-goals (next session)
- Backend dispatch targets /
task_type(RFC phase 2) — kept out because it changes schema/API/executor and would muddy this PR's review. - Backend-stored / user-authored recipes — front-end built-ins are enough for the first cut (YAGNI).