session_slot and the scheduler tests held a temporary JSON Value across statements (E0716). Cron now pins DOM and DOW as AND, which the given tests had not. Gate counts from task 04 on move up by one. 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; leftover
|
|
`started` is `failed` on the first tick (the due job may then run); speaker is `None`; two due
|
|
minutes make two sessions.
|