Files
boxmaker/docs/plans/M5d2/07-loopd-scheduler-cron.md
T
kyleandGrok 4.6 2eb0ffd6d7 docs: M5d-2: read only the implementer-log header
Task 06 opened the whole implementer log (~360 kB) after reading supporting
modules; OpenCode returned "Context size has been exceeded" and made no
commit. AGENTS.md and tasks 06-10 now say to read the first 12 lines only.

Co-Authored-By: Grok 4.6 <noreply@x.ai>
2026-10-03 14:42:59 -07:00

113 lines
5.4 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.
**Read only what the step names.** Step 1 is the `cp`. At the log step, read only the first 12
lines of `docs/implementer-log.md`. Do not load the rest of that 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). After `gate: ok`, do not add up
`test result:` lines.
- [ ] **7. Log and commit.** Task cell `M5d2/07-loopd-scheduler-cron`. Read only the first 12
lines of `docs/implementer-log.md`; insert the row under the header. 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's last line is not `gate: ok`; 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.