Files
boxmaker/docs/plans/M5d2/07-loopd-scheduler-cron.md
T
kyleandGrok 4.6 df987c3058 docs: M5d-2 checklist: given-test lifetimes and cron AND
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>
2026-10-03 01:31:07 -07:00

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 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.

    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.