Files
Sylpheed/docs/agents/CONTAINERS-AND-AGENTS.md
Claude 46ee0d7f78
Some checks failed
CI / Native — linux (pull_request) Failing after 5m14s
CI / WASM — Web (pull_request) Successful in 36m12s
CI / Formatting (pull_request) Successful in 1m40s
chore(agents): restore evidence sharing after the asset purge
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>
2026-09-20 13:12:26 +02:00

9.1 KiB

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.

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/Dockerfilerust:1.98.1-bookworm, the apt list copied verbatim from .github/workflows/ci.yml, plus clippy, rustfmt and the wasm32-unknown-unknown target.

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.