# Storage and MAX in the m0 wheel — 2026-09-25 The `m0` wheel carried five source trees and no way to a database, and no word about MAX. This round adds both, on the two decisions the rounds before it made: `m0-sqlite` opens its library at run time (D49), so it can ride where `m0 build` takes no link flag; and an m0 application reaches N cores as threads (D48), so a binary that links MAX's parallel runtime has a served shape. SPEC N39–N41 are the rows; D50 is the decision; `m0` is `0.3.0`. ## Storage rides because it links nothing `hatch_build.py`'s table gains `packages/m0-sqlite/src` and `packages/m0-postgres/src`, and `smoke-m0-wheel`'s second spelling of it gains the same two lines — the guard, as before. Nothing else about the wheel changes: the trees are `git ls-files` of the two directories, mapped under `m0/_mojo/m0_sqlite` and `m0/_mojo/m0_postgres`. What makes them shippable is what they do not do. Neither names its C library on any link line — libpq never did, libsqlite3 stopped in D49's round — so an application's `m0 build` needs no flag, and `m0 test`, which is `mojo run` and resolves nothing from its own image, can open a database. The wheel smoke writes a test that does (`test_store.mojo`, in-memory SQLite through the installed tree) into its project and requires it green under `m0 test`; that is the property the trees ride on, held from outside. The library still has to be on the machine. macOS has it in the shared cache; Debian's slim image does not, so the scaffold's Dockerfile installs `RUNTIME_LIBS` in the runtime stage — `libsqlite3-0` by default, `libpq5` one build argument away — and records what it installed in `about.json`'s `libs`. The image smoke asks the running container for `/usr/lib/*/libsqlite3.so.0`, because "the storage package is in the wheel" and "the image can open a database" are two claims. ## The `live` scaffold keeps its kick count A template with a database in it is what shows an application author the rules. `store.mojo` is one row in SQLite — `counters(name, value)` — and the HANDLER owns it: `LiveHandler.make` opens the connection, and the host runs `make` once per worker, per loop and per handler-pool thread, always after it forks — a SQLite connection carried across `fork()` is the one thing SQLite's own documentation says not to do. The kick view counts in the database inside its own request (`UPDATE … value = value + 1`, so two workers kicking at once both count) before it touches the board's word, and `/stats` reads the total back from the file; the board's word keeps kicks since start, for the wave alone. The schema is created inside an IMMEDIATE transaction, for the reason `datastar_todo` found at two workers. That is the second shape. The first had the PRODUCER own the store — opened at its first step, the board seeded from it, the count written back whenever it moved — and CI's ubuntu leg found it in one run: after `smoke.sh`'s restart, `/stats` said `{"steps":0,"kicks":0}`. The producer polls, every 50 ms while nobody watches, and the smoke's kick landed inside one poll and its SIGTERM before the next, so the write never ran; locally the same race had simply not fired. A count that must survive a restart is written in the request that changes it, where the 204 the client sees is a committed row, and a producer that polls is the wrong owner for it. `smoke.sh` restarts the server on the same file and polls `/stats` for the kick; `smoke-scaffold` runs that script, and `sabotage-scaffold` holds both halves: the view's write reverted is caught at the wire's first `/stats`, which reads the database, and the file swapped for an in-memory store — counted, then forgotten — is caught only by the restart. The move left the board's word with nothing on the wire watching it. `sabotage-scaffold`'s older kick rule drops `st.board.add(B_KICKS, 1)` and expected the wire's `/stats` to notice, which it did while `/stats` read the word; reading the database, it cannot, and the 2026-09-26 pre-release run reported the rule MISSED with the whole scaffold green. The word's one consumer is the wave, so the wire now watches the wave: it keeps its stream open across the kick and requires a later frame whose lowest bar stands above the highest bar the wave drew before it. Unkicked, a bar never passes 20 plus the sine's full swing of 30; the step that applies a kick adds 40 to every bar, so the frame that shows it clears every frame before it outright, and with the word dropped no frame ever does. The template's own test opens the store in memory (`open()` wants a file it can put in WAL mode) and runs under `uv run m0 test` with nothing linked, on both platforms, which is the first thing this round wanted. The `views` template stays in memory on purpose. Its list is the smallest htmx example there is, and a second database template would be a second copy of the rules above; an application that wants both starts from `live`'s store and `views`' routes. ## MAX is a pinned companion, not a dependency `max-core` pins its own `mojo-compiler` exactly, so a MAX beside the wrong mojo is a second toolchain in the venv. The wheel therefore records the pair it was gated beside: `gated_max`, read from the root's `max` dependency group — the one `smoke-parallel-runtime` syncs — the way `gated_mojo` is read from the root pin, refusing anything but one exact `max-core==X`. `max-gated` joins the ONE list in `checks.py`, after `mojo-gated`: absent, it passes saying how to add MAX; present at another version, it refuses with the sentence naming the fix; present at the gated version, it passes naming it. The doctor's JSON carries `gated_max` and `max` beside their mojo twins. The wheel smoke's refusal arm is a stub `max-core` 9.9.9 beside the REAL mojo, so `mojo-gated` passes and the sentence is `max-gated`'s alone; a stub, because MAX is a gigabyte of wheels and nothing of it needs to run to read a version from metadata. What does need to run is measured where MAX is already installed: `smoke-parallel-runtime` gained a phase that runs the release recipe — `relocate.py`, then `bundle_artifact.py`, the two scripts the wheel ships unedited — on a copy of the probe and requires `libAsyncRTMojoBindings` to land beside it, then answers `--doctor` and serves `/par` from that directory with the build venv unreachable. The bundler discovers the closure from what the binary names rather than from a list, which is why it needed no change: MAX's runtime sits in `modular/lib` like the rest. The scaffold says the rest: `pyproject.toml` carries the `uv add` line with the gated version substituted (`__M0_MAX_VERSION__`, the fourth token, `pyproject.toml`-only like the other two versions), and `AGENTS.md` the four rules — the capture list on `parallelize`'s closure, where it belongs (a producer's step, a heavy view rarely busy twice), that a binary linking it is served as threads and refused as forked workers (E32), and that the release build bundles its runtime. ## What building it turned up **`uvx --from WHEEL` runs the m0 it cached, not the wheel it was handed.** The first run of `sabotage-scaffold`'s new rule — the `__M0_MAX_VERSION__` substitution reverted in `new.py`, the wheel rebuilt, `m0 new` run through `uvx --offline --from dist/m0/…whl` — was MISSED: the written `pyproject.toml` carried the version, substituted. uv keys a tool environment by the wheel's name and version, and every smoke's wheel is rebuilt under the one version `0.3.0+tree`, so on a machine that had run the smoke before, `uvx` served the m0 it built the first time. Measured with a marker printed from `new.py`: absent through a plain `uvx`, present through `uvx --refresh-package m0` and through `--no-cache`. The venv half of the smokes already carried `uv sync --refresh-package m0` for the same hazard; the three `uvx` sites (the scaffold, image and dev smokes) now carry `--no-cache`, and the rule is caught. `--no-cache` and not `--refresh-package`, because CI's uv (0.12.19) refuses the latter beside `--offline` — `the argument '--offline' cannot be used with '--refresh'`, the first CI run of this branch — while the container's 0.8.17 accepted the pair, and offline is the point of that step. Measured on both: 0.8.17 serves the stale environment through a plain `uvx` and 0.12.19 does not (it noticed the rebuilt wheel), and `--offline --no-cache` runs the wheel handed to it on either. CI's fresh runner never saw the stale cache, which is the shape of a gate that passes with the bug live. ## What follows An application outside the tree on `live`'s store, deployed with a volume, is the soak this layer still lacks (`poe milestones`). Postgres in a scaffold waits for one that needs it: the package rides, the image takes libpq by one argument, and the rules are in `AGENTS.md`.