For agents, on top of README.md, which they read first.
- Run it inside the devshell. The binary links
libasound.so.2and dlopens wayland, xkbcommon and vulkan by bare name, and buck2 writes no runpath, so outsidenix developit dies beforemain(../../flake.nixcarries the library path and the reason). - To see the window, call the MCP
windowtool; to see the game,frame; to know what it is doing,state. Do not start a second instance to screenshot it - the user sees every window, and the port is already taken. - The UI thread owns the console. MCP handlers send a
Requestdown a channel and the UI answers at a frame boundary; never reach for the console from the server thread. - A tool that changes anything goes in the
drivingrouter, neverwatching.../../packages/mcp/src/lib.rs(moved out of this crate 2026-09-20 - see that crate's own CLAUDE.md for why) has two#[tool_router]blocks; which block a tool is in IS whether it exists with dev mode off. The end state is a client with dev mode off while the bot plays, so a mutating tool inwatchingis a hole in that, and nothing else checks. - Pick up a new build with the
restarttool, not by killing the window. Killing it skipson_exit, so up to 30 seconds of game are lost, and the user watches the window vanish for longer. And neverpkill -f native: the pattern matches the shell running it (2026-09-20, exit 144).pgrep -x native. - Turn dev mode off again when done debugging. It is also off after any restart.
- Nothing but MCP may reach stdout in
mcpmode.proxy.rsand anything it calls useeprintln!. One strayprintln!corrupts the client's stream, and the symptom is the client dropping the server with a parse error. - Test
native mcpwith/tmp-style scripted stdio, not by restarting the harness. Pipe JSON-RPC lines in, read lines out; the case that matters is a windowrestartunderneath it, which must producetools/list_changedtwice (gone, back). It once produced none: rmcp's client reconnects silently and the window restores the session, so the proxy could not tell (2026-09-20)./aliveexists for exactly this; do not remove it as unused. - Both MCP lifecycles are served and both matter. Sessions (before
2026-07-28) are told of a tool-list change on their stream; sessionless
clients by
subscriptions/listen. Claude Code 2.1.278 was seen on the sessionless path (its calls succeeded with no session in the store, 2026-09-20). Test a change to either with curl: the new protocol needsMCP-Protocol-VersionandMcp-Methodheaders and the threeio.modelcontextprotocol/*keys inparams._meta. - Every tool call goes through
Activityfirst. A new tool that skips it is invisible in the panel. - Draw panels beside the picture, never over it.
- The bot is
zbanks::Bot, one per process (its state is C globals), and every emulated frame goes throughBot::tick. Never set the console's buttons directly for a frame: the human pad (keyboard + MCP holds) is the bot's INPUT, and what it returns is what the console holds. That is upstream's Snes9x contract;../../research/zbanks-alttp.mdhas it. - Nothing may write to stdout in window mode except the bot. Before the
bot starts,
stdout_tomakes fd 1 a pipe whose reader thread is the only writer ofrun/zbanks-window/bot.log, rotated atBOT_LOG_CAP(50 MB,BOT_LOG_KEEP_FILES= 3). App messages areeprintln!, which isrun/native.logwhen started as the README says. Do not go back to truncatingbot.login place: the user rejected that, and a truncate under the C's open fd is the race the pipe exists to avoid. - The C is not hot-patchable.
hot_patchreacheslogic_impl/ui_implandpackages/panels; anything underthird-party/corpackages/zbanksneeds a build andrestart, and a restart starts a bot that remembers nothing (its map is C globals, not part of a snapshot). - The console boots the PATCHED image (
zbanks::rando::patch_rom), so a snapshot taken by some other tool from the vanilla image is refused by its cartridge CRC. Make states for this window from a patched image (apps/zbankspatches before it boots). - Adding a field to
Appneedsrestart, nothot_patch. - Only
Playback::Livemay callbot.tick.Paused/Playingdriveapp.consolestraight from the log (replay::format::apply_wram_diff,zbanks::apply_pad) - the bot's own C state is left exactly where it was, which is what lets returning toLiveat the tip just resume ticking it with no reconstruction (proven byte-identical byapps/replay-probe). Callingbot.tickfromPaused/Playingwould silently desync the bot's understanding of the game from whatever frame the picture is showing. - "Run the bot from here" is a restart (
--branch-from), never an in-process rebuild.zbanks::Bot::startpanics on a second call in one process (packages/zbanks/CLAUDE.md), so reconstructing the bot at a past frame cannot happen alongside the LIVE bot already running here - the new process replays the whole history through its own fresh bot (replay::rebuild::Rebuilder) before going live as a new branch. Do not try to short-circuit this by driving a secondBotin this process. - A rebuild reinstalls the real Jev chooser when it finishes
(
logic_impl'sprogress.finishedbranch) - the catch-up itself runs under aReplayChooserthat only ever replays logged picks. Forgetting this leaves Jev silently disabled after every "run the bot from here", even with--jevset.
Hot patching
logic/ui (the eframe::App trait methods) are thin shims: the real
per-frame work is logic_impl/ui_impl, bare #[inline(never)] fns taking
&mut App (plus &egui::Context/&mut egui::Ui), called through
subsecond::HotFn::current(f).call((self, x)) rather than the subsecond::call
sugar (which only takes a zero-argument closure - these take arguments).
research/subsecond-patch-build.md §6-§8 and apps/hotdemo/CLAUDE.md are the
background; this is the same mechanism against the real window.
- A new patch point must be a bare
fnitem, passed by name, never a closure. A closure capturing even one pointer-sized value silently takes subsecond's "treat these bytes as a function pointer" branch instead of the real redirect (§8.2.1) - the patch "applies" and does nothing, with no error anywhere.logic/uieach carry adebug_assert_eq!(size_of_val(&f), 0)right before the call for exactly this reason; keep that assert on any new patch point. -Copt-level=3inlines a small, single-call-site function into its caller unless#[inline(never)]says not to (§8.2.2). Without it there is no addressable symbol left for a patch to jump to.Appitself is never patched - only what runs against it. State onapp/selfsurvives a patch because the value already exists in the running process; astaticrecompiled as part of the tip crate does not (apps/hotdemo'sTICKSresets on every patch for this reason - same limitation, not a bug to chase).- A struct-layout change needs
restart, nothot_patch. Subsecond has no safety net for it (research doc §5); a patch that changes the size or field order ofAppor anything reachable across the patch boundary is undefined behaviour, not a loud failure. - A genuinely NEW thread-local can still fail loudly.
tools/hotpatchcopies a base binary's own thread-locals into the patch (its.tdata); something that did not exist as a thread-local in the currently-running binary at all has nothing to copy from andtools/hotpatchsays so and exits rather than emitting nonsense - rebuild without introducing it (or restart, which relinks the whole binary and starts fresh). mcp::Request::HotPatchis handled on the UI thread, at a frame boundary, like every other request -answer()inapp.rs(notmain.rs, since the 2026-09-20 split - seeapp.rs's own doc comment) is the only placesubsecond::apply_patchis called from; never call it from the MCP server thread.patch_infoandhot_patchexist fortools/hotpatch/patch.sh, not for a human to type by hand - see that tool's own README/CLAUDE.md for the MCP session dance (initialize->Mcp-Session-Id-> tools/call) and for what buck2 output shapes it depends on.main.rsis a three-line dispatcher;app.rsis its ownrust_library(//apps/native:app). This is what letstools/hotpatch/patch.shget a panels/app edit's fresh objects without ever relinking the executable - seeapp.rs's doc comment and../../tools/hotpatch/CLAUDE.md. Adding a file to the window's own code means adding it toapp'ssrcsinBUCK, not tonative's.