1@README.md 2 3- Run it inside the devshell. The binary links `libasound.so.2` and dlopens 4 wayland, xkbcommon and vulkan by bare name, and buck2 writes no runpath, so 5 outside `nix develop` it dies before `main` (`../../flake.nix` carries the 6 library path and the reason). 7- To see the window, call the MCP `window` tool; to see the game, `frame`; to 8 know what it is doing, `state`. Do not start a second instance to screenshot 9 it - the user sees every window, and the port is already taken. 10- The UI thread owns the console. MCP handlers send a `Request` down a channel 11 and the UI answers at a frame boundary; never reach for the console from the 12 server thread. 13- **A tool that changes anything goes in the `driving` router, never 14 `watching`.** `../../packages/mcp/src/lib.rs` (moved out of this crate 15 2026-09-20 - see that crate's own CLAUDE.md for why) has two 16 `#[tool_router]` blocks; which block a tool is in IS whether it exists with 17 dev mode off. The end state is a client with dev mode off while the bot 18 plays, so a mutating tool in `watching` is a hole in that, and nothing 19 else checks. 20- **Pick up a new build with the `restart` tool, not by killing the window.** 21 Killing it skips `on_exit`, so up to 30 seconds of game are lost, and the 22 user watches the window vanish for longer. And never `pkill -f native`: the 23 pattern matches the shell running it (2026-09-20, exit 144). `pgrep -x 24 native`. 25- **Turn dev mode off again when done debugging.** It is also off after any 26 restart. 27- **Nothing but MCP may reach stdout in `mcp` mode.** `proxy.rs` and anything 28 it calls use `eprintln!`. One stray `println!` corrupts the client's stream, 29 and the symptom is the client dropping the server with a parse error. 30- **Test `native mcp` with `/tmp`-style scripted stdio, not by restarting the 31 harness.** Pipe JSON-RPC lines in, read lines out; the case that matters is 32 a window `restart` underneath it, which must produce `tools/list_changed` 33 twice (gone, back). It once produced none: rmcp's client reconnects silently 34 and the window restores the session, so the proxy could not tell 35 (2026-09-20). `/alive` exists for exactly this; do not remove it as unused. 36- **Both MCP lifecycles are served and both matter.** Sessions (before 37 2026-07-28) are told of a tool-list change on their stream; sessionless 38 clients by `subscriptions/listen`. Claude Code 2.1.278 was seen on the 39 sessionless path (its calls succeeded with no session in the store, 40 2026-09-20). Test a change to either with curl: the new protocol needs 41 `MCP-Protocol-Version` and `Mcp-Method` headers and the three 42 `io.modelcontextprotocol/*` keys in `params._meta`. 43- Every tool call goes through `Activity` first. A new tool that skips it is 44 invisible in the panel. 45- Draw panels beside the picture, never over it. 46- **The bot is `zbanks::Bot`, one per process (its state is C globals), and 47 every emulated frame goes through `Bot::tick`.** Never set the console's 48 buttons directly for a frame: the human pad (keyboard + MCP holds) is the 49 bot's INPUT, and what it returns is what the console holds. That is 50 upstream's Snes9x contract; `../../research/zbanks-alttp.md` has it. 51- **Nothing may write to stdout in window mode except the bot.** Before the 52 bot starts, `stdout_to` makes fd 1 a pipe whose reader thread is the only 53 writer of `run/zbanks-window/bot.log`, rotated at `BOT_LOG_CAP` (50 MB, 54 `BOT_LOG_KEEP_FILES` = 3). App messages are `eprintln!`, which is 55 `run/native.log` when started as the README says. Do not go back to 56 truncating `bot.log` in place: the user rejected that, and a truncate 57 under the C's open fd is the race the pipe exists to avoid. 58- **The C is not hot-patchable.** `hot_patch` reaches `logic_impl`/`ui_impl` 59 and `packages/panels`; anything under `third-party/c` or `packages/zbanks` 60 needs a build and `restart`, and a restart starts a bot that remembers 61 nothing (its map is C globals, not part of a snapshot). 62- **The console boots the PATCHED image** (`zbanks::rando::patch_rom`), so a 63 snapshot taken by some other tool from the vanilla image is refused by its 64 cartridge CRC. Make states for this window from a patched image 65 (`apps/zbanks` patches before it boots). 66- **Adding a field to `App` needs `restart`, not `hot_patch`.** 67- **Only `Playback::Live` may call `bot.tick`.** `Paused`/`Playing` drive 68 `app.console` straight from the log (`replay::format::apply_wram_diff`, 69 `zbanks::apply_pad`) - the bot's own C state is left exactly where it was, 70 which is what lets returning to `Live` at the tip just resume ticking it 71 with no reconstruction (proven byte-identical by `apps/replay-probe`). 72 Calling `bot.tick` from `Paused`/`Playing` would silently desync the bot's 73 understanding of the game from whatever frame the picture is showing. 74- **"Run the bot from here" is a restart (`--branch-from`), never an 75 in-process rebuild.** `zbanks::Bot::start` panics on a second call in one 76 process (`packages/zbanks/CLAUDE.md`), so reconstructing the bot at a past 77 frame cannot happen alongside the LIVE bot already running here - the new 78 process replays the whole history through its own fresh bot 79 (`replay::rebuild::Rebuilder`) before going live as a new branch. Do not 80 try to short-circuit this by driving a second `Bot` in this process. 81- **A rebuild reinstalls the real Jev chooser when it finishes** 82 (`logic_impl`'s `progress.finished` branch) - the catch-up itself runs 83 under a `ReplayChooser` that only ever replays logged picks. Forgetting 84 this leaves Jev silently disabled after every "run the bot from here", 85 even with `--jev` set. 86 87## Hot patching 88 89`logic`/`ui` (the `eframe::App` trait methods) are thin shims: the real 90per-frame work is `logic_impl`/`ui_impl`, bare `#[inline(never)] fn`s taking 91`&mut App` (plus `&egui::Context`/`&mut egui::Ui`), called through 92`subsecond::HotFn::current(f).call((self, x))` rather than the `subsecond::call` 93sugar (which only takes a zero-argument closure - these take arguments). 94`research/subsecond-patch-build.md` §6-§8 and `apps/hotdemo/CLAUDE.md` are the 95background; this is the same mechanism against the real window. 96 97- **A new patch point must be a bare `fn` item, passed by name, never a 98 closure.** A closure capturing even one pointer-sized value silently takes 99 subsecond's "treat these bytes as a function pointer" branch instead of the 100 real redirect (§8.2.1) - the patch "applies" and does nothing, with no 101 error anywhere. `logic`/`ui` each carry a `debug_assert_eq!(size_of_val(&f), 102 0)` right before the call for exactly this reason; keep that assert on any 103 new patch point. 104- **`-Copt-level=3` inlines a small, single-call-site function into its 105 caller unless `#[inline(never)]` says not to** (§8.2.2). Without it there is 106 no addressable symbol left for a patch to jump to. 107- **`App` itself is never patched - only what runs against it.** State on 108 `app`/`self` survives a patch because the value already exists in the 109 running process; a `static` recompiled as part of the tip crate does not 110 (`apps/hotdemo`'s `TICKS` resets on every patch for this reason - same 111 limitation, not a bug to chase). 112- **A struct-layout change needs `restart`, not `hot_patch`.** Subsecond has 113 no safety net for it (research doc §5); a patch that changes the size or 114 field order of `App` or anything reachable across the patch boundary is 115 undefined behaviour, not a loud failure. 116- **A genuinely NEW thread-local can still fail loudly.** `tools/hotpatch` 117 copies a base binary's own thread-locals into the patch (its `.tdata`); 118 something that did not exist as a thread-local in the currently-running 119 binary at all has nothing to copy from and `tools/hotpatch` says so and 120 exits rather than emitting nonsense - rebuild without introducing it (or 121 restart, which relinks the whole binary and starts fresh). 122- **`mcp::Request::HotPatch` is handled on the UI thread, at a frame 123 boundary, like every other request** - `answer()` in `app.rs` (not 124 `main.rs`, since the 2026-09-20 split - see `app.rs`'s own doc comment) is 125 the only place `subsecond::apply_patch` is called from; never call it from 126 the MCP server thread. 127- **`patch_info` and `hot_patch` exist for `tools/hotpatch/patch.sh`, not for 128 a human to type by hand** - see that tool's own README/CLAUDE.md for the 129 MCP session dance (`initialize` -> `Mcp-Session-Id` -> tools/call) and for 130 what buck2 output shapes it depends on. 131- **`main.rs` is a three-line dispatcher; `app.rs` is its own `rust_library` 132 (`//apps/native:app`).** This is what lets `tools/hotpatch/patch.sh` get a 133 panels/app edit's fresh objects without ever relinking the executable - 134 see `app.rs`'s doc comment and `../../tools/hotpatch/CLAUDE.md`. Adding a 135 file to the window's own code means adding it to `app`'s `srcs` in `BUCK`, 136 not to `native`'s.