Issue #49 removed game assets from git. It also, silently, removed the only transport agents had for showing each other evidence: each agent works in its OWN clone, so "commit the screenshot, the other one pulls it" was the mechanism, and PROTOCOL.md's file table said so in as many words -- "evidence cited by a finding -> git". That row sat directly above "🔴 Never commit game content", which is how 76 MB accumulated: the two contradicted each other and the table won, because it was the one that told you what to do. WHAT REPLACES IT * one host directory, `Sylpheed/docs/re/captures/`, bind-mounted read-write into BOTH agents at /work/docs/re/captures. All three -- host, decoder, port -- see the same files live, every citation resolves, and nothing can reach git history. Read-write on purpose: showing each other a screenshot is the point. * PROTOCOL.md's table rewritten. Cited evidence -> present but never committed; derived measurements (csv/tsv/txt/log/json) -> still git, they are our numbers not game content; evidence that must cross MACHINES -> attached to the issue or PR, because a bare clone has no captures. Verified, not assumed: container A wrote a .png there, a SEPARATE container B read it back, the host saw it, `git status` reported 0 changes, and `git check-ignore` named the rule. THE CHECKER WAS RED ON EVERY CLEAN CHECKOUT A fresh clone/worktree/CI has no captures, so it called all 134 citations dangling and exited 1. A gate that is red before anyone changes anything is one people learn to ignore -- the exact failure this file already carries a comment about. It now distinguishes "no captures here" (expected, explains itself, exit 0) from "these are missing" (real, exit 1, unchanged when assets ARE present). Both paths tested. ALSO * `sylph-decoder` no longer mounts `xenia-rs` -- retired repo, gone from disk, the mount pointed at nothing. * CONSOLIDATION.md closed: it still described captures as committed and the history fork as undecided. Both are settled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
194 lines
9.1 KiB
Markdown
194 lines
9.1 KiB
Markdown
# Containers and agents — how this is actually set up
|
|
|
|
**Written 2026-09-13 for the other machine.** Everything below was read off the
|
|
running host, not remembered. Paths are as they exist on the desktop; the Pi
|
|
differs where it says so.
|
|
|
|
## 1. The rule that governs all of it
|
|
|
|
🔴 **Every heavy command runs in a capped container. No exceptions, including
|
|
one-off checks.**
|
|
|
|
```bash
|
|
docker/ci/run cargo check --workspace --all-targets
|
|
```
|
|
|
|
A bare `cargo build` on the host is **unbounded**. `CARGO_BUILD_JOBS` caps
|
|
codegen units — not rustc's own threads, not the linker, not the test harness.
|
|
A full-parallel build has OOM-crashed this box, and unbounded host runs have
|
|
frozen it repeatedly since. `--memory-swap` equal to `--memory` means a build
|
|
that *would* swap is killed instead: a fast failure beats an hour of thrashing
|
|
that takes the desktop with it.
|
|
|
|
What may run on the host: `git`, `grep`, `python3` over text, API calls. Anything
|
|
that compiles, links, transcodes or boots an emulator does not.
|
|
|
|
## 2. Three kinds of container
|
|
|
|
| | image | what it is |
|
|
|---|---|---|
|
|
| **CI / build** | `sylph-ci:local` | the runner, reproduced locally — `docker/ci/Dockerfile` |
|
|
| **Decoder agent** | `sylpheed-agent:latest` | an autonomous Claude Code session doing RE — `docker/decoder/` |
|
|
| **Port agent** | `sylpheed-port:latest` | the same, building the Godot port — `docker/port/` |
|
|
|
|
### 2.1 The CI image
|
|
|
|
`docker/ci/Dockerfile` — `rust:1.98.1-bookworm`, **the apt list copied verbatim
|
|
from `.github/workflows/ci.yml`**, plus `clippy`, `rustfmt` and the
|
|
`wasm32-unknown-unknown` target.
|
|
|
|
```bash
|
|
docker build -t sylph-ci:local docker/ci
|
|
docker build -t sylph-ci:ffmpeg -f docker/ci/Dockerfile.ffmpeg docker/ci # exporter only
|
|
```
|
|
|
|
⚠️ **The Rust version is pinned here and floats on the runner**
|
|
(`dtolnay/rust-toolchain@stable`). That is issue #15, and it is not theoretical:
|
|
`collapsible_else_if` is `warn` on 1.92 and `allow` on 1.98.1. When a local
|
|
clippy pass and a runner pass disagree, **the runner is the authority.**
|
|
|
|
⚠️ **`cargo clippy` stops at the first failing compilation unit.** Without
|
|
`--keep-going` the list looks short and is not. On the corpus branch the honest
|
|
count was **80**, and the first run showed **14**.
|
|
|
|
`docker/ci/run` gives 6 CPUs / 7 GB / no swap, mounts the repo at `/work`, the
|
|
extracted disc read-only at `/disc`, and uses **named volumes** for `CARGO_HOME`
|
|
and `CARGO_TARGET_DIR` — never the host `target/`, which still holds 32 GB from
|
|
older host-side builds and produces rebuilds that look like cache misses.
|
|
|
|
### 2.2 The agent containers
|
|
|
|
Launched from the host by `docker/decoder/sylph-decoder` and
|
|
`docker/port/sylph-port`. Each starts a Claude Code session inside a container
|
|
with the repo, the disc, Canary's source and a warm build tree.
|
|
|
|
**The budget is split deliberately, not halved per container** — there are two
|
|
agents and a Referee is planned, so each claiming half a box it shares would
|
|
oversubscribe it:
|
|
|
|
| | CPUs | memory | `/dev/shm` |
|
|
|---|---|---|---|
|
|
| decoder | 5 | 6 GB | 2 GB |
|
|
| port | 3 | 4 GB | — |
|
|
|
|
Overridable with `SYLPH_CPUS` / `SYLPH_MEM_GB` and `SYLPH_PORT_CPUS` /
|
|
`SYLPH_PORT_MEM_GB`. `/dev/shm` is an **exec-capable tmpfs**, not `--shm-size`:
|
|
Docker mounts the default `noexec` and Xenia maps its JIT code cache out of a
|
|
shm file. Its pages count against the memory cap, hence a third of it and no
|
|
more.
|
|
|
|
**Volumes** (`docker volume ls`):
|
|
|
|
```
|
|
sylpheed-decoder-repo sylpheed-port-repo the agent's own clone
|
|
sylpheed-decoder-claude sylpheed-port-claude Claude Code state + transcripts
|
|
sylph-agent-cargo sylpheed-port-cargo CARGO_HOME
|
|
sylph-agent-target sylpheed-port-target CARGO_TARGET_DIR
|
|
sylph-agent-canary-build the warm 235 MB Canary build tree
|
|
sylpheed-exchange agent -> agent files, read-only in
|
|
```
|
|
|
|
**Plus one bind mount, which is not a volume on purpose** — the host's
|
|
`Sylpheed/docs/re/captures/`, mounted read-write into both agents at
|
|
`/work/docs/re/captures`:
|
|
|
|
```
|
|
-v "${SYLPH_CAPTURES:-<workspace>/Sylpheed/docs/re/captures}:/work/docs/re/captures"
|
|
```
|
|
|
|
🔴 **Why it has to exist.** Each agent works in its OWN clone, so before issue
|
|
#49 the transport for evidence was *git*: commit the screenshot, the other agent
|
|
pulls it. #49 removed that — captures are gitignored now — and without a
|
|
replacement a capture written in one container is invisible to the other agent,
|
|
to the human, and to `check-capture-citations`, which would call every citation
|
|
dangling. One host directory shared by all three restores it: the same file is
|
|
live everywhere, every `docs/re/captures/...` citation resolves, and nothing can
|
|
reach git history. Read-write on both, because showing each other a screenshot
|
|
is the point.
|
|
|
|
Verified rather than assumed: container A wrote a `.png` there, a **separate**
|
|
container B read it back, the host saw it, and `git status` reported **0**
|
|
changes.
|
|
|
|
⚠️ **Evidence that must cross MACHINES still cannot go this way** — a bare clone
|
|
on the other desktop has no captures at all. Attach it to the issue or PR; that
|
|
is the only channel that travels.
|
|
|
|
⚠️ **The agent volumes were all deleted in the 2026-09-18 cleanup** and Docker
|
|
recreates them empty on next launch. Nothing was lost — both agent clones were
|
|
verified clean with nothing unpushed, and the exchange held only spent artefacts.
|
|
One side effect is welcome: the empty `*-claude` volumes mean the next launch
|
|
starts a **fresh** session, which is the documented workaround for the
|
|
"resumes the old brief" defect below.
|
|
|
|
⚠️ **`sylph-decoder` used to mount `xenia-rs` read-only. That repo is retired and
|
|
the directory is gone**, so the mount pointed at nothing; removed.
|
|
|
|
**Credentials** — three files on the host, `chmod 600`, mounted read-only:
|
|
|
|
```
|
|
~/.sylph-claude-token Claude Code OAuth (a subscription, never the API)
|
|
~/.sylph-git-credentials the agents' PUSH credential — write:repository ONLY
|
|
~/.sylph-gitea-token-decoder the decoder's own Gitea account, for issues/PRs
|
|
~/.sylph-gitea-token-port the port's
|
|
~/.sylph-gitea-token-fabi the HUMAN's, full grant — mounted into NOTHING
|
|
```
|
|
|
|
🔴 **The narrow scope on `~/.sylph-git-credentials` is load-bearing.** Every
|
|
issue endpoint refuses it, which is what stops an agent reaching the Gitea API
|
|
as the human. Putting a broader token there would hand both agents the human's
|
|
identity, including merge. The human's own token lives in a path no launcher
|
|
reads and no container mounts — verified, not assumed.
|
|
|
|
The Gitea MCP server is passed the token **by path**, not by value:
|
|
`GITEA_TOKEN_FILE=/sylph-home/re/.sylph-gitea-token`.
|
|
|
|
## 3. How an agent iteration actually works
|
|
|
|
Both briefs — `decoder-loop.md`, `port-loop.md` — are read **from the host tree
|
|
at container launch**. The loop is: read notifications → pick one approved issue
|
|
→ do the smallest experiment → classify → refute something → write it down →
|
|
`push-work`, open the PR, label `state/needs-human`, stop.
|
|
|
|
⚠️ **Notifications are POLLED. Nothing is pushed.** An `@mention` or a PR comment
|
|
reaches an agent only on its next iteration.
|
|
|
|
### 🔴 Two defects the other machine should know about
|
|
|
|
**A brief change does not reach a running agent.** The entrypoint resumes the
|
|
newest transcript in the container's Claude volume, and the only guard is a
|
|
`<120 s` restart-loop test. Everything older resumes unconditionally — so a
|
|
container restarted after a workflow change comes back **still following the old
|
|
brief**, on the old branch. Seen: a decoder resumed a 7-day-old pre-migration
|
|
session and had to be stopped. Until the entrypoint learns to test transcript
|
|
age (or take a `SYLPH_FRESH=1`), adopting a brief change means **archiving the
|
|
transcripts** in `sylpheed-decoder-claude/projects/` and starting fresh.
|
|
|
|
**Nothing brought an agent back to its own red PR.** It opened one, labelled the
|
|
issue, stopped — correctly, per the brief — and the next firing started a new
|
|
question on top. It pushed over a red CI six times. Fixed in both briefs (#24),
|
|
which is itself subject to the defect above: the fix only reaches a *fresh*
|
|
session.
|
|
|
|
## 4. The other machine's half
|
|
|
|
The Pi runs the Gitea instance (proxied to `git.mc02.dev` via a VPS, reachable
|
|
from the LAN and the internet) and the Actions runner. Its
|
|
`/var/lib/docker` is still on the **117 GB SD card** beside an idle 916 GB SSD;
|
|
moving `data-root` is outstanding.
|
|
|
|
⚠️ **The runner serialises.** Three jobs per push, one at a time — a PR's checks
|
|
sat *pending* for 40+ minutes behind a failed native job. A pending check there
|
|
is not necessarily a slow one.
|
|
|
|
## 5. Standing constraints that are not about containers
|
|
|
|
* **One emulator process at a time**, ours or Canary, never both — a lockfile.
|
|
* **Canary runs muted.** Point it at the real ISO, not the `sylpheed.iso` symlink
|
|
(Wine cannot resolve it).
|
|
* **Never judge a crash or a hang from a Bash-launched emulator run.** A SIGKILL
|
|
that looked like our binary was the editor's process supervisor. Ask the human
|
|
to run it natively.
|
|
* **Never screenshot the port or the emulator yourself**; ask the human.
|
|
* The oracle is the real game in Xenia Canary — not our renderer, not the port.
|