Files
panel/modules/7dtd/CLUSTER_PLAYBOOK.md
T
2026-07-14 23:25:11 -07:00

342 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 7 Days to Die — Cluster Playbook
> The single operator + architecture doc for the Refuge 7DTD cluster: how shared
> characters/inventory work, how new servers auto-fix themselves on create, the
> one rule you must never break, and exactly what we do at the 3.0 wipe.
>
> Deep "why" lives in `CANONICAL_NIM_FREEZE.md` (root cause) and the manual
> fallback in `nim-freeze/REMAP_RUNBOOK.md`. **This doc is the one to read first.**
---
## 1. The one thing to understand
7DTD stores everything — your inventory, the blocks in the world, the trees —
by a **numeric id**, not by name. The engine hands out those numbers automatically
when it boots, and the assignment **drifts** whenever the *build* changes. "Build"
= the exact set of mods + their load order + the RefugeBot DLL version + the engine
version. Nothing else matters.
* **Same build everywhere → same numbers everywhere.** Inventory transfers cleanly,
worlds load. (Proven: two cluster servers on the same build produce byte-identical
id tables.)
* **Different builds → different numbers → "morph".** A glass block saved as id 21895
on one server reads as a car hood on another that put a different block at 21895.
Characters can even reset to level 1.
Everything below — the canonical table, the stamp, the decoration remap, the
auto-provision — exists for **one** reason: to bridge servers/worlds that were
baked under *different* builds so they all agree on the numbers.
---
## 2. The golden rule (non-negotiable)
> **Every server in the cluster must run the byte-identical build at all times.**
Same mods, same load order, same RefugeBot build, same game version — on **all**
members, always. Break this and inventory morphs. The build *can* evolve over time,
but only as a **coordinated, all-servers-at-once maintenance event** (see §6), never
one server at a time.
---
## 3. The two moving parts
| Part | What it is | Who maintains it |
|---|---|---|
| **The canonical** | One frozen `name ↔ id` table (`blockmappings.nim` + `itemmappings.nim`) every server stamps on, so the **shared player inventory** (`.ttp`) decodes identically everywhere. Lives at `/cluster/canonical/` (host: `…/7dtdcluster/<id>/canonical/`). | `nim-freeze/rebuild.py` (re-anchor / extend) |
| **The decoration remap** | A world bakes its **decorations** (`decoration.7dt` / `multiblocks.7dt`) with the engine's *native* ids at generation. Those must be rewritten to the canonical's ids or the world NREs on load. | `nim-freeze/remap_7dt.py`, now **automatic** (see §4) |
Why both: the canonical fixes **inventory**, the remap fixes **the world the
decorations live in**. The deep mechanics are in `CANONICAL_NIM_FREEZE.md`.
---
## 4. How it works now — auto-provision on create (the steady state)
You don't run any of the remap tooling by hand anymore. The game container's
entrypoint + the agent do it for you. A brand-new clustered world becomes
canon-compatible **on its own**, in two boots:
1. **Gen boot.** You create the server (cluster picked), Update (downloads the
game), and Start. The engine generates the world and bakes its decorations
with native ids. The world is up but *not yet aligned* to the cluster.
2. **Align boot (automatic).** The agent's auto-provision watcher notices the
fresh world's decorations are in the native band and **bounces the container
once**. On that boot, the entrypoint remaps `decoration.7dt` + `multiblocks.7dt`
native → canonical (using the world's own native table), drops a
`.canon-provisioned` marker, and stamps the canonical. The world comes back
**clean, on canon, joinable, transfer-safe.**
**What you see (visibility):** in the server's **Console**
`[panel] auto-provision: aligning the new world's decorations to the cluster…`
then `remapped decoration.7dt native->canonical` then
`decorations aligned to cluster canonical — world is now canon-compatible` then
`StartGame done`. The card briefly shows **"aligning decorations to cluster…"**.
**It is a strict no-op for everything else:** existing canon worlds
(season10/creative/insane/pvp) and non-cluster servers never enter the remap
branch (their decorations are already in the canon band), so recreating/restarting
them does nothing. The remap is **stable** — once a world is canon, it stays canon
across every save and reboot (the engine preserves loaded decoration ids; it only
writes native ids at *generation*).
**The backstop:** even if the auto-bounce is ever missed, the entrypoint aligns the
world on its *next* ordinary restart. The auto-bounce just makes it zero-touch.
### Files that implement it (for the next engineer)
* `modules/7dtd/entrypoint.sh` — the remap block (right before the canonical-stamp
guard) + the `.panel-save-base` writer. Gated on the **decoration band** only.
* `modules/7dtd/Dockerfile` — adds `python3-minimal` + the vendored remapper.
* `modules/7dtd/nim-freeze/remap_7dt.vendored.py` — self-contained in-container remapper.
* `agent/internal/dispatch/decoremap_provision.go` — the watcher (single-flight,
probes the running container, bounces once on a native-band world).
---
## 5. Adding a new server (operator steps)
1. **Add server** in the panel, module **7 Days to Die**, and **pick the cluster**
in the create form. (This wires the shared Player folder + the canonical mount.)
2. **Update** (installs the game) → **Start**. Let it generate the world.
3. **RWG worlds only:** once the world has generated, set the **"Shared player world"**
in the **Cluster tab** to the new (seed-named) world. The picker writes
`CLUSTER_PLAYER_SAVE` and triggers a recreate, so the entrypoint stamps + aligns the
right dir. **Also point Region Medic at the same `<World>/<GameName>`** (Cluster tab →
Region Medic card), or Medic keeps healing the old, now-deleted world. Until both are
set, alignment can't target the world.
4. That's it. The world auto-aligns to the cluster (watch the Console). When you see
`StartGame done` after the alignment bounce, it's joinable and transfer-safe.
> **Is the new world done? Don't let players join until all three pass:**
> 1. Console shows `StartGame done` *after* the `auto-provision: aligning…` line.
> 2. Save dir has a `.canon-provisioned` file.
> 3. Decorations read in the **24xxx** band — verify it yourself (read-only):
> ```sh
> docker run --rm -v panel-<inst>-saves:/sv:ro debian:12-slim sh -c \
> 'f="/sv/.local/share/7DaysToDie/Saves/<World>/<GameName>/decoration.7dt"; \
> echo "first deco id = $(( $(od -An -tu2 -j17 -N2 "$f") & 0x7FFF ))"'
> ```
> `>= 24000` = canon (good). `< 24000` = still native (NOT aligned — do not let players in).
>
> **If after 2 boots there's still no `.canon-provisioned` / the band is native** (the
> §12 #3 case): do **one** more panel **Restart** — the entrypoint backstop aligns it on
> that boot — and re-check. Still native after that → manual repair via
> `nim-freeze/REMAP_RUNBOOK.md`.
---
## 6. Changing mods (a maintenance event, not a casual edit)
Because of the golden rule, a mod change is a **whole-cluster** operation:
1. Update **every** server's mods together (same set, same order, same RefugeBot
build). Never one server at a time.
2. **Rebuild the canonical** so it contains the new names (else they append per-world
again → morph). This is **two** steps — ⚠️ **`rebuild.py` does NOT discover new mod
content; it only re-anchors an existing name universe.** Doing 2b without 2a silently
produces an incomplete canonical and re-introduces the exact wipe bug this system
prevents.
* **2a — Re-harvest the complete name universe** (the hard part). Blocks = the
`<base>:<shape>` cross-product (`shapes.xml` × the 10 shape-helper bases) the
engine's authoritative block dump (telnet `exportcurrentconfigs`, or the runtime
`Block` list) every named block; items = every `<item name=>` every
`<item_modifier name=>`. The original harvest ran as two ultracode workflows; its
inputs/outputs live on figaro at `~/nimcanon/registry/` but **the generator is not
committed** — exact greps/commands are in `CANONICAL_NIM_FREEZE.md` → "After a mod
change". **Do not attempt a mod change without re-running this harvest.**
* **2b — Re-anchor** against season10's CURRENT, STOPPED `.nim` (the
`West Apeeni Mountains/MyGame` anchor — verify 38,626/2,719+ first, §13) with the new
universe: `nim-freeze/rebuild.py` preserves every existing id and appends new names
at fresh ids (so old worlds + the ~88 shared characters still decode). Verify
`GATE PASS: True`, then copy into `/cluster/canonical/` (chmod 644).
3. **Re-stamp every member** so they pick up the new canonical — a panel **Restart**
is enough (the entrypoint re-stamps from `/cluster/canonical` on every boot; season10
too, by Restart only — never Rebuild). New worlds genned after the change auto-align
(§4); existing worlds keep working (their content + the canonical's existing ids are
unchanged).
Adding content is safe (append). Removing/reordering content is where ids shift —
that's exactly what a **wipe** (§7) resets cleanly.
---
## 7. The 3.0 wipe roadmap (the easy button)
A version wipe is the moment to make the whole thing **bulletproof and remap-free**,
because you're discarding all the old worlds anyway. Plan:
1. **Freeze + hash the 3.0 build.** Lock the exact mod set + load order + RefugeBot DLL +
engine version, and record a hash of the artifact set (e.g. `tar c Mods/ | sha256sum`)
into a committed `3.0-build.lock`. That snapshot *is* the contract going forward.
2. **Wipe all worlds — through the panel, never docker CLI.** Per server: delete the old
`Saves/<World>/<GameName>` dirs (mount the saves volume, per the runbook's volume-access
pattern) and set a fresh `world_seed` via `env-config`, or delete-preserve-volumes +
recreate. Characters too — `.ttp` is build-dependent; a wipe is a fresh start for
inventories (carrying them across a build change needs the `.7rg`/`.ttp` codec work in §8).
3. **Re-anchor the canonical to the LIVE 3.0 runtime.** "Capture the current runtime's
table" = the `REMAP_RUNBOOK.md` **"CAPTURE the world's native table"** sub-procedure: on
a fresh 3.0 world, Stop → move the canonical aside (back it up first!) → write the 8-byte
empty `.nim` → Start (the engine writes the world's authoritative native table) → copy it
out → restore the canonical. Feed that as the new anchor to `rebuild.py` together with a
freshly-harvested 3.0 universe (§6 step 2a). Now **`canonical == runtime`** and the gap
that forces per-world remapping is gone.
4. **From then on, fresh maps just work with zero remap** — every world is born from the
same starting numbers. The auto-provision/stamp machinery stays in place as a harmless
safety net; it simply has nothing to fix.
5. Only a *future* mod change re-introduces the need to rebuild the canonical (§6).
> ⚠️ **The 3.0 wipe is irreversible and depends on the (currently uncommitted) §6 harvest
> workflow.** Do NOT execute it from this strategy alone — when 3.0 lands, write a dedicated,
> tested 3.0 runbook with the exact harvest + capture + wipe commands first. The plan above
> is sound; the runnable commands are the gap to close before you pull the trigger.
Net: **today** = same-mods rule + auto-provision handles new servers; **at 3.0** =
clean re-anchor and new maps need no special handling at all. It's a ~one-evening job
when 3.0 lands; nothing to prep now.
The truly permanent fix that removes even the maintenance events: pin every block/item
to an explicit `id=` in a top-priority modlet so the runtime can never drift from the
canonical (untested on this engine version — a worthwhile experiment), or freeze the
RefugeBot build the canonical was anchored under.
---
## 8. Known limits / frontier
* **RWG needs one pick.** A random-gen world's save folder name is seed-derived and
unknown until after gen, so the operator sets "Shared player world" once (§5 step 3).
Auto-align kicks in after that.
* **Regions + player `.ttp` are not remapped.** The codecs for `Region/*.7rg`
(explored chunk blocks) and `Player/*.ttp` (inventory) weren't cracked, so alignment
is done on a **fresh, 0-region world before exploration** (which is exactly when a
new server provisions). The 87 shared characters are fine (already canon-written).
Solving these codecs (decompile `Assembly-CSharp.dll`) is what would let you migrate
characters across a build change instead of wiping them.
---
## 9. Quick reference
| Want to… | Do this |
|---|---|
| Add a server | §5 (auto-aligns; RWG needs the one Cluster-tab pick) |
| Confirm a world is aligned | save dir has `.canon-provisioned`; decorations in 24xxx band |
| Change mods | §6 (all servers together + rebuild canonical) |
| Wipe for 3.0 | §7 (freeze build → wipe → re-anchor canonical to live runtime) |
| Manually fix a stuck world | `nim-freeze/REMAP_RUNBOOK.md` |
| Understand the root cause | `CANONICAL_NIM_FREEZE.md` |
| Re-anchor / extend the canonical | `nim-freeze/rebuild.py` |
| Deploy controller / agent / module | §10 (+ the `panel-deploy` skill) |
| Recover a wedged agent (config fields won't load / 504s) | restart `panel-agent` on figaro — §10 / §11 |
| Known issues & open work | §12 (season10 console, the "setting up" indicator, probe-path) |
| What's deployed vs. uncommitted | §13 |
---
## 10. Operational architecture & deploy (for the next engineer)
**Where it runs**
| Piece | Host | Notes |
|---|---|---|
| **Controller** (dashboard + gRPC) | kaiten `<user>@<controller-host>` | binary `/home/refuge/panel/bin/controller`, unit `panel-controller.service`, HTTP `:8180`, gRPC `:8443` |
| **Agent** (owns the docker game containers) | figaro `<user>@<agent-host>` | binary `/home/refuge/panel/bin/agent`, unit `panel-agent.service`, dials the controller |
| **Cluster shared dir** (the `/cluster` bind) | figaro `…/panel/data/7dtdcluster/cl_37e3a7f72cdc/` | holds `canonical/` + `Player/`; bind-mounted into every member as `/cluster` |
| **Game containers** | figaro, image `panel-7dtd:latest` | named `panel-<instance>` (e.g. `panel-rg-season-10`) |
SSH everywhere with `~/.ssh/bubbly_ed25519`, user `refuge`. **The full deploy recipe is the `panel-deploy` skill — follow it.** The NOPASSWD sudoers is exact-match (single-service `systemctl` forms only).
**Panel access & conventions (read before driving anything)**
* **Dashboard:** `http://<controller-host>:8180` on the LAN (controller HTTP). Log in with your panel account. On kaiten itself the `http://127.0.0.1:8180/api/...` routes are unauthenticated — that's what the read-only diagnostic `curl`s in this doc use.
* **Server name → ids:** an instance's id is its slug. "Season 10" ⇒ instance id `rg-season-10` ⇒ container `panel-rg-season-10` ⇒ saves volume `panel-rg-season-10-saves`. List them all (with status) via `GET /api/instances`. Full live table in §13.
* **UI = API:** the `POST /api/instances/<id>/{env-config,start,stop,rebuild}` calls in these docs are exactly what the dashboard's Cluster-tab / Maintenance buttons fire. Use either — just target the right `<id>`.
* **"Stop / Start / Restart" always means the PANEL action** (agent-owned, clean shutdown). **NEVER `docker stop/start` a `panel-*` container** — the agent owns lifecycle + state reconciliation. Confirm a container is stopped via the grey card, or read-only `docker inspect -f '{{.State.Running}}' panel-<id>``false`. (Read-only `docker exec/logs/inspect` and `docker run` sidecars are fine.)
* **"Restart" vs "Rebuild" (load-bearing):** Restart = panel Stop then Start, **same image** — safe for any server *including season10*. **Rebuild/Recreate = adopts `panel-7dtd:latest`** — needed to pick up a module/image change, but **forbidden for `rg-season-10`** (it must stay on its old image; §13).
* **New entrypoint env vars MUST be declared in `module.yaml` `env:`** or the agent silently drops them (symptom: the var is empty at runtime even though you set it). `CLUSTER_ID` + `CLUSTER_PLAYER_SAVE` are already declared — add any new `CLUSTER_*` / provisioning var there too.
**Three deployable pieces**
1. **Controller** (Go + the dashboard HTML in `controller/cmd/controller/static/{new,index}.html`) — `GOOS=linux GOARCH=amd64 go build -o bin/controller-linux ./controller/cmd/controller`, scp to kaiten `bin/controller.new`, bak + swap + `systemctl restart panel-controller.service`.
* ⚠️ **After every controller deploy, HARD-refresh the browser (Ctrl-Shift-R).** A normal refresh serves the *old* cached JS against the new backend and the page half-breaks (fields won't load, console blank). It looks exactly like a real outage but is just stale cache — rule this out first.
2. **Agent** (Go) — `… go build -o bin/agent-linux ./agent/cmd/agent`, scp to figaro, bak + swap + `systemctl restart panel-agent.service`.
* Restarting the agent does **NOT** stop the game containers — it re-attaches/rehydrates them cleanly (stats + rcon + log streams re-open). Safe anytime; it is also the **recovery** for a wedged agent (§11).
3. **Module** (`modules/7dtd/{entrypoint.sh,Dockerfile,nim-freeze/remap_7dt.vendored.py}`) — sync the module dir Windows → kaiten → figaro, then build the image **on figaro** (where the containers run; kaiten holds the source-of-truth copy but builds nothing):
```sh
# from the Windows repo C:\Users\dbled\sources\panel :
scp -r modules/7dtd/ <user>@<controller-host>:/home/refuge/panel/modules/
ssh <user>@<controller-host> 'rsync -av /home/refuge/panel/modules/7dtd/ <user>@<agent-host>:/home/refuge/panel/modules/7dtd/'
ssh <user>@<agent-host> 'cd /home/refuge/panel/modules/7dtd && docker build -t panel-7dtd:latest .'
```
Existing containers **pin to the old image ID** — a container only adopts the new image via **Settings → Maintenance → Rebuild** (or delete-preserve-volumes + recreate).
* ⚠️ **Never Rebuild/recreate `rg-season-10`.** It is the live released server *and* the canonical anchor; it must stay on its current (old) image. `.Config.Image` shows the name `:latest` for every container — verify by image **ID**, not name.
**Do NOT `docker stop/start` a `panel-*` game container from the CLI** — let the panel do it (the agent owns lifecycle + state reconciliation). Read-only `docker exec`, `docker logs`, `docker inspect`, and `docker run` sidecars are fine.
---
## 11. Incident log & landmines
### 2026-06-13 — agent-wide hang (ExecCapture connection leak) ✅ FIXED + HARDENED
* **Symptom:** config fields wouldn't load on **any** server (the durable `serverconfig.xml` editor → `/api/instances/{id}/files/read` returned **HTTP 504 "timeout waiting for agent response"**); one server's console went blank; the agent was **idle (0% CPU) but unresponsive to file ops**; no panic.
* **Root cause:** the auto-provision watcher (`agent/internal/dispatch/decoremap_provision.go`) polls a running container with a docker exec (`DockerRuntime.ExecCapture`) every 20s. `ExecCapture` ran `stdcopy.StdCopy(…att.Reader)` on the hijacked exec stream **with no `ctx` enforcement on the Read**. When it caught the just-bounced `cluster-seed-test` mid-restart, the read blocked **forever** and leaked a docker SDK connection each cycle → the SDK client's connection pool exhausted → **every** later docker op (`CopyFileFromContainer` for config reads, a log-stream re-attach) hung. It is **not** a Go-mutex deadlock (the only mutex in `docker.go` — `hijackedStream.mu` — just guards the stdio-attach `Close()` and is unrelated); it's connection-pool starvation.
* **Recover:** `ssh <user>@<agent-host> 'sudo -n /usr/bin/systemctl restart panel-agent.service'` — clears the leaked connections; game containers keep running. **Confirm recovery:** re-open any server's Config tab (fields load within a few seconds), or on kaiten `curl -s -o /dev/null -w '%{http_code}\n' 'http://127.0.0.1:8180/api/instances/rg-season-10/files/read?path=/game-saves/serverconfig.xml'` → expect **200**, not 504.
* **Permanent fix (shipped):** `ExecCapture` now runs `StdCopy` on a goroutine and `select`s on `ctx.Done()`; on timeout it `att.Close()`s to unblock the read, then drains the goroutine before reading the buffers (race-free). On the timeout path it returns the partial stdout/stderr with exit code `-1` and `exec copy: <ctx err>`, so callers can tell a wedged exec from a clean one. An exec can no longer hang the whole agent.
* **Landmine for the future:** *any* agent docker exec/stream read that ignores `ctx` can leak connections and wedge the entire agent the same way. If you add exec-based features, bound the read by `ctx`. (The log-stream demux at `docker.go:271` is a long-lived `Follow` stream — a different model, closes on the docker `rc`'s EOF/ctx — and is fine.)
* **Recognize it next time:** `files/read` 504s for *every* server + agent at 0% CPU + no crash ⇒ restart the agent first, then hunt for a new unbounded docker read.
---
## 12. Known issues & open work
1. **season10's panel console is blank** (separate from §11; pre-existing). season10's docker json-log has grown to **13 MB** and it is actively spamming exceptions: `No ItemClass entry for type 678xx` (×611) + `Log:Exception` / `ThreadManager:UpdateMainThreadTasks`. Docker's own `--follow --tail 400` serves season10 instantly and the other four servers stream fine, but the agent's SDK log-follow **stalls on season10's stream before forwarding a single line** (blocked-inside, not thrashing — no "log stream ended" retries). Re-attaching (two agent restarts) did **not** fix it. It will likely clear on a **season10 container restart** — a plain panel **Stop → Start**, which keeps season10 on its current image and is safe; this is **not** a Rebuild/recreate (that would adopt `panel-7dtd:latest` and break the canonical anchor — forbidden, see §10/§13). It may re-blank once the exception spam refills the log. **Durable fix:** resolve the `No ItemClass entry` item-config gap (kills the console stall, the log bloat, *and* the exceptions at once), **or** harden the agent log-follow — mechanism not fully pinned (the agent has no pprof endpoint, so no clean goroutine dump was possible). **Do NOT recreate season10 to fix this** (old-image constraint, §10).
2. **The "setting up" provisioning indicator doesn't surface.** The agent watcher emits `detail="Finishing cluster setup…"` and the dashboard renders a matching amber "setting up" pill — but the agent's own **state-reconciliation loop emits `detail="announced:running"` a beat later and overwrites it**, so the card flips green prematurely. The **Console line** (`auto-provision: aligning the new world's decorations to the cluster…`) *does* show and survives a refresh — that part works. Proper fix: a **dedicated provisioning state field** the reconciler respects, not the shared `detail` string. (The card-pill matcher is live but dormant in `new.html`/`index.html`; harmless.)
3. **Auto-provision watcher probe-path caveat (zero-touch isn't universal yet).** The watcher gates "gen finished" on `main.ttw` **only**; it reads `decoration.7dt` solely to detect the already-canon (≥24000) case. A *missing* `decoration.7dt` still yields `NEEDS_ALIGN` and a bounce — the graceful-stop save writes the native deco, which the entrypoint then remaps on the align boot. For some RWG worlds (e.g. Tuxeno on `cluster-seed-test`/pvp) that two-step doesn't always *complete* zero-touch, but the **entrypoint backstop still aligns the world on its next ordinary restart**, so it ends up correct. Worlds that bake deco at the save path at gen (e.g. refuge-mall) provision fully zero-touch. **Treat the entrypoint remap as the reliable path; the watcher bounce is the optimization.**
4. **`No ItemClass entry for type 678xx` on season10** (root of #1). A set of high-band item ids (mod / RefugeBot runtime items) the merged config references but doesn't define. Tracing the missing items cleans up the console stall, the 13 MB log bloat, and the on-server exceptions in one shot. Not urgent — season10 runs fine (20 FPS, players on).
---
## 13. Handoff status — what's deployed, what's uncommitted
**Cluster:** `cl_37e3a7f72cdc`. Shared `Player/` ≈ 88 `.ttp`. Live members:
| Server | Instance id | Container / saves volume | Active world (`CLUSTER_PLAYER_SAVE`) | Role |
|---|---|---|---|---|
| Season 10 | `rg-season-10` | `panel-rg-season-10` / `panel-rg-season-10-saves` | `West Apeeni Mountains/MyGame` *(canonical anchor)* | **master / anchor — DO NOT Rebuild** |
| Creative | `creative` | `panel-creative` / `panel-creative-saves` | `Tefasizi County/Refuge` | creative |
| Insane Survival | `insane-survival` | `panel-insane-survival` / `panel-insane-survival-saves` | `Yixacove Valley/Refuge` | survival |
| PvP | `cluster-seed-test` | `panel-cluster-seed-test` / `panel-cluster-seed-test-saves` | `Tuxeno Territory/RefugeTHREEPVP` | pvp |
| Mall | `refuge-mall` | `panel-refuge-mall` / `panel-refuge-mall-saves` | `New Vebape Territory/RefugeMall` | mall |
> **Canonical anchor — the single highest-blast-radius fact.** The complete frozen table (**38,626 blocks / 2,719 items**) lives ONLY at season10's `…/Saves/West Apeeni Mountains/MyGame/{block,item}mappings.nim`. season10's *other* save dirs are stale partials (e.g. `Navezgane/MyGame` = 3,356 blocks, `Iupu Territory/Troutdale` = 5,028) and its `CLUSTER_PLAYER_SAVE` env is **empty** (old image, predates the picker — so don't trust the env to find the anchor). Any re-anchor (§6 / §7) MUST copy from exactly `West Apeeni Mountains/MyGame` and **verify the counts read 38,626 / 2,719 (or higher — never lower)** before running `rebuild.py`. Anchoring against a partial silently wipes the ~88 shared characters.
**Deployed (live):**
* **Controller** on kaiten — has the create-form cluster note + the auto-provision "starting" chip + the (dormant) "setting up" card-pill code.
* **Agent** on figaro — has the auto-provision watcher **and** the hardened `ExecCapture` (§11).
* **Image `panel-7dtd:latest`** on figaro — has the entrypoint remap block + `python3-minimal` + the vendored `remap_7dt.py`. Running on creative / insane-survival / cluster-seed-test / refuge-mall.
* **`rg-season-10` deliberately on the OLD image** (no `.panel-save-base`, no remap block) — the live anchor; do not move it.
* **refuge-mall** — created via the panel this session and registered as a RefugeBot tenant (prod DB row Id=31 + `refugebot.json` in its saves volume), connected.
**⚠️ UNCOMMITTED — nothing from this work is in git.** `C:\Users\dbled\sources\panel` is a git working tree but all of the below is uncommitted: the controller dashboard edits, the agent Go (`decoremap_provision.go`, the `dispatch.go` hook + `provisionGuard`, the `ExecCapture` hardening), the module (`entrypoint.sh` remap block, `Dockerfile`, `nim-freeze/remap_7dt.vendored.py`), and these docs. **Rollback points** if a deploy goes bad: `controller.bak-<ts>` on kaiten, `agent.bak-<ts>` on figaro (newest = the binary just before the current one).
**If/when you commit,** suggested grouping: (1) canonical + deco freeze/remap, (2) auto-provision (entrypoint + watcher), (3) the `ExecCapture` hardening, (4) docs. Note in the message that the cluster is **live** so a reviewer treats it as load-bearing.
---
## 14. Disaster recovery — the three irreplaceable trees
These three live on figaro and **cannot be regenerated from the repo** — they *are* the source of truth. If figaro's disk dies, this is what hurts:
| Tree | Path on figaro | What it is | If lost |
|---|---|---|---|
| **The canonical** | `…/panel/data/7dtdcluster/cl_37e3a7f72cdc/canonical/{block,item}mappings.nim` | the frozen stamp source every member uses | rebuildable from season10's `West Apeeni Mountains/MyGame` anchor via `rebuild.py` (needs `~/nimcanon`) |
| **Shared characters** | `…/panel/data/7dtdcluster/cl_37e3a7f72cdc/Player/` (= `/cluster/Player`) | the ~88 player `.ttp` (every character + inventory) | **unrecoverable** — everyone resets to level 1 |
| **The rebuild toolchain** | `~/nimcanon/` (`tool/`, `registry/`, `canonical_final/`, `remap/`) | the only parser + name registry + capture tooling to rebuild any of the above | must re-harvest from scratch (the §6 harvest — currently uncommitted) |
> ⚠️ **Open risk — confirm these are actually backed up off-figaro.** `~/nimcanon` is persisted out of `/tmp` (survives a reboot) but a disk loss is unaddressed in these docs. **Recommendation:** snapshot all three trees to another host on a schedule (reuse the panel's backup system or a periodic `tar`). Restore order: canonical → Player → validate with an in-game transfer (CANONICAL_NIM_FREEZE "Verify a server is frozen"). Treat this as the **top open risk** until a backup is confirmed.