scheduler_heartbeat imported scheduler::{self} without using it, which
clippy -D warnings would fail at task 06. The live-previous cron tick now
asserts a held leftover stays started.
Co-Authored-By: Grok 4.6 <noreply@x.ai>
108 lines
5.1 KiB
Markdown
108 lines
5.1 KiB
Markdown
# M5d-2 task 07: cron tick
|
|
|
|
**Branch:** `m5d2`. Check it: `git branch --show-current` prints `m5d2` and `git status --short`
|
|
is empty; otherwise stop. Never switch branch.
|
|
**Commit subject:** `loopd: cron tick, new session per run`
|
|
**Log row:** the Task cell is `M5d2/07-loopd-scheduler-cron`.
|
|
|
|
## Goal
|
|
|
|
After the heartbeat work, `tick` loads `{home}/jobs/*.toml` and runs each job whose schedule
|
|
matches `now` (spec 4.2). A new session `job-<name>-<yyyymmdd>t<hhmm>` on the background slot.
|
|
Skip the tick when a previous run of that name is still live. Leftover `started` from a dead
|
|
process becomes `failed` on the first tick (already started in task 06: only when that session
|
|
is not busy). Speaker `None`. Same time-line prefix as heartbeat.
|
|
|
|
## Every place that changes
|
|
|
|
| Place | What happens there |
|
|
|---|---|
|
|
| `crates/loopd/src/scheduler.rs` | after heartbeat, iterate jobs (edit T1) |
|
|
| `crates/loopd/tests/scheduler_cron.rs` | new, given |
|
|
| everything else | **no change**: not `TickWork`, not `hold`, not heartbeat |
|
|
|
|
## Files
|
|
|
|
- Copy, from `docs/plans/M5d2/files/` (the same path under it): the new test
|
|
`crates/loopd/tests/scheduler_cron.rs`.
|
|
- Modify: `crates/loopd/src/scheduler.rs`, `docs/implementer-log.md`.
|
|
- Do not touch: any other file.
|
|
|
|
## Facts you need
|
|
|
|
- **Load.** `jobs::load(&ctx.cfg.paths.home.join("jobs"))`. On `Err`, log `loopd: {e}` and
|
|
skip cron this tick (the process stays up: README known limits). On `Ok`, for each `Job` in
|
|
order:
|
|
1. `cron::due(&job.schedule, now)`. `Err` → log and skip that job. `Ok(false)` → skip.
|
|
2. Ledger path `{home}/jobs/runs.jsonl`. If `live_session` is `Some(_)`, push
|
|
`SkippedJob { name }` and continue. (Task 06 has already closed leftovers that were not
|
|
busy.)
|
|
3. `session_id(&job.name, now)`. `Err` → log and skip.
|
|
4. `ctx.hold(session.clone())`. `Err` → `SkippedJob`. `Ok(held)` → create the session
|
|
(`Session::create` with `assemble` and `slots.background`; if `Exists`, `open` instead),
|
|
`jobs::append` `Started`, run `run_turn` then `finish_turn` with user text
|
|
`format!("It is {}.\n\n{}", now.to_rfc3339(), job.prompt)`, speaker `None`, then
|
|
`append` `Finished` or `Failed` (turn `Err` → `Failed`). `drop(held)`. Push
|
|
`TickWork::Job { name, session }`.
|
|
- **Two due minutes.** Two ticks at 06:59 and 07:00 with `* * * * *` make two session ids.
|
|
- **Not due.** `0 7 * * *` at 06:59 pushes nothing.
|
|
- **`scheduler.rs` stays ≤ 500 lines.** After this task it is about 200.
|
|
- The items the edits use. This table is complete.
|
|
|
|
| Item | What it does |
|
|
|---|---|
|
|
| `jobs::load` / `session_id` / `append` / `live_session` | task 05 |
|
|
| `cron::due` | task 04 |
|
|
| `Context::hold` | task 06 |
|
|
| `run_turn` / `finish_turn` | same as heartbeat |
|
|
|
|
## The edits, exactly
|
|
|
|
There is no quoted "before" for a unique heartbeat-only block: you add a loop after the
|
|
heartbeat section of `tick`, before `work` is returned. If `tick` currently ends with
|
|
`work` after heartbeat, insert the job loop there. If you cannot find a single place to
|
|
insert without touching heartbeat, stop.
|
|
|
|
Do not change heartbeat behaviour. Do not add a sleep.
|
|
|
|
## Steps
|
|
|
|
- [ ] **1. Copy the given test.**
|
|
|
|
```sh
|
|
cp docs/plans/M5d2/files/crates/loopd/tests/scheduler_cron.rs crates/loopd/tests/
|
|
```
|
|
|
|
- [ ] **2. See it fail.** `cargo test -p loopd --test scheduler_cron`, timeout 600000 ms: the
|
|
tests compile (`TickWork::Job` already exists) and `a_due_job_starts_a_new_session_on_the_background_slot`
|
|
fails: `work` is empty, not a `Job`. `a_job_not_due_does_not_run` may pass.
|
|
- [ ] **3. The edit.** Add the job loop. `cargo check -p loopd` prints no error and no
|
|
warning. `wc -l crates/loopd/src/scheduler.rs` prints a number ≤ 500.
|
|
- [ ] **4. The tests.** `cargo test -p loopd --test scheduler_cron --test scheduler_heartbeat
|
|
--test jobs --test cron`: every test passes, 6 in `scheduler_cron`.
|
|
- [ ] **5. Check.** `git diff --stat` lists exactly `crates/loopd/src/scheduler.rs`.
|
|
- [ ] **6. Run the gate.** `cargo fmt --all`, then `make gate`, timeout 600000 ms. Last line
|
|
`gate: ok`. Test count **1557** (1551 before, plus 6).
|
|
- [ ] **7. Log and commit.** Task cell `M5d2/07-loopd-scheduler-cron`. Stage with
|
|
`git add crates/loopd docs/implementer-log.md Cargo.lock`, then commit as `AGENTS.md` says.
|
|
|
|
## Done when
|
|
|
|
- `make gate` prints `gate: ok` with 1557 tests.
|
|
- `git diff HEAD~1 --stat` lists only `crates/loopd/src/scheduler.rs`,
|
|
`crates/loopd/tests/scheduler_cron.rs` and the log.
|
|
|
|
## Stop and report if
|
|
|
|
- The branch check fails; step 2 differs (tests do not compile, or a different assertion
|
|
fails); `cargo check` warns; a test fails; the gate count is not 1557; you reuse a session
|
|
across runs; you send `slots.main`; you set a speaker; you skip leftovers that are not busy;
|
|
a heartbeat test fails.
|
|
|
|
## What the given tests pin
|
|
|
|
A due job creates `job-mail-…` on slot 1 with the time line and the prompt; a job not due
|
|
does nothing; a live previous run (Started + `hold`) is `SkippedJob` with no request and the
|
|
ledger still says `started` (not `failed`); leftover `started` is `failed` on the first tick
|
|
(the due job may then run); speaker is `None`; two due minutes make two sessions.
|