Files
serverAppConfigs/serverAppConfigs-docs/Operations/Remote Job Runner.md
Frank a5fe045b58 docs(vault): one fact, one owner — repair the doubled pages, single-source the facts
Structural repair:
- 13 pages were the same document twice (each exactly 2x its HEAD size),
  a second frontmatter block whose closing fence had been glued onto a body
  line by a frontmatter rewrite that re-read its own output. Un-doubled by
  cutting at the second frontmatter and keeping the copy whose body is a
  superset; the orphan census reports 0 lost tokens against HEAD.
- log.md: one entry per date, bare `## YYYY-MM-DD` headings, newest first —
  66 dated headings merged into 34, titles demoted to bold leads, every
  content line verified present. The file had been descending at the head
  and ascending at the tail.

Ownership moves:
- New `LLM/frank-medium.md` is the locator for the retired Dankserver
  endpoint: era windows quoted from each era page's own banner, no numbers
  restated, and the two unresolved date conflicts recorded instead of
  silently resolved. `frank-archon.md` links to it instead of restating the
  pre-rename window.
- The four Pipeline-Parallelism stubs (frontmatter only, no body) are gone;
  `Multi-Node Runtimes.md` owns runtime construction, launch, mixed-vendor
  verdicts and cross-split spec decode, and the four links that pointed at
  the stubs were repointed at it or at the drafter page.
- Authentik's three stub pages fold into `Authentik.md`, with the process
  detail and the outpost reference split out so the parent stays under the
  length hint.

Verified corrections (host census + live probe, 2026-10-19):
- The MXFP4 checkpoint two pages called the "on-disk rollback" is gone:
  /home/dank/models/ holds StillDeadcode, rad and tpl.jinja. Stated once, on
  the era page that owns that payload; the swap page now links to it.
- frank-medium-radiance-mxfp4: the 4.24/7 accept rate belongs to the DFlash2
  arm measured on the FP8 target, not to the MXFP4 era; the 3-family soak run
  census is kept once (155 requests) instead of twice (~120 and 155).
- Fleet shapes re-confirmed against the live endpoints before touching any
  page: 3x200000 fp8 KV, 2x200704, 2x262144.

Index rows are regenerated, not hand-carved. Net -1,849 vault lines.
2026-10-19 05:00:43 -07:00

5.4 KiB

type, title, description, tags, timestamp
type title description tags timestamp
reference Remote Job Runner Detached long jobs with sentinel files: why tail-watchers fail structurally, the script, its invariants, verification.
operations
ssh
jobs
sentinel
detached
2026-10-05T00:00:00-07:00

Remote Job Runner

Use for any long-running job on a remote host (benchmarks, model loads, builds, sweeps) whose result must survive the session. Also whenever you catch yourself writing ssh host 'tail -f log | sed …' as a watcher.

Why the obvious approach fails structurally

ssh host 'tail -f log | grep/sed …' as a background watcher fails in ways that all LOOK like "the job died":

  • the session broker aborts long-lived ssh — the watcher vanishes and the result is lost;
  • a ready regex can match the log's own header on the first line — the supervisor reports success instantly while the job is still running;
  • a sed -n '/PAT/,$p' filter exits at the first match, ending the pipe;
  • output goes into the ssh stream, so an aborted session loses everything.

The mechanism: detach on the HOST, poll a sentinel file

Completion is a .done sentinel touched last, not a log pattern. Status is then one fast independent command with no long-lived process to lose.

#!/usr/bin/env bash
# job.sh {start|status|wait|out|tail|watch} ...   Jobs live in $JOBS.
set -uo pipefail
JOBS=/home/dank/jobs
mkdir -p "$JOBS"

case "${1:-}" in
start)
    NAME="$2"; shift 2
    [ "${1:-}" = "--" ] && shift
    rm -f "$JOBS/$NAME.done" "$JOBS/$NAME.rc"
    # setsid detaches from this session's process group: the critical bit.
    setsid nohup bash -c '
        NAME="$1"; shift
        "$@" > "'"$JOBS"'/$NAME.out" 2>&1
        echo $? > "'"$JOBS"'/$NAME.rc"
        touch "'"$JOBS"'/$NAME.done"
    ' _ "$NAME" "$@" < /dev/null > /dev/null 2>&1 &
    echo "started $NAME"
    ;;
status)
    # Glob *.out (not *.done) or running jobs are invisible.
    names="${2:-$(cd "$JOBS" && ls *.out 2>/dev/null | sed 's/\.out$//' | sort)}"
    for n in $names; do
        if   [ -f "$JOBS/$n.done" ]; then
            printf '  %-18s DONE   rc=%s  %s lines\n' "$n" \
                "$(cat "$JOBS/$n.rc" 2>/dev/null)" \
                "$(wc -l < "$JOBS/$n.out" | tr -d ' ')"
        elif [ -f "$JOBS/$n.out" ]; then
            printf '  %-18s RUN    %s lines so far\n' "$n" "$(wc -l < "$JOBS/$n.out" | tr -d ' ')"
        else
            printf '  %-18s PENDING\n' "$n"
        fi
    done
    ;;
wait)
    NAME="$2"; LIMIT="${3:-3600}"; waited=0
    while [ ! -f "$JOBS/$NAME.done" ] && [ "$waited" -lt "$LIMIT" ]; do
        sleep 5; waited=$((waited+5))
    done
    [ -f "$JOBS/$NAME.done" ] \
        && echo "DONE $NAME rc=$(cat "$JOBS/$NAME.rc" 2>/dev/null)" \
        || echo "TIMEOUT $NAME after ${waited}s"
    tail -"${4:-25}" "$JOBS/$NAME.out" 2>/dev/null
    ;;
out)  NAME="$2"; tail -"${3:-40}" "$JOBS/$NAME.out" 2>/dev/null ;;
tail) [ -n "${2:-}" ] && exec tail -f "$JOBS/$2.out" || exec tail -f "$JOBS"/*.out ;;
watch) while true; do clear; date '+%H:%M:%S'; bash "$0" status; sleep 5; done ;;
*) echo "usage: job.sh {start|status|wait|out|tail|watch}" >&2; exit 2 ;;
esac
job.sh start NAME -- <command>   # launch detached
job.sh status [NAME]             # one-shot state
job.sh wait NAME 900 25          # block, then print the tail
job.sh out NAME 60               # retrieve any past result
job.sh tail [NAME]               # live follow
job.sh watch                     # refreshing status table

Invariants

  • .done is touched LAST, after the exit code is written — its presence alone means the output file is final.
  • Glob *.out for status, never *.done — globbing .done hides running jobs entirely, the exact blindness this fixes.
  • Capture the exit code (rc=7 proved a self-test worked; without it a job can "complete" having failed).
  • Never put the payload in a line you grep for — the sentinel is the only completion signal.
  • Never pkill -f <pattern> where the pattern also matches your own ssh command line — it kills the session. Distinctive names or pkill -x.
  • Write helper scripts to a file and scp them. Triple-nested ssh 'bash -c "python3 -c \"…\""' quoting broke repeatedly (unterminated quotes; grep -c returning "0\n0" because || echo 0 appended a line); every failure disappeared once the script was a file.

Verify before trusting it (3am law)

job.sh start selftest -- bash -c 'echo one; sleep 2; echo two; exit 7'
job.sh start slowtest -- bash -c 'echo start; sleep 30; echo end'
sleep 4; job.sh status      # selftest DONE rc=7; slowtest RUN

Both paths, including the failure case, and the rc round-trip — a runner that only proves the happy path is not trustworthy.

Reporting discipline

  • Give the user a real terminal when the run is long: alacritty --title "bench - dankserver" -e bash -c 'ssh -o ServerAliveInterval=30 -t dank@192.168.0.235 "bash /home/dank/job.sh tail"' &
  • State the MECHANISM, not vibes: the job is durable on the host; job.sh out NAME retrieves it later. Do not narrate "still loading" repeatedly — check once, act, report the resolved result.
  • When the user asks for status, one fast command, not a narration.