Infrastructure #
The machines the generation loop runs on, and how work reaches them. The loop itself is
in docs/generation-loop.md; measured rates are in docs/rig-capacity.md. Hardware is
named by role and class, never by hostname, address or account — this repository is
assumed public. Real names live in the gitignored bench/*.local.json and the local rig
notes.
In brief #
- The loop. One engine function per spec algorithm (for example the CSS 2.1 width calculation), written by a model, checked against cached browser results.
- Sampling tier. GPU cards running mid-sized models write N versions of a function from the spec text, the engine's Rust interfaces and the currently failing cases. Many cheap tries beat one careful one, because the browsers say which tries are right.
- Capacity tier. When every version fails, a larger model gets the spec, the best failing versions and the exact per-box geometry differences.
- Verification tier. CPU servers compile candidates in one batch and run each inside the engine over the cached corpus, under a millisecond per page. The same tier builds that cache by running the three browsers once per page, and runs conformance suites.
- Outcomes. Keep the passing version, sample again with the new failures, or escalate. What is kept is ordinary Rust with a provenance record; no model runs in the shipped engine.
Environment variables are named NEV_*; older records use GENERA_*, which every reader
still accepts (docs/rename-to-nev.md).
Detail #
The full cycle #
flowchart TD
browsers["Chromium, Firefox, WebKit<br/>once per page (CPU)"] --> cache["cached ground truth<br/>three-engine vote"]
spec["spec text<br/>one algorithm"] --> sample["SAMPLE (GPU pool)<br/>N versions of one function"]
sample --> verify["VERIFY (CPU)<br/>dedupe, batch compile,<br/>run over the corpus"]
cache --> verify
verify -- "all pass" --> accept["commit Rust source<br/>+ provenance record"]
verify -- "some fail" --> sample
verify -- "all fail repeatedly" --> escalate["ESCALATE<br/>larger model + geometry diffs"]
escalate --> verify
Nothing is accepted on a model's word: escalated output goes through the same check. Writing candidates is the binding constraint; verification has a large margin over it.
What comes out, and how the engine uses it #
The cycle outputs ordinary Rust source, committed next to the hand-written code it
plugs into, with spec steps as comments and a provenance record (roadmap.md →
"Provenance is a first-class artifact"). The engine compiles it like any other code; the
GPU and CPU fleets exist only at build time.
Provenance drives maintenance. When a spec section or a definition it uses changes,
the provenance index finds exactly the functions to regenerate (roadmap.md → "A
specification changes"); a browser update re-dumps the ground truth instead. The same
index is the bill of materials for a configurable build (roadmap.md → "Phase 4").
Measured baseline #
One oracle process, three engines, 235 documents including browser startup: 17.9 s,
about 13 documents per second per engine, using about two cores. Scaled to a few
hundred cores that would be about 3,000 documents per second, a ten-million-case corpus in
about an hour — but per-core throughput on the contended 16-core development box fell from
1.48 to 0.48 docs/s/core between 1 and 16 workers (docs/rig-capacity.md → "Oracle
corpus building"), so the figure on dedicated hardware is still to be measured. GPU-side
figures: docs/rig-capacity.md → "GPU tier".
Three tiers #
Sampling tier — high memory bandwidth GPUs. Bulk best-of-N generation, throughput-bound. Decode speed tracks active parameters, so a mixture-of-experts model with a small active fraction gives several times the candidates of a dense model. Ampere cards have no fp8, so quantization is 4-bit weight-only.
Capacity tier — large unified-memory accelerators. About a third of the bandwidth, several times the memory: for the few hard calls and any teacher model. About 100B total with ~12B active is the realistic ceiling.
Verification tier — many CPU cores. Corpus building, candidate compilation and the inner loop. It must never spawn a browser per case.
As discovered #
Every host advertises itself over mDNS and answers an HTTP inventory; a collector aggregates it. What that shows, by role:
- The model gateway is a LiteLLM instance fronting one alias per model pool. The
aliases, their pools, how many requests each takes at once, and which are retired are
declared in
bench/folknet.json(the two GLM-5.3 Flash aliases, without backends since 2026-09-22, were removed from the gateway on 2026-09-25); whether each one answers right now is in each lane's snapshot, which a probe refreshes every 5 minutes andbench/folknet-status.sh [--lane N]prints (status, how long it has held it, last answer, snapshot age). Design:docs/folknet-inventory.md. Virtual keys carry per-key allowlists, so a new alias must be granted to the caller's key. Each pool's HAProxy reports liveness, not readiness: a/v1/modelsanswer does not prove a completion will return. - Four ARM unified-memory nodes (DGX Spark class) —
aarch64, one GB10 GPU, 20 cores, 121 GB RAM, 3.7 TB NVMe each. They served GLM-5.3 Flash as two independent two-node pairs until 2026-09-22. Qwen3.8-Flash-Next did not run under any vLLM build on the rig (the architecture was too new for the rig's EXL3 fork, and upstream has no EXL3 backend), so it serves through the checkpoint's own engine (TabbyAPI) instead, one replica per node, about 75 GB plus ~45 GB of page cache each. Two of the four now serve DeepSeek-V4.1-Flash. Full account:docs/rig-capacity.md→ "Attempted — Qwen3.8-Flash-Next". - The eight-card host — eight 24 GB consumer cards on a 64-core EPYC with ~500 GB
RAM, bare Linux, six serving containers using every card (four behind
-fast, two behind-long). The closest thing to the sampling tier as designed. No spare GPU. - The verification tier — ten Proxmox 9.x nodes (EPYC 7742, 7B13, 7K62 and 7452; 32 to 64 physical cores, 125 GB to ~500 GB RAM, 1–2 TB NVMe), shared with tenant VMs; five pass thirteen consumer cards through to tenant machines. A VM sized for browser builds sits on the GLM-pool proxy host (open: its core count and whether it should be running). Free capacity is host by host.
- A downloader host pulls checkpoint shards in parallel across the site's four WAN uplinks onto the NAS, verifies them and publishes a pinned revision for nodes to sync.
- Access. Root with the jump host's key works on every physical host; the ARM nodes use their own admin key. The gateway's own guest refuses, and is not a compute host.
Where each piece runs #
Every role is a guest or a container; nothing is installed on a Proxmox node's own OS.
flowchart LR
dev["development box<br/>16 cores, shared"] -- "ssh via jump host" --> lanes["2 build lanes<br/>(LXC, 96 threads each)"]
dev -- "port forward" --> gw["model gateway<br/>(LiteLLM)"]
lanes --> gw
gw --> fast["eight-card host<br/>-fast, -long"]
gw --> vm["four-card VM<br/>-fast-2"]
gw --> fn["2 ARM nodes<br/>flash-next"]
gw --> ds["2 ARM nodes<br/>deepseek pair"]
lanes --> helpers["compile helpers<br/>(sccache-dist)"]
- Development box. No GPU, no rig role; holds the port forwards to the gateway.
- Jump host. A VM on one node; the only route in. Nothing of this project on it.
- Build lanes. One unprivileged LXC on each of two nodes (
bench/lane-create-lxc.sh,bench/lane-provision.sh), started at node boot. A gate running when a lane stops fails. - Compile helpers and scheduler. Four more containers plus builders on the two lanes;
scheduler, redis cache and a loopback CONNECT proxy on one lane (
bench/lane-dist.sh). - Eight-card host. Docker containers under a templated unit
folknet-qwen38-vllm-worker@<profile>, behind that host's own HAProxy on port 4000. Not edited by this project. - ARM nodes, split two ways since 2026-09-24.
- Two serve
qwen3.8-flash-next, one systemd unit and container each on port 5000 (bench/sparks/), behind the proxy host's HAProxy (least connections, eight in flight per replica) — so the alias takes 16 in flight, not 32; a batch at concurrency 32 queues the rest. Either replica can stop alone. - Two, joined by a direct cable, serve
deepseek-v4.1-flashas one tensor-parallel pair: one oneshot unit on the head node (bench/sparks/dsv41/folknet-dsv41-exl3-pair.service) starts both containers and returns once the API is healthy (about 7.5 minutes cold); stop and start only through that unit, never one node alone. HAProxymaxconn 2. - Units are stopped and started through a privileged container that chroots into the host, because the admin account has no sudo. GLM units are disabled on all four.
- Two serve
- Four-card VM (
qwen3.8-27b-fast-2). Four 3090s borrowed from a stopped tenant VM on the same node (only one of the two may run;bench/gpu-vm/). Four unitsfolknet-qwen38-vllm-worker@fast-{0..3}on ports 8101–8104, the eight-card host's-fastrecipe, HAProxymaxconn 4per worker (16 in flight). A separate alias because the-fastload balancer lives on the eight-card host.
Open. Which host carries the four-card VM behind qwen3.8-27b (the measured Qwen
server), and how it is started and stopped; whether anything restarts the eight-card
host's HAProxy or re-pulls its image;
what keeps the development box's port forwards up after a reboot; and the gateway config
is generated from a site file outside the rig, which needs every hand-added alias too.
Sharing the hardware #
The Proxmox nodes are shared with other tenants. Three rules:
- Nothing of this project goes on a node's own OS, so a guest can be stopped and its hardware handed back without touching the node.
- GPU work runs in a VM with passthrough, never an LXC (an LXC would need the vendor driver on the node). CPU work runs in an unprivileged LXC, or in docker on non-Proxmox hosts.
- Stopping a project guest frees what it held, and a lane competes with tenants as an equal under contention. Nothing checks this; it is how the rig is laid out.
Limits today: the ARM replicas' HAProxy queues a request at most 30 minutes; power
budgets are modelled (bench/capacity.py) but not enforced.
Not built yet: a per-role start/stop/drain wrapper; a drain (a replica's stop gives in-flight replies only 60 s); taking a backend out of HAProxy before stopping it; a narrow sudo rule for the ARM nodes' admin account.
Build lane #
Two unprivileged containers (96 logical CPUs — three quarters of a 64-core, 128-thread
node — 96 GB, 200 GB disk), each a build lane and oracle host: the Rust toolchain, Node
22, the repository cloned from the rig's forge ("Where the repository is stored" below),
Playwright's three browsers, and a PulseAudio null sink so a realtime AudioContext runs. The scripts are the record and are
re-runnable: bench/lane-create-lxc.sh, bench/lane-provision.sh, bench/lane-nas.sh.
Running a gate there.
sequenceDiagram
participant W as worktree (dev box)
participant L as lane
participant F as rig forge
W->>L: ssh: git bundle of unpushed commits + diff + untracked files
L->>F: fetch the lane's mirror (only if needed, or older than 10 min)
L->>L: lane-apply.sh: check out exact commit, apply diff, run command
L-->>W: streamed output (and --pull directories)
bench/remote-lane.sh <worktree> <command...> does this. Each lane root keeps one bare
mirror (<root>/.git-mirror) with one git worktree per track; it fetches only when the
bundle cannot supply the commit or the last fetch is older than NEV_LANE_FETCH_MAX_AGE
(600 s), because the public forge the mirrors first fetched from rate-limits. The lane
address comes from NEV_LANE_SSH or bench/lane.local.json; the mirror URL from NEV_LANE_GIT_URL, that file, or the
worktree's origin. Examples:
bench/remote-lane.sh . cargo test --workspace
bench/remote-lane.sh --pull corpus/absolute-ground-truth . \
cargo run --release -p generation --bin absolute_ground_truth_cache
Why a bundle rather than rsync: uploads from the development box to the lane run at about 18.6 KB/s (downloads 435 KB/s), so rsyncing a worktree took ~315 s of a ~424 s run.
Where the repository is stored. Two forges, with different jobs:
| forge | role | holds |
|---|---|---|
| the rig's Forgejo | intermediate storage, on the rig network | everything the lanes need: main, job branches job/<name> and their results result/<name> (specs/job-submit-land.md) |
| tangled | persistent storage, public | main and the spec/<name>/<sha> landing tags only |
The Forgejo repository is a regular repository, not a mirror of tangled. The lanes clone
and fetch from it: each lane root's .git-mirror and the lane user's clone fetch from
http://<forge-user>:<forge-token>@<forgejo-address>/<owner>/<repo>.git, and
bench/lane.local.json git_url (or NEV_LANE_GIT_URL) names the same URL. The user and
token come from the primary checkout's .env (GENERA_FORGE_USER, GENERA_FORGE_TOKEN,
still under the old prefix; docs/rename-to-nev.md). The development box reaches Forgejo
through a local port forward, as the remote rig
(http://<forge-user>:<forge-token>@127.0.0.1:<forward-port>/<owner>/<repo>.git), which
the job scripts use by default (JOBS_FORGE_REMOTE). origin fetches from tangled and has
push URLs for both forges, so git push origin main updates both; job and result branches
go to rig only and are deleted there after a landing. Each of those URLs carries the
token: they live in .git/config and the gitignored files, never in a committed file, a
log line or a message.
The track gate runs only on the build lane. tracks.json → verify runs
bench/verify.sh, which hands itself to bench/on-lane.sh --lane-only and, once there,
runs cargo test --workspace, the job event store's tests (jobs/events/*.test.mjs under
node --test, then bench/job-events-lane-test.sh), and then
bench/generated-scores.sh. The other *.test.mjs and bench/*-test.sh files are run by
hand, not by the gate. With no lane
configured or none answering (ssh -o BatchMode=yes -o ConnectTimeout=5 fails) the gate
exits 3 and runs nothing on the development box; a lane that answers but fails the tests or
a score is a failed gate — no local retry. Measured before the scores were added: a new
worktree's first test run 106 s (124 s with a fresh mirror), a warm rerun 22 s, local
fallback 78 s. A clean cargo test --workspace compiles in 61 s on a lane and 57 s on the
development box — the build is bound by the dependency chain through Stylo, not by cores;
the lane's value is that it takes load off the shared box and runs several gates at once.
Unattended landings never fall back to the development box. bench/on-lane.sh
without --lane-only runs its command locally when no lane answers, and bench/land.sh
runs each landChecks command that way, and verify directly. Setting
NEV_LANE_STRICT=1 in bench/land.sh's environment (the scheduled
jobs/scripts/poll-and-land.sh gets it from tracks.json's specWatch.pollEnv) makes
verify go through bench/on-lane.sh --lane-only too and every check --lane-only, so
with no lane answering the landing is refused with "verify failed on the tip (exit 3: no
build lane answered ...)" and nothing of the gate runs here. Unset, as in interactive
use, a check keeps its local fallback. bench/verify.sh itself is lane-only either way;
the one thing that would run it on the development box is NEV_ON_LANE set there by hand.
Already on a lane, bench/on-lane.sh runs the command in place. With NEV_ON_LANE
set, bench/on-lane.sh (and bench/remote-lane.sh) runs its command in the named
worktree on the machine it is on, --lane-only or not, reads no lane settings, and
ignores --pull, since the outputs are already where they belong; the command's exit
status is the run's. This is the same check bench/verify.sh makes, so a spec's
bench/on-lane.sh --lane-only "$PWD" ... works unchanged inside a Dagu job, whose clone
has no .env and no bench/lane.local.json and whose writer gets NEV_ON_LANE=1 from
jobs/scripts/pi-session.sh. As with bench/verify.sh, a NEV_ON_LANE set by hand on the
development box would run the command there. bench/on-lane-test.sh covers both sides.
The cost: with both lanes down nothing lands until one answers. bench/land-test.sh
scenario s1 covers both settings.
The gate scores every generated function against its own corpus. A committed file under
crates/layout/src/generated/ can be wrong while every test passes, because the tests
exercise in-tree reference rules, not the generated function. bench/generated-scores.sh
clears and rebuilds each corpus with its *_ground_truth_cache binary (the browser oracle
runs on the lane), then generated_score_check scores each generated function through its
own runner and fails it against its floor. The floor is two numbers in
crates/layout/src/generated/<name>.floor.json beside the function's provenance record:
cases_passed, how many cases it passes, and cases_run, how many cases its corpus holds
(only cases three engines agreed on). It fails when the passing count drops, when the corpus
shrinks even though the passing count holds, and when a function passes no case at all (a
lane missing a browser leaves fewer than three engines to vote, so every case drops; a floor
of zero cannot hide a function that scores nothing). The provenance record describes the
model batch that admitted the function and is never rewritten; the floor is never below its
verification.cases_passed, and a function with no floor file yet is held to that alone. A
placeholder record, such as the hand-written table one (0 recorded), therefore floors at
its real score once a floor file exists.
Floors rise by themselves, after a landing. bench/after-land.sh ends with
bench/generated-floors-update.sh, which runs bench/generated-scores.sh --print-raises on
the build lane, writes each floor that the fresh score is above (a function with no floor
file gets one), and commits only those files as one commit on main, the way the map and
dashboard commits are made. It refuses on uncommitted changes, writes nothing when the lane
does not answer or a score fails, never lowers a number, and a failure is one line and does
not change the landing. Raising happens after the landing rather than in the gate because
the gate runs on the lane's copy, where a commit cannot reach the track, and because a track
never edits a floor: bench/land.sh refuses a diff that touches any *.floor.json, so an
unattended track cannot lower one. The cost is a second scoring run on the lane per landing,
and a window between a landing that improves a function and the raise in which the old floor
holds. A floor comes down only by a person's reviewed edit of its file.
One place to add a generated function. A function is discovered from its
<name>.provenance.json; its runner and the cache binary that writes its corpus are one
entry in FUNCTIONS in crates/generation/src/bin/generated_score_check.rs, and a
provenance record with no entry fails the gate with that instruction. bench/generated-scores.sh
takes the caches to rebuild from generated_score_check --list-caches, so nothing under
bench/ changes and a track adding a function can land unattended.
bench/generated-scores-test.sh exercises the scripts, including the raise, without a
browser.
Model batches run only on a lane. bench/absolute-mixed-batch.sh and
bench/overflow-mixed-run.sh are started from the development box like any script
(NEV_MODELS=qwen3.8-27b-fast:32:32 bench/absolute-mixed-batch.sh) and re-run themselves
on the lane through bench/on-lane.sh --lane-only (bench/batch-lane.sh): the NEV_*
settings go along, credentials and lane settings excepted, and the batch's results come
back with --pull. With no lane configured or none answering the script exits 3 and runs
nothing locally; bench/lane-apply.sh exports NEV_ON_LANE=1, which is how a script
knows it is already there. Each run also records its phase timings and how many of the
lane's CPUs each phase kept busy (bench/cpu-sample.sh, bench/cpu-phases.py;
docs/rig-capacity.md → "A real batch on a build lane").
A stray directory at a worktree's lane path breaks the gate. bench/lane-apply.sh
tests only for <lane-root>/<name>/.git; a leftover directory without it makes git worktree add fail, and the gate cannot pass until it is removed by hand (check git -C <mirror> worktree list first). Not yet fixed in the script.
Cleanup. Every worktree run on a lane leaves its copy (<root>/<name>, target/
and all, 3–12 GB) and <root>/.payload-<name> there; left alone they filled a lane's
disk. bench/lane-sweep.sh removes, on every lane in bench/lane.local.json lanes[],
the copies whose track has no worktree in the primary checkout (--dry-run lists them
with sizes). It keeps the primary's own copy, any copy with a process running in it (a
spec writer runs inside its copy), and directories that are not worktrees of the lane's
mirror. Untracked data under a copy's corpus/ (links excepted: they point into the
NAS already) goes to the NAS first, as one tar file at
archive/corpus/<name>/<time>.tar; with no NAS directory the copy is kept. A branch a
copy was on stays in the mirror. It runs at three points: by hand; from bench/land.sh
after a landing, for that track only (a failure is named on its line and undoes
nothing); and from bench/remote-lane.sh when the lane's root filesystem has less than
NEV_LANE_MIN_FREE_GB (20) free before a run, which then refuses with exit 4 if the
sweep did not free enough, naming the lane by its place in the list, the free space
and the largest copies. A hand landing leaves no worktree, so the next sweep takes its
copies. Scenarios: bench/lane-sweep-test.sh.
Test-run memory. A cold cargo test --workspace on the development box adds about
1.1 GB at peak (10.8 → 12.0 GB), a warm one 67 MB; seven concurrent tracks fit in its 30
GB if their peaks do not coincide.
Distributed compilation. sccache-dist builders on six nodes (416 cores registered)
with redis as the shared cache. After dropping one builder on a node with slow outbound
network (failures 84 → 1), it still lost on wall time: a clean -p dom release build took
82 s distributed against 45 s local on the same lane. The dependency graph is too narrow
to use the cores. A wide build fares no better: a batch's candidate probe (one
--keep-going build of 32–64 small crates) took 14–45 s through sccache-dist against
1.4–1.6 s with no compiler wrapper on the lane, just as slow on a second pass, so each
compile through the wrapper carries a fixed delay far larger than the compile itself
(docs/rig-capacity.md, "A real batch on a build lane"). The delay is the input
upload: for a Rust compile the sccache client packages every library in the
-L dependency= directory (target/release/deps, 225 rlibs, 336 MB), not only the
crates the compile loads. One probe library compile (0.1 s with plain rustc) took
4.4 s distributed, sent 121.9 MB (the gzip size of that whole directory) and spent 2.3 s
of client CPU compressing it. The cost grows with the dependency directory, not with the
crate, and is paid again on every compile, which likely explains the -p dom result
too (not measured). The probe therefore runs without the wrapper, on the lane's own cores; the
development box's sccache config stays untouched.
Shared data on the NAS. Each lane bind-mounts one NAS directory at
/srv/lane-shared (NEV_LANE_SHARED, also the npm cache). Login shells get the npm cache
from ~/.lane-env; the Dagu job's prepare and run steps (bench/dagu/job.yaml) read
only npm_config_cache from ~/.lane-shared-env, and bench/oracle-install.sh runs
npm ci --prefer-offline, so a package is downloaded from the registry once and every
lane and job after that installs it from the share. The NAS is fast from the lanes
(about 92 MB/s write, 114 MB/s read) but slow for small files (~38 ms each), so target/
stays local. bench/lane-apply.sh links each worktree into it on every run:
| in the worktree | on the share |
|---|---|
oracle/suites/checkouts/<suite> |
suites/<suite>-<pinned commit>/ |
webgpu-cts/cts |
suites/webgpu-cts/ |
corpus/<name> for every *ground-truth cache the code names |
ground-truth/<name>/ |
oracle/suites/results |
archive/suite-results/<worktree>/ |
webgpu-cts/results |
archive/webgpu-cts-results/<worktree>/ |
| (the command's output, copied) | archive/gate-logs/<worktree>/<time>-<guest>-<commit>.log |
(one line per model batch, bench/ledger.py) |
ledger/<worktree>.jsonl |
(a Dagu job's full logs, bench/dagu/job.yaml, scrubbed) |
archive/job-logs/<job name>/<Dagu run id>/ |
The last row comes from the Dagu job workflow, not bench/lane-apply.sh: it copies each job's stage logs,
state.json, the review, each pi session's prompt and events.jsonl (its turns and
tokens) and oracle-install.log there when a run ends,
whether it passed, failed or was stopped, with an archive.txt listing every file and its
line count; the step log prints the path. Credentials, URLs, addresses and token-shaped
values are replaced on the way (bench/dagu/job.yaml, "handler_on"). Neither
archive/gate-logs/ nor archive/job-logs/ is pruned: both grow until removed by hand
(find <share>/archive/job-logs -mindepth 2 -maxdepth 2 -type d -mtime +30 -exec rm -rf {} + drops job runs older than 30 days).
The ledger is what a landing's announcement to the community Discord reads for its
numbers (docs/web-features-map.md → "Announcements"): each batch's start and end time,
lane CPU-seconds, and per model the candidates, compiled count, best score, tokens and
draw seconds, with no host names or addresses.
An existing real directory is left alone. A ground-truth cache written by one branch is read by every other, so a recipe change should regenerate its cache. The export is open to the whole subnet: nothing secret goes on it. Measured: fetching the suites into an empty share took 6 min 19 s; a second lane then found them in 13 s.
Reaching the model gateway. A lane reaches the gateway at its rig-network address,
not the development box's loopback forward. Each lane keeps that address in
~lane/.lane-model-env as NEV_LANE_MODEL_URL (written by bench/lane-provision.sh
from LANE_MODEL_URL), and bench/lane-apply.sh swaps it into the worktree .env's
NEV_MODEL_URL line on every run, leaving the key as copied. The address never enters
the repository (bench/lane-apply-test.sh).
No credentials left behind. The worktree's .env (the gateway key, usually the forge
token) is copied into the lane copy for the run only. When the run ends -- success,
failure or interrupt -- bench/lane-apply.sh removes the lane copy's .env, its
temporary rewrite and the payload's .env, and bench/remote-lane.sh removes the same
three over its own ssh, which covers a run whose connection dropped first. Nothing else in
the lane copy is touched. NEV_LANE_KEEP_ENV=1 keeps them, and
bench/lane-writer-launch.sh sets it, because the writer it starts outlives the launch
and its model batches read that .env. Two known gaps: an interrupted run exits 130
(SIGINT) or 143 (SIGTERM) locally but does not stop the command on the lane, which runs
until it finishes or next writes to the dropped connection; and the git mirror's clone
URL can carry the forge token, which then shows in the lane's process list as an argument
to lane-apply.sh, a copy of that secret outside .env.
Running writers on a lane. No script here names an agent harness; the harness is a
declaration. tracks.json → specWatch.lanes.tool names it (TRACKS_TOOL in the
environment is the fallback, and a launch with neither is refused), and pi-tracks'
writer command prepares that harness on the lane and starts the session.
bench/lane-provision.sh's writer-host section (enabled by LANE_PI_TRACKS_URL) clones
pi-tracks on the guest and prints changed:/unchanged: per step, so a rerun is
idempotent. What a particular harness needs installed ahead of time is a command the
operator supplies as LANE_WRITER_SETUP, run as lane with LANE_WRITER_TOOL and
LANE_WRITER_DIRS (the lane roots and mirrors, relative to the home directory) in its
environment; with none set the step does nothing. A harness login is a person's step
over ssh as lane. bench/lane-writer-launch.sh <worktree> <lane-index> <model> <brief-file> syncs a worktree, checks out its branch, pulls pi-tracks and runs
"$PI_TRACKS_HOME/bin/writer-exec.sh" --tool <name> --model <model> --brief-file <path>
there (PI_TRACKS_HOME defaults to $HOME/.local/share/pi-tracks), refusing over a
dead writer's unfetched commits and failing when that command exits nonzero; the brief
lands in ~/.cache/lane-writer-briefs/ on the lane, outside the worktree.
bench/lane-writer-fetch.sh brings commits back fast-forward only. tracks.json →
specWatch.lanes declares both. bench/lane-nas.sh decides a guest is busy from the
process names in LANE_BUSY_COMMANDS (default cargo|rustc|node); add the harness's
process name when a writer runs under a different one. crates/harness/tests/no_harness_name.rs
fails the workspace tests when a bench/ script, tracks.json (other than the tool
entry) or .gitignore names a harness. bench/lane-writer-launch-test.sh tests the
launcher against a stand-in writer command; a real launch on a lane is unverified until
the pi-tracks writer command is on its main.
Dagu on a lane. A job (specs/job-submit-land.md) runs as one run of the Dagu workflow
bench/dagu/job.yaml, on the lane NEV_DAGU_LANE names (an index into
bench/lane.local.json, default 0; Dagu's coordinator is off, so one lane).
bench/dagu-lane.sh installs, starts, reaches and cleans up after it; its header lists the
actions and settings. Without --lane it takes the same lane, NEV_DAGU_LANE or 0,
unless NEV_DAGU_LANE is unset and NEV_LANE_SSH names a lane, which then wins.
jobs/scripts/submit.sh and jobs/scripts/poll-and-land.sh check
manifests and results with the job-check binary from $JOBS_STATE_DIR/bin (default
~/.local/state/nev-jobs/bin) on the development box; jobs/scripts/install-job-check.sh
builds it on a lane (bench/on-lane.sh --lane-only --pull) and installs it there, and is
rerun after a change to jobs/.
-
The requirements check. Before the
runstep'sjob-runstarts the first stage that is notpassed, on every start and every resume, it checks that the lane has what the manifest is about to need --git,python3,nodeandkillresolvable,cargoand the oracle's Playwright browsers when a writer is in the stage list, a reachable model gateway when a stage runs a pi session, 20 GiB free on the workdir's filesystem, the shared archive directory,NEV_ON_LANEwhen a stage callsbench/on-lane.sh,bench/verify.shorrun-batch.sh, and at least 600 s left of the wall-clock limit across resumes (jobs/src/preflight.rs). One unmet requirement prints every reason to stderr, records them in state.json'spreflightfield, runs no stage and exits 9 -- distinct from 7 (publish failed) and 8 (stopped), and never resent byjobs/scripts/poll-and-land.shthe way 7 and 3 are, since a missing requirement is still missing a minute later. Run the same check by hand withjob-check preflight <manifest> [--workdir <dir>] [--state <dir>], exit 0 when every requirement is met, 9 otherwise;--workdirdefaults to the current directory and{tools}in a stage's command resolves under itsjobs/scripts. This check only asks what the lane itself can answer right now (a binary, a socket, a directory); whether a model or provider is currently available on the rig is the separate inventory check that follows it. -
The inventory check. Once the requirements check passes, the
runstep'sjob-run --inventory ~/folknet/inventory.jsoncopies the lane's snapshot into the state directory (again on every resume), hands every stageJOB_INVENTORY(the copy) andJOB_INVENTORY_LIVE(the lane's own path), and checks the manifest against it and the declared filetools/folknet.json(the submitting checkout'sbench/folknet.json). A model that is not ready, or a snapshot older than 15 minutes or missing, prints every reason, records them in state.json'sinventoryfield, commits anot-runresult locally (not published), runs no stage and exits 3, whichjobs/scripts/poll-and-land.shresends uncounted. A refusal that never clears by itself -- a model retired or not declared, a provider not declared or not allowed -- is reported the same way but commits afailedresult and exits 10, which the poller records asfailedwithout resending. The same check by hand:job-check inventory <manifest> <snapshot> [--declared <file>](docs/folknet-inventory.md-> "The check before the first stage"). -
Installed as a plain binary. The lanes have no container runtime (unprivileged LXC, no
dockerorpodman, no passwordlesssudo; rootless podman would also need/dev/fuseand/dev/net/tunadded to the guest's config on its node), so Dagu runs as the lane user from~/dagu:bench/dagu-lane.sh --lane N upinstalls the pinned release (its sha256 checked against the pin and the release's ownchecksums.txt), pushes the gateway key, starts it and checks it.stop, then deleting~/dagu, removes it.autostart onadds a lane-user@rebootcrontab line, which has brought Dagu back after a guest restart. -
Reached only through ssh. Dagu binds to the lane's loopback (
NEV_DAGU_PORT, default 18431) with basic auth. The password is generated on the lane, kept in~/dagu/home/config.yaml(mode 600) and read over ssh when needed, never stored on the development box.tunnelholds a port forward open for the web UI,/api/v1and/mcp. The MCP server is the/mcproute ofdagu start-all(Dagu 2.x has nodagu mcpcommand). The one MCP client is the curl-based one inbench/dagu-lane.sh(run,stop-run,watch). -
Secrets never go through run parameters. Dagu keeps a run's parameters in its run records on disk, so
runrefuses a parameter whose name contains KEY, TOKEN, SECRET, PASSWORD, PASSWD or CREDENTIAL. The gateway key goes bykeyover ssh on stdin to~/dagu/home/model-key(mode 600);startexports it asNEV_MODEL_API_KEYwithDAGU_ENV_PASSTHROUGH=NEV_MODEL_API_KEY, so every step sees that one variable and it lands in no run record or log. Steps start from a non-login shell and inherit nothing from~/.lane-env: the gateway address comes from~/.lane-model-env, the npm cache and the share from~/.lane-shared-env(one variable at a time), and the forge through the lane mirror's origin URL, which carries the credential; git messages from forge commands pass through a filter that replaces anything shaped like a URL. -
How Dagu 2.17.2 behaves. Workflow keys are snake_case (
retry_policy,hist_retention_days); camelCase is refused. Onedagu_executecall waits at most 300 s, sorunfollows a longer run by reading its status every 15 s until it ends orNEV_DAGU_WAITseconds pass. Dagu records no exit code for a step:watch --once --step <step>takes 0 for a succeeded step and otherwise the N of the step's error textexit status N, and-when there is none (a step killed by a signal). The exit handler (handler_on: exit) runs after every run, succeeded, failed or aborted, but not after a run cut off by a lane restart or a KILL of Dagu. -
The queue. A workflow's own
max_active_runsabove 1 is ignored for local queues; the limit is a global queue in~/dagu/home/config.yaml(queues.config[].max_concurrency, written byset_queue(): queuejobs, 2 at once), read only when the scheduler starts, so a change needsstopthenstart. Only enqueued runs pass through it (dagu_executeaction=startruns at once), sorunenqueues any workflow file with a top-levelqueue:key. A second submission while one is building passes--no-sync, so the source copy under the first run's build is not replaced. -
What it keeps. Run history 7 days (
hist_retention_days, set in~/dagu/home/base.yamlbyset_retention()and injob.yaml): the forge holds each job's result. A job's directory~/dagu/jobs-work/<name>/stays until the job passes, so a new run with the same parameters resumes it, and after a pass it stays too when its logs did not reach the NAS share (stepcleanupremoves it only once the archive wrote its markerarchived-<Dagu run id>there, and says in its log when it kept it);clean-jobs <name>removes one by hand. The job tools are built from the copy of the sourcesrunsends to~/dagu/src(sync_source()), not from the job's base commit. -
The mirror keeps deleted job refs. The lane mirror's fetch (refspec
+refs/*:refs/*, no prune) copies everyjob/*andresult/*branch from the forge and keeps it after the forge deletes it.prune-mirrorfetches and prunes those two namespaces only;bench/dagu/fixture-base.sh --deleteruns it.
Landing approved spec tracks from a tick #
bench/land.sh <spec-name> <spec-sha> <branch-tip> lands an approved track without a
person; the spec-watch tick calls it (tracks.json → specWatch.landScript). Exit codes:
0 landed, 1 not landed (reason on one line), 2 rebase conflict outside handoff.md. The
contract: ~/.claude/docs/working-modes.md → "Let a tick land tracks".
flowchart TD
start["tip and spec sha match<br/>what was reviewed?"] -- no --> refuse["refuse"]
start -- yes --> scope["diff inside the spec's touches,<br/>no protected file?"]
scope -- no --> refuse
scope -- yes --> rebase["rebase onto main<br/>(handoff.md → main's roster)"]
rebase --> gate["verify + landChecks<br/>(tests, 227 floor, per-function floors)"]
gate -- fail --> refuse
gate -- pass --> merge["fast-forward main<br/>(retry if main moved, max 3)"]
merge --> after["tag spec/name/sha, remove worktree<br/>and branch, update roster"]
- Protected files. A diff touching
bench/,tracks.json,.github/,specs/, or at any depth.cargo/,rust-toolchain*,.env*,.gitattributes,.gitmodulesor*.floor.json(a generated function's score floor) is refused whatevertouchessays, since those change the gate itself. So is a spec whosetouchesmatches everything or reaches those paths. Case-insensitive, no rename detection. Such a track waits for a person's/land. - What the gate still trusts. The track's own crates, tests,
build.rs,oracle/andCargo.toml/Cargo.lock. The gate catches regressions and honest mistakes, not a track built to fool it. - The checks.
verify(bench/verify.sh: the tests, then every generated function scored against its own corpus, each at or above its floor in<name>.floor.json), thenlandChecks:bench/fragment-floor.sh(fragment_check --randommust pass 227), which runs throughbench/on-lane.sh(--lane-only, likeverify, whenNEV_LANE_STRICT=1; see "Build lane"). The four hand-written floors thatbench/seam-floor.shused to hold (floats, stacking order, relative positioning, block height) are floor files like the rest, and that script and its checker are gone. Floors are raised after a landing, never lowered by a track: a diff that touches a*.floor.jsonis refused. - Safety. Pinned SHAs re-checked right before a
--ff-onlymerge; a lock on the git directory; signals kill the gate's process tree and restore the branch. Aland-pending/<name>marker makes a rerun finish anything owed after a merge. Nothing is pushed. handoff.md. A landing restoresmain's roster on the branch before the gate, so a track's working note never overwrites the roster.- Install after a landing. If
oracle/package*.jsonchanged or a crate was added, onlybench/oracle-install.shruns, staged and swapped in on success. - Testing.
bench/land-test.shdrives the script through about 130 assertions in a throwaway clone.bench/land.sh --check ...runs every condition and stops before the merge.
Training limits #
Full fine-tuning is out of reach on 24 GB Ampere cards; LoRA or QLoRA on a 30B-class model across eight GPUs is feasible, so any supervised step is a LoRA.
Sharding without a broker #
Case generation is deterministic from a seed, so worker i of n takes seeds
i, i+n, i+2n, …: no queue, no coordinator, and any worker can restart without losing or
duplicating work. Output is content-addressed and written once; ground truth is immutable
per engine version, so there is nothing to invalidate.
Storage #
About 10 KB per document for three engines, so ten million cases is about 100 GB. The style subset dominates and is redundant; compress it, sample it, or store it as a delta.
Virtual machine template #
One image, duplicated: WebKit's 188 system packages, pinned Playwright browsers, Rust with sccache, the repository. Workers are stateless. Pin one VM per NUMA node rather than one large VM per host.
Engine versions pin the ground truth #
Ground truth is immutable only for a given set of browser versions, so every dump records
engine_version (docs/dump-format.md → "Dump") and a corpus is keyed by that version
set. A browser update invalidates only what it touches, never silently.
Rig change log #
Every change tried on the rig's hardware, worked or not, newest last. Hosts by role and
class only. Before a change, and again after each change that works,
bench/rig-snapshot.sh snapshot <label> records the serving configs of every role (proxy
host HAProxy, gateway container config and key database, each ARM node's units and
TabbyAPI configs, the four-card VM's Proxmox config and units) on the host itself and in
the local rig notes; restore <label> puts them back. The label named in an entry holds
the working state after it. Snapshots contain the gateway's master key and never go into
git or onto the NAS.
2026-09-24
- Gateway key for self-hosted writers. A new virtual key on the gateway, alias
nev-folknet-writer, allowed onlyqwen3.8-27b-longandqwen3.8-27b-fast, so writer traffic is accounted apart from batch traffic and cannot reach any other alias. No serving config changed. The key itself lives outside every repository, asFOLKNET_WRITER_API_KEYin$HOME/.config/folknet-writer/env(mode 600) on the development box. First used by stage 0 ofdocs/folknet-writer-design.md(docs/writer-harness-spike.md→ "Rerun through Pi"). Worked. - Snapshot and restore.
bench/rig-snapshot.shwritten;known-good-2026-09-24taken before any serving change.restore known-good-2026-09-24 haproxyrun once as the round-trip test: config check passed, HAProxy reloaded, all four ARM replicas still up. Worked. - Four-card VM. A new VM on a Proxmox node, given the four 3090s of a stopped tenant
test VM (same
hostpcientries, q35, OVMF, 256 GB 64-bit PCI hole), 32 cores, 160 GB RAM, a 120 GB OS disk, DHCP on the main LAN (bench/gpu-vm/create-vm.sh). The tenant VM's disks were not touched. Worked; four 3090s visible. - Provisioning, first two attempts failed. First: the fresh cloud image was still
running cloud-init's package upgrade, which held the dpkg lock and then rebooted the
guest. Second: the rig's apt cache answers
403 CONNECT deniedfor HTTPS repositories, so docker's and NVIDIA's apt repositories could not be reached through it. Fixed inbench/gpu-vm/provision.sh: wait for cloud-init and the dpkg lock, send HTTPS repositories direct, take docker from the Ubuntu archive (docker.io, which the cache serves). Third run worked: driver 595 (open kernel modules) loaded without a reboot,nvidia-smishows four 3090s, docker has the NVIDIA runtime. - Model choice for the four cards, by size (no download attempted).
qwen3.8-flash-next, EXL3 4.05 bpw (the ARM nodes' checkpoint): 101 GB on disk, of which 39 GB is the hashed n-gram table that exllamav3 keeps in host RAM or on disk, so about 62 GB of weights would sit on the cards. That the checkpoint does not fit 96 GB was assumed, never tested. A 3.05 bpw branch is also published (85 GB on disk, 32.6 GB of it n-gram table). Not pursued: the target moved to a different model family for variety.- DeepSeek-V4-Flash (the July release, 284B parameters, 13B active): 167 GB native (MXFP4 experts). Mainline llama.cpp with CUDA serves it on sm86 from GGUF. The smallest quant its quantizer recommends (3-bit, 104 GB) does not fit 96 GB of VRAM; 2-bit quants (87–91 GB) fit with only a few GB left for cache and compute buffers, and re-quantizing the native 4-bit experts to 2 bits costs a lot of accuracy (published KL divergence around 0.4 at 2 bits against 0.01 at 4 bits). Download straight from Hugging Face inside the VM measured about 250 KB/s. Not tried.
- DeepSeek-V4.1-Flash is a different, larger model, not a revision of the one above: a
new architecture (
deepseek_v41, 384 experts over 40 layers, n-gram memory tables), 510 GB native. It does not fit the four cards. The first reading, that it fits no pair of ARM nodes either, was wrong: it looked only at GGUF quants (smallest about 260 GB) and missed a published EXL3 recipe for exactly two GB10s — a 2.9 bpw EXL3 checkpoint (~197 GiB, about 99.5 GiB of weights per node at tensor-parallel 2) plus the model's unquantized n-gram ("Engram") tables, which stay on disk and are read per lookup rather than held in memory (two native shards, ~190 GiB), served by a vLLM build that carries thedeepseek_v41architecture. That recipe is what the ARM pair uses (entries below). - qwen3.8-27b, W4A16 AutoRound with DFlash2 speculative decoding, the eight-card
host's
-fastrecipe: about 22 GB per card, one worker per card. Chosen.
- qwen3.8-27b on the four cards. The eight-card host's image and prepared checkpoint
(24 GB) were streamed to the VM read-only (
taranddocker savepiped over ssh, nothing on that host changed). One worker per card came up in about 6.5 minutes from cold; all four report healthy and answer chat completions. Worked; snapshotvm-qwen27b-workers-up. - Proxy and gateway for the VM. A new HAProxy frontend and backend on the proxy host
(the existing backends untouched; backup
haproxy.cfg.pre-vm-qwen27b-20260924beside the config) and a gateway aliasqwen3.8-27b-fast-2(backupconfig.yaml.pre-qwen27b-vm-20260924), granted to the project's key only. The first gateway attempt failed with "Missing credentials": an OpenAI-style entry needs someapi_keyeven when the upstream checks none;sk-none, as the flash-next entry uses, fixed it. Worked: the alias is listed and returns code through the gateway; snapshotvm-qwen27b-fast-2-alias. - Measurement batch at
qwen3.8-27b-fast-2on a build lane: one 32-candidate absolute-positioning batch, 2/32 compiled, best 74/108 (docs/model-first-cost.md, rowvm-batch 1). Stopping the loop after batch 1 withpkill -fon the shared lane also killed another track's run of the same binary in its own lane worktree (a mixed-model batch at 32 of 64 draws). On a shared lane, stop a run by the pid whose/proc/<pid>/cwdis this worktree's lane copy, never by name. - Checkpoint fetch for an ARM pair.
bench/sparks/hf-to-nas.sh(a runtime bundle and oneshot unit on the downloader host, reusing that host's multi-WAN scripts) started DeepSeek-V4-Flash (July release, 167 GB); stopped after 14 GB when the ARM target changed to DeepSeek-V4.1-Flash. The partial staging directory is left on the NAS. Then both parts of the V4.1 recipe, pinned by revision: the EXL3 2.9 bpw checkpoint (42 files, 210.7 GB) and the two Engram shards plus index and config of the native checkpoint (203.1 GB), both through the multi-WAN downloader at about 130 MB/s combined. Snapshot before:pre-deepseek-sparks. - Which two ARM nodes. LLDP shows each node's first 200G port on the switch and its second port cabled straight to one other node, in two cabled pairs, each crossing the old GLM pairs (which ran over the switch). The DeepSeek pair is one of the cabled pairs; the other cabled pair keeps flash-next. Its tensor-parallel traffic is set to run over the switch port with the RoCE settings the GLM pairs used, because the direct cable carries no addresses today; moving it onto the cable means addressing that port on both nodes (not done).
- Staging the pair (nothing serving yet). The recipe is cloned at a pinned commit on
the head node, its
.envwritten frombench/sparks/dsv41/env.template(weights from local disk, no Hugging Face download,AUTO_DOWNLOAD=0), an ssh alias for the worker added to the head's admin account (bench/sparks/dsv41/ssh-config-worker; previous config kept asconfig.pre-dsv41),/srv/dsv41created on both nodes, and the recipe image pulled on both (the2.9bpwtag, 22.5 GB; digest in the local rig notes). Both nodes still serve flash-next. Snapshotdsv41-staged. - Throughput sweep of
qwen3.8-27b-fast-2(bench/rig_throughput.py) on a build lane: failed twice before sending a request — the lane's Python has norequestsmodule, andpython3 -m venvfails there without thepython3.12-venvpackage. Not run. - Four-card VM reboot. All four workers healthy three minutes after
reboot, same DHCP address, HAProxy backends back UP without intervention. Worked. - Second measurement batch at
qwen3.8-27b-fast-2, after the prompt fixes, run withbench/absolute-mixed-batch.shon a build lane: 8/32 compiled, best 82/108, 233 s for the batch (docs/model-first-cost.md, rowvm-batch 2). The run ended by itself (GENERA_MAX_BATCHES=1); nothing needed stopping. No serving change. - Throughput sweep of
qwen3.8-27b-fast-2, second attempt, run from the dev box (which hasrequests) through the gateway, with the prompt generated on a build lane: worked, 819 output tok/s at 16 in flight (docs/rig-capacity.md→ "Measured — the four-card VM"). No serving change. - Abandoned DeepSeek-V4-Flash download removed from the downloader host: the 14 GB partial staging directory on the NAS, its oneshot unit and runtime bundle. Worked; the two V4.1 units and their published trees are untouched.
- DeepSeek-V4.1-Flash weights to local disk on the ARM pair. Both NAS downloads had
finished and published.
rsync -afrom the NAS mount to local NVMe on both nodes, the EXL3 checkpoint (39 shards, 197 GB) to each, the two Engram shards with index and config (190 GB) to the head's engram source directory and to the worker's engram directory. The worker gets no second engram tree: the recipe'sstart.shbuilds the slim engram set on the head and rsyncs it to the worker, and with the NAS mtimes kept, that rsync finds the shards already in place. About 100 MB/s per node, both done in about 55 minutes. Both nodes kept serving flash-next throughout, all backends UP after. Worked; no serving change, so no new snapshot (dsv41-stagedstill holds the serving state). - Two ARM nodes out of the flash-next pool. Snapshot before:
pre-dsv41-pair-move. Their two server lines were removed from the flash-next HAProxy backend (backuphaproxy.cfg.pre-dsv41-pair-20260924beside the config; config check, reload; the other two replicas UP), then their flash-next units disabled and stopped; unit, config and checkpoint stay on disk. Nothing was drawing from the alias at the time. The alias now takes 16 requests in flight (two replicas × 8), not 32. Worked; snapshotflashnext-two-replicas. - DeepSeek-V4.1-Flash on the pair. The published recipe image carries the recipe's
own build stamp from four commits before the pinned one, so the pinned
start.shwould have compiled the image on the node. The only image-side change in between is one Python patch, which thatstart.shbind-mounts into both containers anyway, so the unit runs the published image as-is (SKIP_BUILD=1, reason in the unit file). Unit installed on the head and started: preflight passed (memory headroom 117.6 GiB available per node against 111.5 needed, RoCE GID accepted), the weight sync to the worker found everything already in place and took 3 s, weights loaded and warmed up, API healthy about 7.5 minutes after the start; the README's17*19request returned323. The recipe's own warmup reports one sampler-kernel variant not pre-compiled ("may JIT mid-serve"); harmless so far. Worked; snapshotdsv41-pair-serving. - Proxy and gateway for the pair. A new HAProxy frontend and backend on the proxy
host, one server,
maxconn 2(backuphaproxy.cfg.pre-dsv41-backend-20260924), and a gateway aliasdeepseek-v4.1-flash→ the recipe's served model name,api_key: sk-none(backupconfig.yaml.pre-dsv41-20260924), granted to the project's key only. Worked: the alias is listed through the gateway and a genera-shaped chat completion (thinking off) returned a one-function Rust code block in 2.7 s with no reasoning incontent. Snapshotdsv41-alias-serving. - First measuring batch at
deepseek-v4.1-flashon a build lane, 8 candidates at 2 in flight: 4/8 compiled, best 82/108, 1,731 s (docs/model-first-cost.md, rowdsv-batch 1). The first launch failed before drawing anything: the lane's shared git mirror refused a ref update (cannot lock ref 'refs/heads/main') because another run had movedmainat the same moment; a plain retry worked. No serving change.
2026-09-25
- Dead GLM aliases removed from the gateway.
glm-5.3-flashandglm-5.3-flash-exl3were still listed by/v1/modelsthree days after their backends stopped, so a caller could pick one and get a 503 ("No server is available to handle this request"), as a job on another track did. Snapshot before:pre-glm-alias-removal. Both model entries deleted from the gateway config (backupconfig.yaml.pre-glm-removal-20260925beside it; no fallback named them) and the gateway service restarted, live again in 18 s. The two names were then dropped from the allowlists of the three virtual keys that carried them, each keeping its other models. Worked:/v1/modelsthrough the dev-box tunnel lists neither name; one-token completions return 200 forqwen3.8-27b-fast,deepseek-v4.1-flash,qwen3.8-27b-fast-2andqwen3.8-flash-next; a request forglm-5.3-flashis refused with 403. Snapshotglm-aliases-removed. The site file the gateway config is generated from still carries the GLM entries, so a regeneration would bring them back.