feat(tasks): support markdown scheduled-task loops in .agents/loops
Adds markdown-based scheduled-task definitions ("loops") discovered from
.agents/loops/*.md (project scope, ancestor directories up to the
worktree root) and ~/.agents/loops/*.md (user scope), mirroring the
skills discovery pattern.
File format: YAML frontmatter (name, schedule cron, enabled, model as
provider/model, optional agent/timezone) plus the markdown body as the
execution prompt. Discovery and parsing live in
scheduled-tasks/loops.js; project-config gains reconcileLoopTasks which
runs inside the project write lock on every syncProject:
- identity by task name; a loop takes over a matching task, preserving
its id and runtime state (markdown wins on conflict with JSON)
- tasks whose loopFile is gone are unscheduled; JSON tasks are never
removed
- new loops are created under deterministic loop:<scope>:<name> ids
- project scope shadows user scope on name collisions
- malformed files are skipped with a warning and never block valid ones
Runtime state stays in the project config/state store; it is never
written to the markdown files. Module documentation updated with the
file format and reconciliation rules.
Fixes #2583
This commit is contained in:
@@ -5,7 +5,8 @@ Server-owned scheduled task runtime and routes for OpenChamber-only automation.
|
||||
## Scope
|
||||
|
||||
- Per-project scheduled task persistence is owned by `packages/web/server/lib/projects/project-config.js`.
|
||||
- Runtime orchestration and execution is owned by this module.
|
||||
- Markdown loop discovery/parsing is owned by `packages/web/server/lib/scheduled-tasks/loops.js`.
|
||||
- Runtime orchestration and execution is owned by `packages/web/server/lib/scheduled-tasks/runtime.js`.
|
||||
- This module is OpenChamber feature logic; it is intentionally separate from OpenCode proxy/runtime internals.
|
||||
|
||||
## Files
|
||||
@@ -17,11 +18,74 @@ Server-owned scheduled task runtime and routes for OpenChamber-only automation.
|
||||
- Session create + prompt_async execution
|
||||
- Emits OpenChamber task-run events
|
||||
|
||||
- `packages/web/server/lib/scheduled-tasks/loops.js`
|
||||
- Discovery of `.agents/loops/*.md` (project scope, ancestors up to the worktree root) and `~/.agents/loops/*.md` (user scope)
|
||||
- Frontmatter parsing into scheduled-task definitions
|
||||
- `syncProject` reconciles discovered loops with the persisted task list on every project sync (startup, task save/delete)
|
||||
|
||||
- `packages/web/server/lib/scheduled-tasks/routes.js`
|
||||
- Scheduled task CRUD endpoints
|
||||
- Manual run endpoint
|
||||
- OpenChamber events SSE stream endpoint
|
||||
|
||||
## Loop file format
|
||||
|
||||
Portable, git-commit-able scheduled-task definitions:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: daily-digest
|
||||
schedule: "0 9 * * *"
|
||||
enabled: true
|
||||
model: anthropic/claude-sonnet-4-5
|
||||
agent: plan
|
||||
timezone: Europe/Kyiv
|
||||
---
|
||||
Summarize repository changes since yesterday.
|
||||
```
|
||||
|
||||
Field mapping (model: `packages/ui/src/lib/scheduledTasksApi.ts`):
|
||||
|
||||
| Frontmatter | Task field |
|
||||
|---|---|
|
||||
| `name` | `name` (required) |
|
||||
| `schedule` | `schedule.kind: "cron"` + `schedule.cron` (required, cron-only in the portable format) |
|
||||
| `enabled` | `enabled` (default `true`) |
|
||||
| `model` | split on the first `/` into `execution.providerID` / `execution.modelID` (required) |
|
||||
| `agent` | `execution.agent` (optional) |
|
||||
| `timezone` | `schedule.timezone` (optional, IANA; defaults to the server zone) |
|
||||
| body | `execution.prompt` (required) |
|
||||
|
||||
`thinking_level` and `goalEnabled`/`goalTokenBudget` are not part of the portable
|
||||
format (UI/JSON-only today); `daily`/`weekly`/`once` schedules remain UI/JSON-only.
|
||||
Runtime state (`lastRunAt`, `nextRunAt`, `lastStatus`, `lastError`, `lastSessionId`,
|
||||
`lastDurationMs`) is never written to the markdown file — it continues to live in
|
||||
the project config state store.
|
||||
|
||||
## Loop reconciliation rules
|
||||
|
||||
`projectConfigRuntime.reconcileLoopTasks(projectID, loops)` runs inside the
|
||||
project write lock on every `syncProject` when the project path is known:
|
||||
|
||||
- **Identity is the task name.** A loop whose name matches an existing task
|
||||
takes that task over: its schedule/execution/enabled are overwritten from the
|
||||
file while the task's `id` and runtime `state` are preserved (markdown wins
|
||||
on conflict with JSON-configured tasks).
|
||||
- **Deletion.** A task carrying the `loopFile` marker whose loop file is no
|
||||
longer discovered (removed or renamed) is unscheduled (removed from the
|
||||
config). The marker is persisted in the config file, so removal is detected
|
||||
across restarts. JSON-configured tasks without the marker are never removed.
|
||||
- **Creation.** Loops without a matching task are created under a deterministic
|
||||
`loop:<scope>:<name>` id so runtime state survives restarts.
|
||||
- **Scope precedence.** Project-scope loops shadow user-scope loops with the
|
||||
same name; among project files the nearest ancestor wins.
|
||||
- **Malformed files** (missing `name`/`schedule`/`model`/body, invalid cron,
|
||||
unreadable) are skipped with a warning and never block valid loops in the
|
||||
same or other scopes.
|
||||
- **UI edits** to a loop-sourced task are preserved in the config but the loop
|
||||
file remains authoritative: the next reconciliation re-applies the file's
|
||||
definition (including `enabled`). Use `enabled: false` in the file to disable.
|
||||
|
||||
## Public exports (runtime.js)
|
||||
|
||||
- `createScheduledTasksRuntime(dependencies)`
|
||||
|
||||
Reference in New Issue
Block a user