Commit Graph

8 Commits

Author SHA1 Message Date
MechaCat02
f173382fd3 docker: the expect wrapper swallowed both the signal and the exit status
Some checks failed
CI / Native — ubuntu-latest (push) Failing after 8m8s
CI / WASM — Web (push) Failing after 6m58s
CI / Formatting (push) Failing after 1m3s
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
A tooling review predicted a PID-1 signal problem from two symptoms we could not
explain: `OOMKilled: true` with **ExitCode 0**, and `--continue` failing to find
a conversation that plainly existed. Traced it, and the prediction was right --
though the culprit is not PID 1, it is one level below.

The path is  tini (PID 1) -> entrypoint.sh (exec'd) -> expect -> spawn -> claude

`spawn` CANNOT be an exec: expect has to stay alive to drive the pty. So expect
is the process Docker signals, and everything depends on it passing things on.
It did neither, in two lines:

1. NO SIGNAL FORWARDING, no trap of any kind. `docker stop` sent SIGTERM to
   expect, which died and took the pty with it. Claude Code never got a SIGTERM,
   so it never ran SessionEnd hooks and never wrote lastSessionId/history --
   which are written ONLY at a graceful shutdown. That is the entire reason
   `claude --continue` answered "No conversation found to continue" with 33 MB of
   transcripts in the volume beside it, and why we resume by scraping a session
   id off a transcript filename.

2. `eof { exit }` RETURNED 0 FOR EVERY DEATH. A bare `exit` in expect is exit
   ZERO. When the OOM-killer took the child, expect saw EOF and reported a clean
   exit. `OOMKilled: true` with `ExitCode 0` was never Docker being odd -- it was
   this line. It also meant `--restart on-failure` would read a memory kill as
   success, which is why the policy had to be `unless-stopped`.

Fixed and MEASURED, old against new, in a container:

  child exits 7        old -> 0    (the bug)      new -> 7
  SIGTERM to wrapper   old -> 143, child's trap NEVER RAN
                       new -> 42,  child trapped and cleaned up

Same file in both images; they were byte-identical, so the port copy takes the
same change.

Consequences worth stating: a kill now reports 137 rather than 0, so exit codes
mean what they say; `docker stop` gives Claude Code a real SIGTERM, so it runs
SessionEnd and writes the session index -- which may make the transcript-filename
resume unnecessary. That is not assumed here: the resume path stays as it is
until it is verified redundant.
2026-09-03 21:07:19 +02:00
MechaCat02
e1749c83e1 docker: auto-restart, and resume the session the agent was actually in
Some checks failed
CI / Native — ubuntu-latest (push) Failing after 7m29s
CI / WASM — Web (push) Failing after 6m17s
CI / Formatting (push) Failing after 1m4s
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
The decoder died mid-task and it took four separate findings to explain, each
of which read as something else:

1. OOM-KILLED, REPORTED AS A CLEAN EXIT. `OOMKilled: true` with **ExitCode 0**.
   So `--restart on-failure` would treat a memory kill as a successful finish
   and leave the agent down -- the policy has to be `unless-stopped`.

2. THE JOB CAP WAS SET AND THEN REMOVED THREE LINES LATER. build-reborn has
   always exported CARGO_BUILD_JOBS, but a raw `cargo test --release -p
   sylpheed-formats` never reaches the wrapper. Adding `-e CARGO_BUILD_JOBS` to
   the launcher did not help either: the entrypoint recomputes and exports over
   it unconditionally. An explicit value now wins, and says so in the log.

3. THE MEMORY CONSTANT WAS WRONG. `mem_gib * 2 / 3` assumes ~1.5 GB per job;
   release rustc on this workspace needs ~2 GB, and 4 jobs in 6 GB is what died.
   Divisor is now 2.

4. `--continue` CANNOT RESUME AN ABRUPT DEATH, which is the only kind we get.
   It resolves through ~/.claude.json's per-project `history`/`lastSessionId`,
   and MEASURED mid-session both are None -- they are written at a graceful
   shutdown. A killed container never writes them, so `--continue` answered
   "No conversation found to continue" with 33 MB of transcripts in the volume
   beside it. Persisting .claude.json did not help, because the fields were
   never populated in the first place; that attempt is removed rather than left
   in looking useful.

   The TRANSCRIPTS are durable and named by session id, so the entrypoint reads
   the id off the newest one for its cwd and passes `--resume <id>`. Verified
   on both agents: each reattached to its exact prior session and appended to
   the same file rather than opening a new one.

The /loop prompt is still passed alongside `--resume`, so the loop is RE-ARMED
rather than merely restored -- a resumed conversation with no wake-up scheduled
answers once and stops, which looks like resuming and is not.

Restarting into the same death is guarded at the other end: a start less than
120 s after the previous one begins FRESH instead of continuing back into
whatever killed it. That fired correctly during this work.

On resume the agent is told it was restarted, that its in-progress work is
uncommitted in the tree, that any build or capture it had running did not
finish and its absence is not a result, and which wrapper to prefer over a raw
release build.
2026-09-01 20:20:51 +02:00
MechaCat02
1b1a4dfcd3 containers: an expired token could never be replaced
Some checks failed
CI / Native — ubuntu-latest (push) Failing after 8m1s
CI / WASM — Web (push) Failing after 7m13s
CI / Formatting (push) Failing after 1m10s
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
Credentials were seeded only when the container's copy was MISSING. So when a
session expired, the file still existed, the copy was skipped, and restarting
changed nothing -- the one recovery path a human has, re-logging in on the host,
could not reach the containers at all.

Now re-seeds whenever the host's copy is newer. Newer-wins rather than
always-copy, because a container refreshes its own token mid-run and that copy
may legitimately be the fresher of the two.

Found when both sessions expired: host credentials at 16:30, containers holding
14:20 and 14:24.
2026-08-29 16:31:53 +02:00
MechaCat02
72b10e7d03 decoder: mount the disassembly DB and the flat VA image
Some checks failed
CI / WASM — Web (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
CI / Native — ubuntu-latest (push) Has been cancelled
The decoder had neither, and reported the gap precisely: four scripts in this
repo READ /work/xenia-rs/sylpheed.db and nothing produces it, so the whole static
PPC route was consumers with the producer missing.

Both exist on the host and are now mounted read-only: the 586 MB database (25 481
functions, 851 classes with RTTI, EH tables, imports, 1.8M indirect-dispatch
candidates) and the decompressed image.

The image is the more useful of the two. It is a FLAT VA DUMP -- file offset =
VA - 0x82000000 -- so reading a known address needs no XEX decrypt, no LZX, and
no booted emulator. The decoder had independently recovered the same bytes by
dumping /dev/shm/xenia_memory_* and validating against the GamePart table, which
is good work and a sound method, but it noted itself that needing a running
emulator is a bad dependency for something the entire static corpus rests on. It
does not need one.

Also recorded that an earlier claim the .pe was STALE was tested and refuted, so
nobody re-litigates it, and that instructions.raw is an INT rather than hex.

Written down as reference material, explicitly NOT a deliverable: they are
read-only, they come from outside the repository, and a fresh checkout elsewhere
has neither. Reimplementing the producer belongs in sylpheed-formats, and until
it exists every static finding rests on an artefact this project cannot rebuild.
2026-08-29 15:17:52 +02:00
MechaCat02
a8d2491366 audio: actually install the capture path I kept deferring
Some checks failed
CI / Native — ubuntu-latest (push) Failing after 8m25s
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / WASM — Web (push) Has been cancelled
The audio work was three parts and I shipped two. The transcode-fidelity method
and the pinned 5.1 downmix landed; the null sink -- the only one that answers
"what does the GAME play" -- I deferred to "the next natural rebuild window" and
then rebuilt both images four times without doing it.

pulseaudio-utils is now in both, with tools/audio-capture wrapping it: a null
sink is a real device as far as an application is concerned, so Canary and Godot
open it normally and parec records what they emit.

This unblocks the decoder's Q8. The cue-to-event bindings are currently a name
match against the authors' own identifiers -- a plausible guess, not a
measurement -- and capturing what the game plays on a menu move converts them.

`audio-capture run` reports the peak level and warns when the capture is silent,
because silence is the failure that looks like success: a WAV of exactly the
right duration, full of zeroes, because the application opened a different sink.
A duration check alone passes it, which is how a confident wrong number gets
made.
2026-08-29 13:41:02 +02:00
MechaCat02
20b3c74b2c agents: they never spoke, the decoder lost the disc, and both shared one state dir
Some checks failed
CI / Native — ubuntu-latest (push) Failing after 8m10s
CI / WASM — Web (push) Failing after 7m31s
CI / Formatting (push) Failing after 1m13s
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
Three defects, all mine, found by checking instead of assuming.

**They never exchanged a word.** SendMessage=0, ListAgents=0 across both new
sessions. PROTOCOL.md specified in detail what a message MAY and MAY NOT do and
never said how to send one or that the other agent was addressable -- they knew
that last time only because the human told them directly, and rebuilding with
fresh volumes wiped it. Policy without mechanism is prose. Now documented with
the two addresses, a worked example, and an instruction to introduce themselves
on the first iteration rather than waiting to have a question.

**The decoder lost the disc and the ISO.** They used to arrive inside the project
mount and silently stopped when /work became a clone. Silently is the word: the
disc-gated tests SELF-SKIP without SYLPHEED_DISC and report green, so a whole
test suite would have passed while measuring nothing. Both are now mounted
explicitly, the ISO at a stable path so run-canary does not depend on host
directory names.

**Both agents shared one Claude state directory.** They share the host's
~/.claude, and once both working directories became /work they resolved to the
same projects/-work/ -- two supposedly independent agents writing to one place,
which undoes the point of separate checkouts. Each now has its own volume, seeded
once from the host with credentials only, so a token refresh writes locally and
neither can corrupt the host's auth.

Also widened the pacing rule. It banned ScheduleWakeup by name; the decoder then
scheduled itself an hourly cron job -- not harmful, but the same instinct that
ended a run yesterday, through a door I had left open. Now: no self-scheduling by
any route.

Mount audit after the changes: shared and intentional are the exchange volume and
the read-only credential seed. Everything else -- repo, Claude state, cargo,
target, canary, disc, ISO -- is per agent or one-sided.
2026-08-29 13:05:41 +02:00
MechaCat02
01a3505b1e containers: fix volume ownership and make the clone guard survive interruption
Some checks failed
CI / Native — ubuntu-latest (push) Failing after 8m12s
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / WASM — Web (push) Has been cancelled
Two bugs, both mine, both found by starting the thing.

**Volume mount points must exist AND be owned by the agent before USER agent.**
Docker seeds a named volume from whatever the image has at that path, ownership
included, and creates a ROOT-OWNED directory when the path is absent. Either way
the agent cannot write, and the failure surfaced far from its cause: "clone
FAILED", with no permission error anywhere in sight. The port's own Dockerfile
already carried a comment explaining this trap, which I then walked into for
/work and /exchange.

**The clone guard checked for a .git directory, not a usable HEAD.** A clone
interrupted partway -- the container was removed while one ran -- leaves a .git
with no commits, and a presence check then skips the retry forever and hands the
agent an empty repository that looks like a checkout. It now verifies HEAD, and
clones via a temp directory so a partial result never lands in /work at all.

Also: the port launcher's path defaults still assumed the old repo root, so it
mounted no disc; and the stale /reborn notice is gone now that there is one
repository.

Verified running: both agents cloned 06676d3, `share` on PATH from /work/tools,
/exchange agent-owned, canary at /canary for the decoder, disc at /disc for the
port.
2026-08-29 12:52:13 +02:00
MechaCat02
06676d3dc0 containers: each agent clones the monorepo into its own volume
Some checks failed
CI / Native — ubuntu-latest (push) Failing after 7m48s
CI / WASM — Web (push) Failing after 7m16s
CI / Formatting (push) Failing after 1m15s
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
The last structural fix for the collision class that has bitten three times. Both
containers now clone the repository into their OWN named volume instead of
bind-mounting a human's working tree, so an agent's local git config cannot
capture a human's commits, a credential helper cannot leak a container-only path
onto the host, and a `git add -A` cannot sweep another party's in-flight files.

Cloned once at startup and never auto-pulled: pulling under a running agent
moves files out from under whatever it is mid-edit, which is the same bug again.

Accepted knowingly: Claude Code keys per-project memory off the working
directory, so moving off the host path starts that memory empty. The corpus in
docs/ is the memory that matters and it travels with the clone.

Other changes:
* docker/agent -> docker/decoder; the launcher is sylph-decoder. Roles, not
  "the agent", now that there is more than one.
* /reborn is gone -- one repository now, so the port reads HANDOFF from its own
  checkout rather than through a live read-only mount of someone else's tree.
* Canary mounts separately at /canary; it stays a fork tracking upstream.
* A shared `sylpheed-exchange` volume at /exchange, with tools/ on PATH so
  `share` is available in both.
* The decoder's credential file gets the .host-copy treatment the port already
  had -- `credential.helper=store` rewrites by rename-over-target, which is
  EBUSY on a bind mount and reports a fatal that is not one.
* Budget split deliberately: decoder 5 cpu / 6 GB, port 3 / 4, leaving room for
  the planned Referee. "Half the host" was right when there was one agent.

Prompts move to docs/agents/ and are rewritten around the protocol: the oracle
is the running game, dynamic RE stays with the decoder, each iteration must
attempt to refute one claim of the other, and neither may verify its way out of
its own role.
2026-08-29 11:48:30 +02:00