Deploying on a cluster¶
sinteractive is one binary with zellij compiled in. The batch job execs
that binary on the compute node (sbatch --wrap "exec /path/to/sinteractive
__job …"), so the only deployment rule is: the binary must live on a
filesystem the compute nodes can see. A user's ~/.local/bin on a shared
home, a group /projects directory, a cluster-wide /opt — any of them
works, and there is nothing to install on the nodes. Only when /usr/local
is node-local and you want it there does make nodes come in.
What the binary needs¶
- glibc no newer than the oldest node's. The binary links glibc
dynamically, so a build on a newer distribution refuses to start on an
older one with
GLIBC_2.xx not found. Build on the oldest system the binary must run on — the release builds run in arockylinux:8container (glibc 2.28, what Alpine's Rocky 8 nodes ship) for exactly this reason. libcurl.so.4at runtime (the zellij web client links it). Present on every RHEL-family node;ldd sinteractiveshows what else it wants.libnvidia-ml.so.1, optionally: the monitor loads NVML lazily and reports no GPUs without it, so a login node without a driver is fine.- Slurm client tools on
PATH:squeue,sbatch,scontrol,sacct,sinfo;srunforattach. - ssh to the nodes only for
attach --ssh,peek,send,monitor --liveanddoctor --nodes.attachitself goes throughsrun --overlap, and the login-node reporting commands read the shared cache.
Building¶
git clone https://github.com/rnabioco/sinteractive
cd sinteractive
make build # cargo build --release -p sint --features web_server_capability
rust-toolchain.toml pins stable Rust and adds the wasm32-wasip1 target:
the status plugin (crates/sint-zellij) is built for it by sint's
build.rs and embedded, so a plain cargo build needs nothing beyond the
toolchain file. The rest is a C/C++ toolchain with cmake and perl, and
the libcurl and OpenSSL headers — on Rocky:
The first build takes a while (zellij is large); SINT_SKIP_BUNDLE=1 skips
the plugin build for quick local iterations (the binary then warns that the
status plugin is not embedded). FEATURES= drops the web client and its
dependencies. Compile on a compute node, not the login node, on clusters
that police it.
The man page and the shell completions are generated by the binary
(sinteractive gen man, sinteractive gen completions bash|zsh|fish); make man
and make completions refresh the committed copies under man/ and
completions/ from the built binary, and the install targets run both.
Installing¶
make install # as a user: ~/.local/bin and ~/.local/share
sudo make install-system # /usr/local/bin and /usr/local/share
Both put the binary, man page and completions in place, and the Claude Code
share tree (<prefix>/share/sinteractive/{skills,claude}) beside the binary,
which is what sinteractive claude install reads from when run from an
installed copy rather than a checkout. make install refuses with a hint
when target/release/sinteractive has not been built; the Makefile does not
fall back to anything else.
The binary is never written over in place. Every running session is this
binary, executing on its compute node as the zellij server, and so is every
launch or attach sitting on a login node. On an NFS home (Alpine's
Isilon) unlinking or renaming over a file that another node is executing
takes the inode away underneath it: the next page that process faults in
comes back ESTALE and it dies with SIGBUS — the server first ("Lost
connection to the Zellij server"), then the client ("Bus error (core
dumped)"). So scripts/install-bin.sh puts each build at
<bindir>/.sinteractive-<sha> (a no-op for a build already there) and
swaps <bindir>/sinteractive, a relative symlink, into place with a
rename. current_exe() resolves through the symlink, so a session keeps
spawning its helpers from the build it started on instead of mixing zellij
client and server versions across an upgrade. Old builds are pruned only
when nothing can still be running them: everything installed since the
earliest submit time in the queue is kept (the user's jobs, or every job for
a root install), plus the two builds before that; with no scheduler or no
jobs, the current build and the two before it. make nodes is different —
/usr/local there is node-local, the running copy is on the same kernel
as the rename, and renaming over it is safe — so it keeps its mv.
Then check it:
sinteractive doctor
# ✓ binary /home/me/.local/bin/sinteractive 1.3.0 (zellij 0.45.1)
# ✓ plugin status plugin embedded
# ✓ bundle extracted to /home/me/.cache/sinteractive/bin/1a14af25b66b
# ✓ cache /home/me/.cache/sinteractive writable on nfs (2.0 GB)
# ✓ slurm squeue sbatch scontrol sacct sinfo
# ✓ cluster alpine
# ✓ ssh /usr/bin/ssh
# ✓ shell /bin/bash
# ✓ nvml libnvidia-ml.so.1 loadable
# ✓ home /home/me on nfs (2.0 GB)
fail means a session cannot work until it is fixed (exit 1); warn is
something to know about — NVML missing on a login node is normal, a $HOME
on a small local filesystem is a reason to set SINTERACTIVE_CACHE.
doctor --nodes additionally sweeps every node sinfo knows about over ssh
and reports reachability, the binary's version there and whether the
extracted bundle is visible from each — the check that catches a binary or
cache directory on a filesystem the nodes do not mount. The node sweep
never affects the exit code; a node being down is Slurm's business.
--json gives both as one object.
The cache directory¶
SINTERACTIVE_CACHE (default $XDG_CACHE_HOME/sinteractive, else
~/.cache/sinteractive) holds everything a session writes for the login
node to read, plus zellij's own working files. It must be on a filesystem
the compute nodes share with the login node, and it needs a little room:
<cache>/
JOBID.json time-budget state file (frozen schema, see Scripting)
JOBID.notices TSV kind<TAB>text; absent when there is nothing to say
JOBID.metrics.json latest host snapshot from the in-session sampler
JOBID.events.ndjson the session's event log, one JSON object per line
JOBID.poke touch to make the session re-check its deadline now
quota.json cached quota probe, per user
bin/<sha12>/ the extracted zellij bundle for this build:
sint-zellij.wasm, config.kdl, layouts/, .complete
xdg/zellij/ zellij's cache (plugin artifacts, permission grants)
The bundle is extracted once per build (the id hashes the zellij version, the plugin and the config), write-to-temp-then-rename, so concurrent first runs on different nodes cannot see a half-written tree; old bundles can be deleted freely. Per-session files are removed when the session ends.
Node-local state lives under SINTERACTIVE_RUNTIME_DIR (default /tmp):
sint-JOBID/ holds the zellij socket, the readiness marker and the path of
the config the server was started with. It never needs to be shared.
Alpine
/home is 2 GB, so export SINTERACTIVE_CACHE=/projects/$USER/.cache/sinteractive
in ~/.bashrc. /scratch/alpine would also do, but it is purged.
Installing on every node¶
Only for a system-wide install into a node-local /usr/local:
make nodes installs the same set of files as install-system — binary,
man page, bash completion and the share tree — on each node. The share tree
matters there and not only on the head node, because claude install
resolves it relative to the running binary, and someone who runs it from
inside a session is running the node's copy.
If the checkout lives on a cluster-wide mount and pdsh is available, each
node installs straight from the shared path; otherwise a tar is piped to
each node in turn. NODES (default sinfo -hN -o '%N') and SSH_USER
(default root) are overridable: sudo make nodes NODES="compute00
compute01".
The pdsh call asks for the ssh rcmd module by name (-R ssh). pdsh's own
default is rsh, which on a cluster with nothing listening on port 514
answers connect: Connection refused for every node at once — and a
PDSH_RCMD_TYPE=ssh exported in root's shell does not rescue sudo make
nodes, because sudo resets the environment. Override with make nodes
PDSH_RCMD=<module>; pdsh -V lists what is compiled in.
The binary is copied to a temporary name and renamed into place rather than
written over: on a node with a running session, the binary is that
session's zellij server, and overwriting a running executable fails with
"text file busy". Running sessions keep the old inode until they end, so an
upgrade never disturbs them; sinteractive doctor --nodes shows which
version each node has.
Upgrading from 0.x¶
The 0.x bash script needed tmux ≥ 3.7 on every compute node and, for a system-wide install, the script itself on each; both are gone. To upgrade:
- Install the binary as above. It can replace the script at the same path.
- Unset
SINTERACTIVE_TMUXin shell profiles; it is ignored. - Run
sinteractive claude installonce per user. It removes the 0.x hook scripts from~/.claude/hooks/, rewrites theirsettings.jsonentries tosinteractive claude hook …, and registers the statusline and MCP server. - Old top-level flags (
--status,--attach, …) keep working with a warning for one release; update scripts to the subcommands.
The state-file contract (<cache>/JOBID.json) is unchanged, so tooling that
polls it keeps working. tmux itself can stay or go; nothing reads it.