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>
5.1 KiB
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 testcrates/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")). OnErr, logloopd: {e}and skip cron this tick (the process stays up: README known limits). OnOk, for eachJobin order:cron::due(&job.schedule, now).Err→ log and skip that job.Ok(false)→ skip.- Ledger path
{home}/jobs/runs.jsonl. Iflive_sessionisSome(_), pushSkippedJob { name }and continue. (Task 06 has already closed leftovers that were not busy.) session_id(&job.name, now).Err→ log and skip.ctx.hold(session.clone()).Err→SkippedJob.Ok(held)→ create the session (Session::createwithassembleandslots.background; ifExists,openinstead),jobs::appendStarted, runrun_turnthenfinish_turnwith user textformat!("It is {}.\n\n{}", now.to_rfc3339(), job.prompt), speakerNone, thenappendFinishedorFailed(turnErr→Failed).drop(held). PushTickWork::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.rsstays ≤ 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_sessiontask 05 cron::duetask 04 Context::holdtask 06 run_turn/finish_turnsame 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.
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::Jobalready exists) anda_due_job_starts_a_new_session_on_the_background_slotfails:workis empty, not aJob.a_job_not_due_does_not_runmay pass. -
3. The edit. Add the job loop.
cargo check -p loopdprints no error and no warning.wc -l crates/loopd/src/scheduler.rsprints a number ≤ 500. -
4. The tests.
cargo test -p loopd --test scheduler_cron --test scheduler_heartbeat --test jobs --test cron: every test passes, 6 inscheduler_cron. -
5. Check.
git diff --statlists exactlycrates/loopd/src/scheduler.rs. -
6. Run the gate.
cargo fmt --all, thenmake gate, timeout 600000 ms. Last linegate: ok. Test count 1557 (1551 before, plus 6). -
7. Log and commit. Task cell
M5d2/07-loopd-scheduler-cron. Stage withgit add crates/loopd docs/implementer-log.md Cargo.lock, then commit asAGENTS.mdsays.
Done when
make gateprintsgate: okwith 1557 tests.git diff HEAD~1 --statlists onlycrates/loopd/src/scheduler.rs,crates/loopd/tests/scheduler_cron.rsand the log.
Stop and report if
- The branch check fails; step 2 differs (tests do not compile, or a different assertion
fails);
cargo checkwarns; a test fails; the gate count is not 1557; you reuse a session across runs; you sendslots.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.