6.6 KiB
M6a task 04: heartbeat and cron yield to a held subagent
Branch: m6a. Check it: git branch --show-current prints m6a and git status --short
is empty; otherwise stop. Never switch branch.
Commit subject: loopd: scheduler yields to a held subagent
Log row: the Task cell is M6a/04-loopd-scheduler-skip.
First action. Run this before any Read of crate source. Do not ls crates/loopd/tests/.
Do not Read tests/support/, turn.rs, session.rs, docs/implementer-log.md (except the
first 12 lines at the log step). The quoted blocks are the only edits.
cp docs/plans/M6a/files/crates/loopd/tests/scheduler_subagent.rs crates/loopd/tests/
Goal
While any held session id starts with sub-, a due heartbeat does not start and a due
cron job does not start (spec 3.3). SlotGate is FIFO; skipping here is what stops
heartbeat and cron from jumping a child that is waiting on the background slot. Drop the
hold and the next tick runs them.
Leftovers still close on the first tick. Compaction already holds its session. Tests call
tick(ctx, now); no sleeps (T27).
Every place that changes
| Place | What happens there |
|---|---|
crates/loopd/src/channel.rs |
holds_prefix |
crates/loopd/src/scheduler.rs |
skip heartbeat after due; skip jobs after heartbeat returns |
crates/loopd/tests/scheduler_subagent.rs |
new, given |
| everything else | no change: not subagent.rs, not brokerd |
Files
- Copy, from
docs/plans/M6a/files/(the same path under it): the new testcrates/loopd/tests/scheduler_subagent.rs. - Modify:
crates/loopd/src/channel.rs,crates/loopd/src/scheduler.rs,docs/implementer-log.md. - Do not touch: any other file. Do not edit
crates/loopd/tests/scheduler_heartbeat.rsorscheduler_cron.rs.
Facts you need
busyis private. Tests cannot see the set.holds_prefixlocks it the same wayholddoes (unwrap_or_else(|p| p.into_inner())) and returns whether any idas_str().starts_with(prefix).- Two skips, both required. Heartbeat runs before the job loop. A skip only after
heartbeat(...)still lets a due heartbeat start. A skip only insideheartbeatstill lets a due cron job start. The given tests cover each. - Where in
heartbeat. After theif !due { return; }block, beforeSessionId::new("heartbeat").every_s == 0still returns earlier. Busy-skip of theheartbeatsession itself is unchanged. - Where in
tick. Immediately afterself.heartbeat(...), beforejobs_dir.return workso leftover-close on this first tick is already done. - Prefix
sub-. A hold ofheartbeatorjob-…does not skip. The given tests holdsub-mm-thread-1. - The items the edits use. This table is complete.
Item Signature Context::holdalready pub; the given tests keep theHeldSessionId::as_stras_str(&self) -> &strScheduler::ticktick(&mut self, ctx: &Context, now: Timestamp) -> Vec<TickWork>
The edits, exactly
Each quoted "before" block occurs exactly once after task 03.
crates/loopd/src/channel.rs
H1. After hold's closing } and still inside impl Context, add:
/// True when any held session id starts with `prefix` (`sub-` for a live child).
pub fn holds_prefix(&self, prefix: &str) -> bool {
let busy = self.busy.lock().unwrap_or_else(|p| p.into_inner());
busy.iter().any(|id| id.as_str().starts_with(prefix))
}
The unique anchor is the end of hold:
busy.insert(id.clone());
drop(busy);
Ok(Held { ctx: self, id })
}
}
becomes that same hold body, then holds_prefix, then the impl closing }.
crates/loopd/src/scheduler.rs
K1. In tick, the lines
self.heartbeat(ctx, now, &mut work);
let jobs_dir = ctx.cfg.paths.home.join("jobs");
become
self.heartbeat(ctx, now, &mut work);
if ctx.holds_prefix("sub-") {
return work;
}
let jobs_dir = ctx.cfg.paths.home.join("jobs");
K2. In heartbeat, the lines
if !due {
return;
}
let id = match SessionId::new("heartbeat") {
become
if !due {
return;
}
if ctx.holds_prefix("sub-") {
return;
}
let id = match SessionId::new("heartbeat") {
Steps
-
1. Copy the given test.
cp docs/plans/M6a/files/crates/loopd/tests/scheduler_subagent.rs crates/loopd/tests/ -
2. See it fail.
cargo test -p loopd --test scheduler_subagent, timeout 600000 ms. The tests compile (holdexists). Both fail:workis not empty (aHeartbeator aJob) andrequests_to(CHAT)is not empty while the child is held. -
3. The edits. H1, K2, K1.
cargo check -p loopdprints no error and no warning.wc -l crates/loopd/src/channel.rsandwc -l crates/loopd/src/scheduler.rseach ≤ 500. -
4. The tests.
cargo test -p loopd --test scheduler_subagent --test scheduler_heartbeat --test scheduler_cron --test scheduler_heartbeat_failed: every test passes, 2 inscheduler_subagent, 6 inscheduler_heartbeat, 6 inscheduler_cron, 1 inscheduler_heartbeat_failed. -
5. Run the gate.
cargo fmt --all, thenmake gate, timeout 600000 ms. Last linegate: ok. Test count 1587 (1585 before, plus 2). Aftergate: ok, do not add uptest result:lines. -
6. Log and commit. Task cell
M6a/04-loopd-scheduler-skip. Read only the first 12 lines ofdocs/implementer-log.md; insert the row under the header. Stage withgit add crates/loopd docs/implementer-log.md Cargo.lock, then commit asAGENTS.mdsays.
Done when
make gateprintsgate: okwith 1587 tests.git diff HEAD~1 --statlists onlycrates/loopd/src/channel.rs,crates/loopd/src/scheduler.rs,crates/loopd/tests/scheduler_subagent.rsand the log.
Stop and report if
- The branch check fails; a "before" block is missing or duplicated; step 2 does not
compile, or only one of the two tests fails, or they fail for a different assertion;
cargo checkwarns; a heartbeat / cron / failed-heartbeat test fails; you skip only one of K1/K2; a test outside the named files fails in the gate (git stash -u, rerun,git stash pop, report both).
What the given tests pin
A held sub-mm-thread-1 makes a due heartbeat tick return empty work and send nothing,
then after drop(held) the next due tick is Heartbeat and one chat request; a held
sub-* likewise skips a due 0 7 * * * job at 07:00, then the same timestamp after
the drop runs the job.