2026-08-05 – SLURM submission for JobServer#
Status: Phases 0-4, 6, 7, and 8 done (SLURM version/JSON groundwork, the
seamm_slurm backend library, JobServer’s SLURM mode, real end-to-end
validation on MolSSI10 – including a live-discovered per-job resource
override feature, proactive job-stopping, and now real ssh-transport
remote dispatch with no shared filesystem – and this documentation).
Phase 5 (a future seamm_exec Slurm executor for per-step submission)
is not started and not currently scheduled. Phase 8 (remote/no-shared-
filesystem JobServer dispatch – stage-in/stage-out, plus moving datastore
status writes from the job itself to the JobServer for all job types) is
implemented and validated live against molssi10 (2026-08-08); not yet
committed/released, and not yet enabled for real use (see the Phase 8
section for what’s left, which is operational – a real remote conda env
– not code). ~/SEAMM_DEV/Mac.ini (dev JobServer only) holds the
validated-working config, deliberately left inert. Originally tracked as
a workspace-root living planning doc;
moved here once the campaign was substantially complete.
All PRs from this campaign are now merged and released:
seamm_jobserver #17
(the SLURM mode, per-job overrides, docs, and this campaign doc) released as
2026.8.6; seamm_exec #30
(the job_data.json header bugfix) released as 2026.8.6.
seamm_slurm had two merged PRs and two releases: #1 (docs) released as
2026.8.6, and #3
(the config module + .limits/merge_overrides) released as
2026.8.6.1. 2026.8.6 itself never reached PyPI – its release event
was published during a GitHub Actions outage
(2026-08-06 15:22 UTC - 2026-08-07 02:04 UTC) and appears to have been lost
rather than queued, so nothing retroactively triggered it once GitHub
recovered. Not a functional problem since 2026.8.6.1 is a strict
superset and did publish successfully (confirmed on PyPI) once cut after
the outage cleared – just a gap in PyPI’s version history for that one
tag. All three packages’ PyPI versions confirmed live: seamm_slurm
2026.8.6.1, seamm_exec 2026.8.6, seamm_jobserver 2026.8.6.
Why#
JobServer.start_job() used to do psutil.Popen(run_from_jobserver,
...) directly on whatever host the JobServer ran on – no SLURM
involvement, and no concurrency limit at all: every submitted job in
the datastore started immediately. Fine on a personal Mac, wrong on a shared
cluster.
seamm_exec was already partially SLURM-aware (not SLURM-integrating):
computational_environment() reads SLURM_* env vars to size
NTASKS etc., and Base.run() auto-picks in-place vs scratch-dir
execution based on SLURM_JOB_ID. That machinery exists purely for the
case where a human hand-wraps a flowchart run in sbatch. Nothing
actually called sbatch itself before this campaign.
Two long-term models (both wanted eventually)#
Option 1 – whole-flowchart submission. JobServer submits the entire flowchart run (
run_from_jobserver) as one SLURM job. Simple, and necessary on clusters with real queue wait, since it’s one queue wait per job rather than one per step.Option 2 – per-executable submission.
seamm_execgains aSlurmexecutor alongsideLocal/Docker; the flowchart driver keeps running locally (as today) and only the heavy codes (ORCA, LAMMPS, VASP, MOPAC…) get individually submitted, sized from real per-step data (computational_environment()already does this). Better on personally-controlled machines / near-zero-queue-wait clusters, since lightweight steps (I/O, DB, control flow) never touch the scheduler – but needs SEAMM to decide, per step, whether to go through SLURM and with what shape, which is real new logic, not just plumbing.
Decision: build Option 1 first. Design the SLURM-talking pieces (submit / poll / cancel / status-map) as a shared, reusable component from the start so Option 2 is an additive consumer of the same backend later, not a rewrite.
Guiding decisions (locked in)#
Option 1 first. Whole-flowchart
sbatchsubmission via JobServer. Option 2 (seamm_execSlurmexecutor) is explicitly future work – not in scope for this campaign, but the shared backend must not preclude it.Dual transport, decided at config time, not hardcoded. JobServer may run on the SLURM cluster (e.g. a head/login node with
sbatch/squeue/sacctonPATHand working munge auth) or on a separate server that reaches the cluster via passwordless SSH. Both must be supported through one interface (LocalSlurmvsSshSlurmtransports), selected by config, not by which package is installed.SLURM is the source of truth for job state, not a locally-tracked pid. JobServer stores the returned SLURM job ID (in the existing
parametersJSON column – no datastore migration needed, same pattern already used forpid/cmdline) and polls SLURM (squeue/sacct) to learn whether a job is pending/running/finished/failed, rather than watching a local process.Crash recovery leans on flowchart restartability, not fragile reattachment. SEAMM flowcharts already checkpoint completed steps and resume from the first incomplete one. So on JobServer restart, there’s no need for delicate process-reattachment logic: look up the last known SLURM job ID for each
runningjob, ask SLURM its state; if it’s still alive, leave it; if it’s gone/failed and the flowchart isn’t marked done, it is safe to just resubmit – the flowchart will skip already-completed steps. Needs a retry cap/backoff so a genuinely-broken step doesn’t resubmit forever.Add a configurable concurrency cap. JobServer had none. Once compute is scheduled via SLURM, the local concern shifts to “how many jobs does this JobServer instance keep outstanding in SLURM at once” (submission hygiene / not hammering the scheduler on a big batch, per the existing TinkerCliffs “job array” concurrent-write lesson) – configurable, not a hardcoded constant.
First deployment/validation target: MolSSI10, not ChemAI. ChemAI is live production (its own JobServer has processed thousands of jobs); not worth risking disrupting it. MolSSI10 runs an equivalent live setup (systemd
org.molssi.seamm.{dashboard,jobserver}.service,seammconda env) but was confirmed idle and lower-stakes to iterate against. TinkerCliffs stays a later target after MolSSI10 (and ideally ChemAI) are proven out.Dev loop: edit source on a Mac checkout, validate by deploying/ running on the target cluster over SSH. No shared filesystem exists between a laptop and either cluster host (confirmed during Phase 0) – deploy via git/rsync + remote install, not by assuming paths line up. This is orthogonal to the dual-transport question above, which is about how the deployed JobServer talks to SLURM, not how its code gets there.
Phase 0 findings#
Groundwork done directly against ChemAI and MolSSI10 over SSH.
SLURM versions differ across targets, and JSON support is not uniform. ChemAI: SLURM 21.08.5,
squeue --json/sacct --jsonboth work cleanly (OpenAPI v0.0.37). MolSSI10: SLURM 20.11.4, ``–json`` is not recognized by either command (squeue: unrecognized option '--json'). This made “prefer JSON, text as a fallback” a firm Phase 1 requirement rather than a nice-to-have:SlurmBackendneeded to support both from day one.sacct --parsable2 --format=...is a clean, stable pipe-delimited path for the non-JSON case;squeue --format=...similarly. Use JSON when available (self-describing, no format-string coupling), parsable2/format text otherwise.``sbatch`` scripts get a non-interactive, non-login shell, so ``.bashrc``’s conda-init block never runs. Confirmed on both ChemAI and MolSSI10: both have the standard Ubuntu/Debian
.bashrcguard (case $- in *i*) ;; *) return;; esacnear the top) that returns immediately for non-interactive shells – which is exactly what a SLURM batch script gets, and also whatssh host cmd/bash -lc cmdget. It initially looked like generated sbatch scripts would need to explicitlysource <conda-base>/etc/profile.d/conda.sh && conda activate seammthemselves, verified end-to-end on MolSSI10 with a real submitted job.Superseded in Phase 2: this
conda activatedance turned out to be unnecessary when the payload invokes the entry point by its full, absolute path (e.g./home/psaxe/miniconda3/envs/seamm/bin/ run_from_jobserver ...) rather than a bare command name – confirmed with a second real sbatch test on MolSSI10 (run_flowchart --helpandpython -c "import seamm_jobserver, seamm_exec"both worked with zero activation lines, exit 0)._build_cmd()already resolved this exact absolute path for the pre-existing local-mode code (Path(sys.executable ).parent / "run_from_jobserver"), so_start_job_slurm()just reuses it – no conda-activation logic was needed in the sbatch script at all, only twoexportlines forSEAMM_JOB_ID/SEAMM_JOBSERVER(SLURM inherits the submitting environment, PATH included, by default).``db_path``’s direct-sqlite-write-at-completion is host-local, and that’s fine for the LocalSlurm-on-cluster-host case (in
seamm_exec/exec_flowchart.py: after a run, the flowchart process itself opensdb_pathand does oneUPDATE jobs ...). Since this campaign targets MolSSI10/ChemAI running their own JobServer against their own local datastore, this keeps working unmodified. It would not work if a JobServer elsewhere tried to dispatch to one of these clusters over SSH while keeping its own separate datastore, since there’d be no shared filesystem for the remote process to reach – relevant to futureSshSlurm/Option 2 work, not a blocker here.Both MolSSI10 and ChemAI already run live production
org.molssi.seamm.dashboard.service/...jobserver.servicesystemd user services. MolSSI10 additionally runs a second, independent dashboard+jobserver pair for another user on the same host. Confirmed MolSSI10 had zero jobs inrunning/submittedstate and an emptysqueueat the time – safe to experiment against, but still a shared, live-configured host: don’t restart its systemd services carelessly, and prefer testing against throwaway job dirs/scripts before wiring changes into the live services.
Config shape: <root>/<jobserver-name>.ini#
This is a system/machine preference (how does this JobServer/host
talk to its scheduler(s)), not a user preference, so it does not belong
in ~/.seamm.d/seamm.ini (the argparse-backed file that holds
[SEAMM]/[JobServer]/[Dashboard] command-line-option defaults –
that file’s own history even documents a past migration away from keeping
things in ~/SEAMM, for exactly the opposite class of settings). It
belongs alongside orca.ini/lammps.ini/dashboards.ini at
<root> (~/SEAMM by default) – those are read directly via
configparser.ConfigParser() by each consumer’s own code, keyed off
seamm_options["root"], the same way e.g. orca_step’s
_orca_config() reads orca.ini.
The file is also not simply named slurm.ini. A JobServer may eventually
route jobs to more than one cluster/queue – different dashboards each
funneling to a different cluster over SSH, or a single JobServer routing
across clusters that don’t even share the same queueing system (SLURM on
one, PBS on another, plain local on a third). A single global slurm.ini
wouldn’t have room for that. Instead: the file is named after the
JobServer instance (its --name, default socket.gethostname()) –
<root>/<jobserver-name>.ini – with one section per cluster/queue
target it knows how to reach, not one section per SLURM version/transport
variant. This also naturally supports multiple JobServer instances sharing
one <root>/datastore (each gets its own config file, no collisions) and
keeps the file format stable even if a future section targets a non-SLURM
scheduler.
# <root>/<jobserver-name>.ini -- e.g. ~/SEAMM/molssi10.ini
# One section per cluster/queue this JobServer instance can route jobs to.
# System/machine config, not a user preference -- lives at <root>, not
# ~/.seamm.d/seamm.ini, same reasoning as orca.ini/lammps.ini/dashboards.ini.
[DEFAULT]
# which section a job uses when it doesn't request one explicitly
default = molssi10
[molssi10]
# type: slurm (only one implemented for now; pbs/lsf/etc. are future
# targets this shape leaves room for -- not building a multi-scheduler
# abstraction yet, just not foreclosing it in the file format)
type = slurm
transport = local
# submission defaults -- applied unless a job's own parameters override them
partition = batch
account =
qos =
nodes = 1
ntasks = 1
time = 01:00:00
mem =
gpus =
# how many jobs this JobServer keeps outstanding in this section's
# cluster/queue at once
max_concurrent_jobs = 20
[chemai]
type = slurm
transport = ssh
host = seamm-chemai
partition = ChemAI
nodes = 1
ntasks = 1
time = 01:00:00
max_concurrent_jobs = 10
Blank values mean “don’t pass that #SBATCH directive” (let SLURM’s own
partition/QOS defaults apply) – matters since different clusters have
different partitions/accounting setups and not all have real QOS/account
associations configured.
How a job picks which section to route to, when a config file has more than
one, was left as an open question – not needed for the initial rollout
(MolSSI10 ships with a single section), but the config format already
accommodates multiple sections so answering it later won’t require a format
change, only new routing logic in check_for_new_jobs()/start_job().
Architecture#
Shared piece (small library, consumed by seamm_jobserver and, later,
seamm_exec – mirrors how seamm_bsse was split out as its own
package rather than folded into an existing one): a new package,
seamm_slurm.
SlurmBackend(seamm_slurm/backend.py):submit(script, job_name=None) -> job_id(feeds the full script text tosbatch --parsableon stdin – never needs the script to exist as a file on the target host, sidestepping the no-shared-filesystem finding from Phase 0),poll_many(job_ids) -> {job_id: JobStatus}(one batchedsqueuecall, plus one batchedsacctcall only for idssqueueno longer lists – not one call per job),cancel(job_id).LocalSlurm(SlurmBackend)(local.py) – shells out tosbatch/squeue/sacct/scanceldirectly.SshSlurm(SlurmBackend)(ssh.py) – same commands overssh <host> .... Both only implement_run; everything else is shared in the base class.JSON-vs-text handled transparently per-backend-instance (probed once, remembered):
squeue --json/sacct --jsonwhen supported,--parsable2/--format=text otherwise.sacct’s andsqueue’s JSON schemas are not analogous (squeue’sjob_stateis a flat string;sacct’sstate/exit_codeare nested objects – confirmed against real completed jobs on ChemAI) – separate parsers, not a shared one.seamm_slurm/status.py:classify(raw_state) -> category, one ofpending/running/completed/cancelled/failed/unknown– a SLURM-domain concept only. Deliberately no SEAMMjobs.statusmapping in this library – that’sseamm_jobserver’s job, keepingseamm_slurmreusable (including, later, byseamm_exec).seamm_slurm/script.py:build_script(directives, payload) -> strturns a directives dict (matching<root>/<jobserver-name>.inisection keys) into#SBATCHlines + payload; unknown keys pass through as--key-with-dashes=value, so the library doesn’t need to special-case every SLURM option the ini file might carry.
seamm_jobserver changes:
start_job(): builds ansbatchscript wrapping the existingrun_from_jobserverentry point (a copy is also written into the job’s working directory, asslurm_submit.sh, for auditability, alongsidejob_data.json/references.db), submits it via the configuredSlurmBackend, storesslurm_job_idinparameters.check_for_finished_jobs(): a single batchedpoll_many()call per cycle over all trackedslurm_job_ids, instead of a psutil per-pid loop.start()’s startup reattachment scan:poll_many()over jobs markedrunning, using storedslurm_job_ids; missing/terminal + flowchart incomplete -> safe resubmit (capped/backed-off), the exact same reconciliation path used during steady-state polling.A configurable
max_concurrent_jobs:check_for_new_jobs()stops pullingsubmittedrows once the outstanding-in-SLURM count hits the cap.status()/GUI: local psutil cpu/mem-per-job stats aren’t meaningful once compute runs on a remote node – SLURM-mode job entries reportslurm_job_id/resubmit_countinstead, and both the JSON status output and the Tk GUI status view were updated not to crash on the differently-shaped entries.
All of this is entirely additive: a JobServer instance only enters
SLURM mode if <root>/<jobserver-name>.ini exists; absent, behavior is
byte-for-byte the same as before this campaign.
Phased plan#
Phase 0 – Groundwork on MolSSI10 (done)#
Target changed from ChemAI (see the guiding decisions above). SLURM version
+ --json support checked on both MolSSI10 and ChemAI (differs – see
Phase 0 findings). Passwordless SSH confirmed. run_flowchart’s env/
.ini-sourcing under sbatch confirmed not automatic at the time
(later superseded, see Phase 0 findings). <root>/<jobserver-name>.ini
config shape drafted and reviewed.
Phase 2 – JobServer integration (done)#
New seamm_jobserver/slurm_config.py (load_slurm_config/
SlurmSection, reads <root>/<jobserver-name>.ini, returns None
when absent – local-subprocess mode is completely unchanged/untouched in
that case). jobserver.py’s start_job, check_for_finished_jobs,
and the startup reattachment scan are each now dispatchers over a
local-mode path (original code, untouched) and a new SLURM-mode path.
Concurrency cap in check_for_new_jobs. Resubmit-on-loss-of-track with a
retry cap in _reconcile_stalled_job, shared by both the steady-state
poll loop and startup reattachment. status()/gui_status() updated
to not crash on SLURM-mode job entries. 29 tests (real temp-sqlite
datastore + a scriptable FakeSlurmBackend, no mocking of SQL), make
format lint install test clean.
A real bug the tests caught and fixed: the finished-jobs cleanup loop
was unconditionally deleting self._jobs[job_id]/self._times[job_id]
even when _reconcile_stalled_job had just resubmitted and repopulated
them – every resubmitted job was silently losing tracking immediately
after being resubmitted.
Phase 3 – Validate on MolSSI10 (done)#
Done via a fully isolated setup (a cloned seamm-slurm-test conda env,
separate root/datastore/job dirs, a unique JobServer --name so it never
read the live molssi10.ini) – the live production JobServers on that
host were never touched or restarted. Real sbatch -> SLURM ->
completion cycle validated end-to-end (a FromSMILES-only job borrowed
from a real production job dir; MolSSI10 has no ORCA environment, so a
heavier flowchart wasn’t an option), plus a genuine kill/restart test using
#SBATCH --begin=now+25 to deterministically hold a job PENDING
through the kill/restart window (no timing race). All four target
scenarios confirmed via real logs/datastore state, not just inference:
Happy path – submit ->
finished, real output files.Resubmit-and-give-up under genuine repeated SLURM failures – 4 real attempts, correct cap, final
error.Restart correctly resuming a still-
PENDINGjob with no duplicate submission – the core guarantee this phase set out to prove.The
job_data.json-trust path avoiding a needless resubmit.
See “Bugs found and fixed during Phase 3” below for two real, pre-existing issues surfaced by this real-world testing that mocks alone would not have caught.
Phase 4 – Config/docs rollout (done)#
seamm_jobserver’s previously-blank User Guide now has real content:
local vs SLURM mode, the full <root>/<jobserver-name>.ini format
(transports, submission directives, max_concurrent_jobs,
max_resubmits), what happens at submission time, and the reconciliation
behavior. README/HISTORY updated to match. seamm_slurm’s README/
HISTORY/campaign doc updated to describe itself as actually wired in and
validated rather than expected/future work, plus a correction (no
conda-activate needed after all) – seamm_slurm’s main branch is
branch-protected, so that went through a dev branch and a pull request
rather than a direct push. ChemAI/TinkerCliffs rollout is still deferred
until MolSSI10 usage is solid, per the original plan. This document is
part of Phase 4: moved from a workspace-root scratch planning doc into
this campaign doc.
Phase 5 – a future seamm_exec Slurm executor (not started)#
Option 2 from the top of this document: reusing the Phase 1 backend,
driven by computational_environment()’s per-step sizing, with hybrid
selection of Option 1 vs Option 2 per job/cluster. Not started, not
currently scheduled.
Phase 6 – per-job SLURM resource overrides (implemented, real-world-driven)#
Discovered directly from real usage: after Phase 3/4’s live deployment on
MolSSI10, every job from a given JobServer instance got the exact same
fixed SLURM resource request. This surfaced two real problems at once when
Paul submitted several real jobs: jobs defaulted to 1 core each with no way
to ask for more, and – more surprising – SLURM was reserving the entire
node’s memory per job (since the ini’s mem was left blank), so only
one job could ever run concurrently on the 6-core node regardless of free
CPU. Fixing the immediate mem gap in the live molssi10.ini (setting
an explicit mem = 20G) was a config change, not a code change, and was
confirmed live: three newly-submitted jobs ran genuinely concurrently once
the fix took effect (the three jobs already in flight before the fix kept
their original whole-node reservation and continued running one at a time,
as expected, since a submitted job’s resource request is fixed at
submission time).
The design, from there: sites need a way to let specific jobs ask for different resources than the instance-wide default, while still corralling requests to valid values (Paul: “the site-specific information in JobServer.ini should also specify limits … or enumerated choices … to corral users to valid specifications”).
A job’s requested overrides live in its own
parameters["slurm"](e.g.{"ntasks": 4, "mem": "40G"}) – same placecmdlinealready lives, no datastore migration.Which directives a job may override, and within what bounds, is controlled by an optional
[<section>.limits]ini section (enumerated.choices, or.min/.maxbounds – unit-aware formem(K/M/G/T) andtime(HH:MM:SS/D-HH:MM:SS), plain numeric otherwise). Secure by default: no.limitssection means nothing is overridable.SlurmSection.merge_overrides()validates and merges a request server side, always – never trusts that a caller (e.g. a future web UI) already enforced this. RaisesValueErroron anything unauthorized or out of bounds, which JobServer’s existingstart_joberror handling already catches and turns into astartup errorstatus – no new error-handling plumbing needed.Moved the whole ini-parsing/validation module from ``seamm_jobserver.slurm_config`` into ``seamm_slurm.config`` (Paul’s call): both
seamm_jobserverand any future lightweight consumer (a job-submission UI) need to read this file, andseamm_slurmwas already built to be dependency-light and reusable, unlikeseamm_jobserveritself (psutil, GUI code, job-running machinery).seamm_jobservernow just importsseamm_slurm.config.``seamm_dashboard`` will not be getting this feature. Paul: it’s too fragile to keep extending. The requirement was instead written into
seamm_webui’s own living plan doc (~/Work/SEAMM/dashboard-rewrite-plan.md, that project’s session is informally called “datastore”) for whenever job submission gets built out there – not reachable via direct agent messaging (not a spawned teammate in this session), so left as a note in the doc that project actually reads.
34 new tests in seamm_slurm (FieldLimits/.limits parsing/
merge_overrides, including real mem/time unit-conversion cases)
plus new seamm_jobserver tests covering the full wiring (override
applied/rejected/out-of-range, preserved across resubmission and restart
reattachment). All passing, lint and docs clean in both packages.
Phase 7 – proactively stopping deleted or explicitly-killed jobs (implemented, real-world-driven)#
Discovered directly from real usage, again: before this campaign, deleting a job (e.g. via the dashboard) removed its row and files but never told whatever was actually running it to stop – it just ran on until it crashed on its own missing files. Fine (if wasteful) for a local subprocess; on SLURM, an orphaned job can sit running, or queued, consuming a node/slot for however long it takes to fail on its own. Paul: “we will need to be more proactive and kill the slurm job.”
The design:
A new
check_for_stopped_jobs()runs every poll cycle, beforecheck_for_finished_jobs()– ordering matters, since a job that was just killed must already be gone from tracking before the finished-jobs reconciliation logic gets a chance to treat it as merely lost and try to resubmit it.Deleted row: for every job the JobServer is actively tracking, a single batched query checks whether its row still exists at all. If not, the JobServer actively stops it (
scancelin SLURM mode,terminate/killin local mode) and drops it from tracking. Nothing to write to the datastore – the row is gone.Explicit kill, files kept: extended the ask to also support stopping a job without deleting it – setting
status = 'kill'on the row (an ordinary status update; no new API needed on the dashboard/webui side, since a generic job-update endpoint already exists). The JobServer stops the run the same way, then finalizesstatustokilled. A job killed before the JobServer ever started it (stillsubmitted) is simply finalized askilleddirectly, no process to touch.Startup reattachment also has to know about this. Both
_reattach_local_jobsand_reattach_slurm_jobspreviously only looked atstatus = 'running'rows. Extended both tostatus IN ('running', 'kill')– a job whose kill was requested right before a JobServer restart still needs its real process/SLURM job cancelled, not silently resumed, and (the sharper bug this would otherwise cause) not handed to the ordinary lost-job reconciliation path, which would try to resubmit it. If such a job is still alive, it’s pulled back into tracking (so the next ordinary poll cycle kills it via the path above); if it’s already gone, it’s finalized askilleddirectly.A new
killed_jobscounter, alongside the existingsuccessful_jobs/failed_jobs/ended_jobs, surfaced instatus()and the Tk status tab.
11 new tests (SLURM and local mode, both the deleted-row and kill-status paths, plus the three reattachment edge cases above) – 39 total, all green, lint and docs clean.
Bugs found and fixed during Phase 3#
Real end-to-end testing surfaced two genuine, pre-existing issues that the mocked Phase 1/2 unit tests could not have caught (their fakes were written to be internally consistent, so they couldn’t reveal a real mismatch between two real writers/readers of the same file format):
``seamm_exec/exec_flowchart.py``’s ``run_from_jobserver()`` exception handler wrote ``job_data.json``’s header without a trailing newline (
fd.write("!MolSSI job_data 1.0"), unlike every other writer of this file, which uses the module-levelheader_line = "!MolSSI job_data 1.0\n"constant). Consequence: the header text and the JSON blob landed on the same first line, so areadline()-then-json.load()reader (what_read_job_data_state()originally did, and whatseamm_datastore.Job.parse_job_dataalso does) failed to parse the file and silently treated it as absent. Effect on this project specifically:_reconcile_stalled_jobcouldn’t tell a job had already recorded its own outcome, and resubmitted it needlessly. Fixed at the source (useheader_line, matching every other writer) plus made_read_job_data_state()itself tolerant of both forms (strip up to the first{rather than assuming a clean header line) as defense in depth, since old already-written files may still have the bug. This also fixes a live (if rare) correctness gap inseamm_datastore.Job.parse_job_datafor any job that fails via this exact exception path, independent of SLURM entirely. Theseamm_execside of this fix is PR #30 (open, not yet merged).Real submissions always pre-create a ``job_data.json`` stub before the flowchart ever runs (confirmed in
seamm_dashboard/routes/api/jobs.pyat job-creation time) –run()unconditionally expects to read it back whenin_jobserver=True. This is not a bug to fix, just a real precondition this campaign’s hand-built test job rows initially missed (onlyflowchart.flowwas copied over, not the stub), which is what surfaced bug #1 in the first place. Noting it here because it’s a real invariant: anything that creates a job row for JobServer to pick up (a futureseamm_execPhase 5, or any other tooling) must also write this stub, not just the flowchart file.
Status log#
2026-08-05 – Requirements/architecture discussion with Paul. Locked in: Option 1 first (Option 2 deferred but backend shared), dual transport (local CLI / SSH), SLURM as source of truth for state, resubmit-on-crash leaning on existing flowchart restartability, add a concurrency cap (previously absent), first target = ChemAI (not TinkerCliffs). Plan written; implementation not started.
2026-08-06 – Phase 0 groundwork done, from a Mac checkout via SSH to both clusters. Target changed: ChemAI turned out to be live production (its JobServer had processed thousands of jobs) – switched the first validation target to MolSSI10, which runs an equivalent live setup but was idle at the time. Key findings: SLURM versions/JSON support differ across targets (backend needs both paths from day one);
sbatchscripts appeared to need explicitconda activate(verified working end-to-end with a real job, later superseded in Phase 2); no shared filesystem exists between a laptop and either cluster (relevant to futureSshSlurm/Option 2 work, not a blocker for Phases 1-3, which target each cluster’s own already-resident JobServer).2026-08-06 (later) – Paul corrected the config location: SLURM settings are a system/machine preference, not a user preference, so they belong at
<root>, alongsideorca.ini/lammps.ini/dashboards.ini, not as a section in~/.seamm.d/seamm.ini. Verified againstseamm_util/argument_parser.pyandorca_step’s_orca_config(). Plan’s config-shape section corrected. Phase 0 fully done.2026-08-06 (further thought) – Paul: don’t name the file
slurm.inieither – a JobServer may eventually route jobs to multiple clusters/queues, possibly with different queueing systems. Renamed the design to<root>/<jobserver-name>.ini, with one section per cluster/queue target instead of one section per transport variant.2026-08-06 (Phase 1) – Built
seamm_slurm(skeleton copied fromseamm_bsse’s boilerplate and adapted):SlurmBackend/LocalSlurm/SshSlurm/status.py/script.py. Realsacct --jsonschema pulled from a live completed job on ChemAI before writing the parser (nestedstate.current/exit_code.return_code, unlikesqueue’s flatjob_statestring – would have gotten this wrong by guessing). 46 unit tests, lint clean. Validated for real (not just mocked) against MolSSI10 viaSshSlurm.2026-08-06 (Phase 1, committed) – Paul created the empty
molssi-seamm/seamm_slurmGitHub repo; committed the scaffold and pushed tomain. Phase 1 fully done.2026-08-06 (Phase 2) – Wired
seamm_slurmintoseamm_jobserver. Discovered (and reused) that full-path invocation ofrun_from_jobserverneeds noconda activateundersbatch– simpler than the Phase 0 finding assumed. New<root>/<jobserver-name>.ini-driven SLURM mode is fully additive: no ini file present means zero behavior change (verified by the local-mode tests continuing to pass unmodified). 29 tests, lint clean; a real cross-tracking bug (resubmitted jobs losing tracking) was found and fixed by the tests, not by inspection. Deliberately did not touch MolSSI10’s live JobServer as part of this pass.2026-08-06 (Phase 3) – Validated for real on MolSSI10, via a fully isolated setup (cloned conda env, separate root/datastore/job dirs, unique JobServer name) – confirmed the live production JobServers on that host kept running throughout, untouched. Real bugs found and fixed along the way (see “Bugs found and fixed during Phase 3” above). After the fix: clean happy-path run; genuine resubmit-and-give-up validated under real repeated SLURM failures; and a deterministic kill/restart-while-
PENDINGtest confirmed reattachment resumes tracking an existing SLURM job with no duplicate submission – the core guarantee this phase set out to prove.2026-08-06 (Phase 4) – Docs rollout:
seamm_jobserver’s User Guide now documents the SLURM mode for real; README/HISTORY updated.seamm_slurm’s README/HISTORY/campaign doc updated similarly. Paul madeseamm_slurm’smainbranch-protected – used adevbranch and a pull request (PR #1, left open for review, not merged) rather than a direct push.2026-08-06 (this document) – Moved from a workspace-root scratch planning doc (
jobserver-slurm-plan.md, not version-controlled) into this campaign doc, per Paul’s request, since the campaign was substantially complete and the plan spans this package plusseamm_slurm/seamm_exec. The original scratch file now just points here.2026-08-06/07 (live deployment + Phase 6) – Deployed to the live MolSSI10 JobServer (patched conda env, restarted the systemd service). Paul submitted real jobs and found only one ran at a time; root-caused to SLURM defaulting to whole-node memory reservation (
memblank inmolssi10.ini) rather than a core-count issue – fixed live with an explicitmem = 20G, confirmed concurrent jobs after. This directly motivated Phase 6 (per-job resource overrides with site-defined limits), designed and implemented the same session, then validated live against MolSSI10 with a real accepted override (ntasks=3, confirmed viasacct) and a real rejected out-of-range override (no SLURM job created, correct error surfaced asstartup error). Redeployed the final code plus a real[molssi10.limits]section (overridable = ntasks, mem, time) to MolSSI10. Test job dirs (Job_000660-Job_000668) cleaned up afterward, leaving the ~130 pre-existing real jobs in the same project untouched.2026-08-07 (releases) – All three packages’ PRs merged by
seammand released:seamm_slurm2026.8.6.1(the2026.8.6tag was lost to a GitHub Actions outage and never reached PyPI, see the Status paragraph above),seamm_exec2026.8.6,seamm_jobserver2026.8.6. All confirmed live on PyPI; local checkouts synced viamake update.2026-08-07 (Phase 7) – Paul: deleting a job in the dashboard leaves whatever is actually running it untouched, which SLURM makes worse than it was locally (an orphaned job can occupy a node/slot for a long time before crashing on its own). Designed and implemented proactive stopping: a deleted row or an explicit
status = 'kill'(new, files-preserving stop mechanism) is now noticed every poll cycle and actively cancelled/terminated, with startup reattachment also coveringkill-status rows so a kill requested right before a restart isn’t lost or wrongly resubmitted. 11 new tests (39 total), lint/docs clean.2026-08-08 (Phase 8, designed) – Paul wants a JobServer on his Mac able to submit to MolSSI10’s SLURM cluster over SSH – a case not actually covered by anything validated so far, since the Mac shares no filesystem with any cluster host. Added a non-functional placeholder
~/SEAMM_DEV/Mac.ini(dev JobServer only) as a marker. Re-reading_build_cmd()/_start_job_slurm()/exec_flowchart.run()confirmed two real gaps (Mac-local executable path, Mac-local--chdir) plus a third already documented as a known limitation (exec_flowchart.py’sin_jobserverdatastore write is unreachable from a remote host). Paul corrected two parts of the initial design sketch: (1) referenced input files need no bespoke transfer logic – the existing flowcharttype: "file"control-parameter mechanism already copies them into the job’s owndata/before the JobServer ever sees the job, so remote stage-in is just “rsync the job’swdir,” full stop; (2) rather than only fixing the datastore write for the new remote case, move ownership of the terminal status write from the running job to the JobServer for all job types (local included) – reusing_read_job_data_state()/_finalize_job_status()(built in Phase 2/3 for SLURM reconciliation only) as the sole path, and deletingexec_flowchart.py’s direct-sqlite-write branch entirely. Full design (stager abstraction paired with the SSH transport, newremote_root/remote_conda_envini keys, stage-out-failure-is-not-job-failure, suggested build order) written up above. Not started.2026-08-08 (Phase 8, implemented) – Built all four sub-steps in one session: (1) moved terminal datastore-status ownership from the job to the JobServer for local and SLURM modes alike, deleting
exec_flowchart.py’s direct sqlite write; (2)seamm_slurm.stage(LocalStager/RsyncStager) plusremote_root/remote_conda_env/remote_run_from_jobserverini keys; (3) wired staging intoseamm_jobserver(_start_job_slurmnow stages then builds the command from the effective wdir;_finalize_or_resubmit_ slurm_jobstages results back before trustingjob_data.json, treating a stage-out failure as retry-worthy, not job failure); (4) a real, unmocked live run from this Mac to molssi10 – isolated test setup, realssh/rsync, a realsbatchjob (sacctconfirmedCOMPLETED), a real flowchart that actually ran on molssi10’s hardware (confirmed via its CPU info in the pulled-backjob_data.json), and correct local finalization by the JobServer itself. 61 new/changed tests total across the three packages (all passing),make lintclean in all three. Test trees removed after the run;~/SEAMM_DEV/Mac.iniupdated to the validated config but left deliberately inert (points at a stale shared test env, not something to actually dispatch real dev work to yet).2026-08-08 (Phase 7 + 8, committed/pushed/PRs open) – All three packages’ working-tree changes committed to their
devbranches (Phase 7 and Phase 8 bundled into oneseamm_jobservercommit, per Paul’s call – simpler than splitting an already-interleaved diff) and pushed. Release prep (HISTORY entries, pre-flight checks, user-guide review/update per the release skill) done for all three; theseamm_jobserveruser guide’s “SLURM submission” section corrected in the process – it previously claimed a job’s own process writes its final status directly (no longer true, see Phase 8 sub-step 1) and thattransport = sshalready worked with no shared filesystem (it didn’t – only the script-over-stdin submission mechanism did; nothing staged files or built a remote-usable command line before this PR). PRs open forseammreview: seamm_jobserver #18, seamm_slurm #4, seamm_exec #31. Not merged or released – that’sseamm’s manual step.2026-08-08 (all three merged and released, campaign complete) – All three PRs merged by
seammwithin the same session.seamm_slurmandseamm_execreleased as2026.8.8first (both confirmed live on PyPI, local checkouts synced viamake update);seamm_jobserverhad to wait, since itsrequirements.txthas no version pin onseamm_slurmand its own tests importseamm_slurm.stagefor real (not mocked) – CI would have pulled the old PyPI release lacking that module otherwise. Onceseamm_slurmwas confirmed live,seamm_jobserver’s CI passed and it was merged; released as2026.8.8too. Localseamm_jobservercheckout synced viamake updateimmediately after the GitHub Release/tag was created, without waiting for the PyPI publish step ofRelease.yamlto finish –make updaterebuilds from the local git checkout at the tagged commit, it does not install from PyPI, so only the tag needs to exist. Phase 8 (and the bundled Phase 7) are now fully shipped: all three packages at2026.8.8,dev == mainin all three local checkouts.2026-08-09 (enabling it for real surfaced a real reattachment bug, fixed) – Before turning Phase 8 on for real dev use: upgraded
seamm_execin molssi10’s own productionseammconda env (2026.8.1 -> 2026.8.8, Paul’s explicit choice to reuse that env over a separate dedicated one – confirmedsqueueempty first, confirmed molssi10’s own resident JobServer/Dashboard services stayed undisturbed since a pip upgrade on disk doesn’t touch an already-running process), pointed~/SEAMM_DEV/Mac.ini’sremote_run_from_jobserverat it, and restarted the~/SEAMM_DEVJobServer (launchctl kickstart, it’s launchd-managed). That restart is what surfaced a real bug:start()picked exactly one of_reattach_local_jobs/_reattach_slurm_jobsbased on whether the instance is currently SLURM-configured, not on what each individualrunningrow actually is – so a stale local job from before SLURM was ever enabled here (a real one:Job_003881, crashed with an MDI error hours earlier,job_data.jsoncorrectly saiderrorbut the datastore row was never corrected, since the old pre-restart code was still running) fell to_reattach_slurm_jobs, which unconditionally tried tostage_outfrom a molssi10 directory that was never created (this job never went anywhere near SLURM) – would have retried forever rather than ever reading the localjob_data.jsonthat already had the answer. Worse in general, a genuinely still-running local job at restart time would have hit the same path and been resubmitted via SLURM as a real duplicate run. Fixed properly (not just worked around):_reattach_local_jobsnow always runs first regardless of mode and leaves SLURM-recorded rows alone;_reattach_slurm_jobsskips any row with a recorded pid and only computes/usesremote_wdirfor a row that actually has aslurm_job_idon record. Also fixed in the same pass: the reattached local-job tracking dict was missing"wdir"(a latent bug from the previous commit – would have KeyError’d once such a job finished) and a dead local process with no other evidence now trustsjob_data.jsoninstead of blindly assuming success.Job_003881itself was hand-corrected first (UPDATE jobs SET status='error'..., matching its ownjob_data.json) so the restart wouldn’t hit the bug before the fix was even written. 6 new tests (58 total),make lintclean, installed and the live~/SEAMM_DEVJobServer restarted again with the fix – confirmed clean ("Jobs": {},"previous jobs": 0, no crash-restart loop). Pushed and PR’d through the same release-skill cycle as the rest of Phase 8: HISTORY entry (2026.8.9, a new day so not a same-day.1), pre-flight checks, a small User Guide addition clarifying that reattachment is now correctly per-job rather than per-instance-mode. PR #19 merged byseamm, released as2026.8.9, local checkout synced viamake update(dev == main, clean2026.8.9install, 58/58 tests green). The live~/SEAMM_DEVJobServer was already running this exact fix from its earlier source install/restart (see above) – functionally identical to the now-released version, just not re-restarted purely to pick up a clean (non-+dirty) version string, since there’s no behavioral difference to gain from doing so. This closes out the Phase 8 campaign.