Files
boxmaker/docs/plans/M5d2/07-loopd-scheduler-cron.md
T
kyleandGrok 4.6 2a48b7f36d docs: M5d-2: after gate: ok, do not sum test results
Task 01 told Ornith to add up every `test result:` N passed. A greedy
sed kept the last digit (1512 became 1022) and it stopped with the
correct T1 and given test uncommitted. T50/T59: the last line of
make gate is the count; summing is not a stop.

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

5.2 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). After gate: ok, do not add up test result: lines.

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