feat(slurm): deadline timer for confined jobs too, plus WIP snapshotter - #9
Conversation
… confined jobs; WIP snapshotter Confined (sbatch) jobs never had a deadline watchdog. _start_slurm_timer is only reached from _ensure_slurm_node on the salloc path, and the confined batch body starts tmux and holds the job with nothing else, so the 30/15/5-minute warnings that prompts/carleton-htc.org promises never fired there (verified on n0036.savio4, job 38661192: no slurm-timer.sh process). Move the script out of cli.py's inline f-string into sucoder/slurm_timer.py (a plain @token@ template; cli imports mirror, not the reverse, so a shared builder could live in neither) and render it for both modes: - confined: _launch_confined stages slurm-timer-<mirror>.sh next to the batch script, and _build_batch_script nohups it after the new-session rc check and before the keeper loop, so it runs in the job cgroup and dies with the job. The job id is read from $SLURM_JOB_ID at run time, and every tmux call carries the dedicated -L socket (has-session, display-message, set-option); missing any one of them makes the timer wait 600s on a session it cannot see and exit as "timed out". - unconfined: the same ssh write/start as before, script from the builder. State files are now per mirror (slurm-deadline-<mirror>.warn and the 30/15/5 markers): several confined mirrors share one $HOME, and a second timer's startup rm -f would otherwise clear the first's markers. The un-suffixed slurm-deadline.warn is still written for prompts that poll it. The script also snapshots the dirty working tree (tracked + untracked, not ignored) to refs/sucoder/wip/<mirror> on the tree's origin at each warning and every slurm.wip_snapshot_minutes (default 10; 0 disables). A tree with no origin is never snapshotted, so on today's shared mirror this is a no-op; it goes live with the local-disk tiering in docs/local-disk-tiering.org (step 1 of that note's implementation order). The snapshot mechanics are the ones spike-verified there: temporary index, marker under .git/ so add -A cannot sweep it up, no snapshot of a clean tree, force-updated scratch ref that no branch depends on. Tests: tests/test_slurm_timer_script.py renders both modes, bash -n's them, checks socket threading and quoting, and drives snapshot_wip under bash against a real temporary origin (clean / dirty with ignored / unchanged via marker / no origin / non-repo). The test_cli scancel guard now inspects the rendered script rather than _start_slurm_timer's source, which the refactor would have made vacuous. test_batch_script pins the nohup line's position; test_mirror asserts the confined launch stages both files before sbatch; test_config covers wip_snapshot_minutes. Full suite CI-style (GITHUB_ACTIONS=true, lock-matched versions): 680 passed. mypy: unchanged (57 pre-existing errors in cli.py, 2 in mirror.py; new module clean). Impact analysis: the gitnexus CLI cannot load on this node (libssl.so.3 missing) and its MCP is not connected, so callers were grepped by hand: _start_slurm_timer <- cli._ensure_slurm_node only; _build_batch_script <- mirror._launch_confined only (+ tests/test_batch_script.py); _SLURM_TIME_LEFT_TO_MINS_SH <- tests/test_slurm_timer.py (kept as alias). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01StcQgXQDE4F6eJer1sEXRb
Review: needs workThe confined-watchdog fix is real and the snapshotter is genuinely non-destructive — I confirmed that independently. But a documented state file regressed, and the snapshotter can stall the very watchdog it lives inside. Review note: Findings1. The legacy CONFIRMED. Reproduced: mirror B's timer writes Inconsistent as well: the "no longer queued" message goes through This contradicts the description's "State files are per mirror …; the legacy 2. CONFIRMED (measured: a
It also means the "every N minutes" cadence is really "every N iterations of (60s + snapshot time)". 3. Mirror names containing CONFIRMED. 4. The snapshotter has no success or failure channel at all — Every step is Concrete scenario: 5. No snapshot at teardown, no SIGTERM trap — Slurm SIGTERMs the step at the wall clock and the timer simply dies. Best case the last snapshot is the 5-minute warning, so roughly five minutes of work is guaranteed unsnapshotted at the deadline; edits landing after that warning are lost. Same on clean agent exit — keeper loop ends, job ends, timer killed mid-cycle. For a feature whose stated purpose is not losing work, 6. The salloc path did not get the per-mirror treatment — CONFIRMED. It still writes and executes a single shared Worse: rewriting the script truncates it while previous timers are executing it. Verified that bash re-reads a running script from its saved offset and dies with The confined path got 7. CONFIRMED (measured). The unchanged-tree short-circuit at line 77 fires after 8. Smaller items
TestsThe snapshot tests are the strong part: real bash against real repos, asserting non-mutation. I independently confirmed non-mutation under detached HEAD, a conflicted mid-merge ( The gap: no test executes the timer's main loop, so findings 1, 5, the elif ordering, and the config gap are all invisible to the suite.
The rewritten ClaimsVerified true: confined jobs had no watchdog before (no Verified false or partial: the per-mirror state-file claim (finding 1) and the shared salloc timer script (finding 6). Could not check: anything against live Slurm ( 🤖 Review generated with Claude Code |
Step (1) of
docs/local-disk-tiering.org's implementation order.Bug fixed. Confined (
sbatch) jobs never had a deadline watchdog:_start_slurm_timeris only reached via_ensure_slurm_nodeon thesallocpath, and the batch body starts tmux and nothing else. Verified on n0036.savio4 (job 38661192): noslurm-timer.shprocess, despiteprompts/carleton-htc.orgpromising 30/15/5-minute warnings.Change. The script moves out of
cli.py's inline f-string intosucoder/slurm_timer.py(a@TOKEN@template shared by both launch paths). The confined launch stagesslurm-timer-<mirror>.shnext to the batch script and the batch bodynohups it after thenew-sessionrc check, inside the job cgroup. Job id from$SLURM_JOB_IDat run time; every tmux call carries the-Lsocket. State files are per mirror (several confined mirrors share$HOME); the legacyslurm-deadline.warnis still written.The same script snapshots the dirty tree to
refs/sucoder/wip/<mirror>on the tree'soriginat each warning and everyslurm.wip_snapshot_minutes(default 10,0off). Nooriginmeans no snapshot, so it is a no-op on today's shared mirror and becomes live with step (2).Tests. New
tests/test_slurm_timer_script.py(render both modes,bash -n, socket threading, quoting,snapshot_wipdriven under bash against a real temporary origin). Thescancelguard intest_cli.pynow inspects the rendered script (the refactor would have made the source-inspection version vacuous). Batch-script, confined-launch, and config tests added. Full suite CI-style: 680 passed. mypy unchanged.Impact analysis by hand (gitnexus cannot load here,
libssl.so.3):_start_slurm_timer<-_ensure_slurm_nodeonly;_build_batch_script<-_launch_confinedonly;_SLURM_TIME_LEFT_TO_MINS_SHkept as an alias fortests/test_slurm_timer.py.🤖 Generated with Claude Code
https://claude.ai/code/session_01StcQgXQDE4F6eJer1sEXRb