For agents, on top of README.md, which they read first.
- Jev and Workers AI calls are not
Send(they hold JS values), and axum handlers must be.streamtherefore spawns the work on the isolate's executor and feeds the response from a channel. Do not await such a call inside a handler. - The archive's lookup-then-start must not await. In
Archive::once, from readingversionsto inserting intorunningthere is no.await, so no second ask can run in between and send the same request. An await there brings the duplicate back. - The call runs under
wait_until, not under the ask that started it. A visitor who leaves cancels their ask; the call still finishes and is kept. versionsis keyed by the body itself, not a hash of it. The page'scall_idis a hash, and only names a call; the archive never looks a response up by it.- The archive's schema changes only by adding a step to
MIGRATIONS. A step that has shipped has run on the live object and will not run again; editing it changes nothing there and breaks a fresh one. - A step runs inside
Past::atomically, and a failed one leaves the archive refusing everything butBookmarkandRestore. Do not put the panic back innew: an object that cannot start cannot be restored either. The tests run every step on a real SQLite (archive::tests), which is where a statement that does not parse is found. - A question the owner's backend asks of
eventsis a view inMIGRATIONS, not SQL in the backend (the owner, 2026-10-04). One that is asked by the day is the three of<name>_live,<name>_finishedand<name>, with a row offinishedand its name inFINISHED; a test holds the four together. A_liveview readshits_unfinishedand groups bydayfirst. To change one that has shipped: a new step that drops and makes its views, empties its table, and sets itsthroughback to -1, so the days are copied again. - Never copy a day into a
_finishedtable but throughfinish_sql. It refuses a day already finished. A finished day's events are not in the_liveview any more, so copying it again would put nothing where the day was (the test found this, 2026-10-04). - A view asked through another view loses its
WHERE day. SQLite pushes the condition one view down, not two:daysreads all ofhours_finishedwhatever day is asked for, which is small. Do not build a view on a_visitorsview; ask it directly. versionsandeventstakeINSERTand nothing else. Triggers refuseUPDATEandDELETE(step 17). A correction to either is a migration step that drops the trigger, corrects, and makes it again, with the reason beside it: do not drop one anywhere else.- What the home page shows is kept in the archive object's memory
(
Shelf::home) until something it is made of changes. Anything new that writesaskedorcountsmust callchanged, or the home page shows the old numbers until the object next sleeps. Rows read are metered (five million a day on Workers Free; three million were gone by mid-morning on 2026-10-03), so do not read a whole table on a path every visitor takes. - The feed's questions for suggesting are kept in the archive object's
memory too (
Shelf::listed), read again only after a question is asked or moderated (relisted). A suggestion is wanted on every pause in someone's typing: never readaskedfor one. view.rsstays free ofworkertypes so its tests run natively.- Text from the visitor or the LLM reaches the page only through maud's escaping. Option labels and level descriptions are the LLM's words and are as untrusted as the input.
- Do not use workers-rs's
Ai::run. It goes through serde-wasm-bindgen, which makes a JSMapof every JSON object that is not a Rust struct.ai.rspasses JSON text throughJSON.parseand back throughJSON.stringify, which is also what lets the page show the reply untouched. - Maud attribute names with a dot are written as string literals
(
"data-on:input__debounce.300ms"). Bare, the dot does not parse. - The look is typesafe.ai's, on purpose (the user, 2026-10-02): near-black
on white, one pink band, ordered-dither dot fields, old operating system
windows, pixel type for labels.
page.css's header says what was copied. Their two display faces are commercial; Inter Tight and VT323 stand in. Do not "modernise" it with rounded corners, shadows or a dark theme. - The dither is drawn into CSS variables, not into elements. The transcript is replaced wholesale on every event, and a canvas inside it would be wiped.
- The
/livesockets are the archive's, accepted withaccept_web_socket(hibernation). Do not hold them in a field or await on them: a hibernating object keeps them only throughget_websockets, and an object kept awake by a socket is billed for every second of it. - A toast shows a question only if the feed may (
listed). Anything else is "someone asked". Keep it that way: the toast reaches strangers. - The storage diagrams in README.md are kept beside
MIGRATIONS. A test (archive::tests) fails if the archive'serDiagramlacks a column the migrations make, so a new column changes the diagram in the same commit. The budgets' diagram has no such test: its keys are inbudget::Which::keyandmeter.rs. - A page's place is the country and city Cloudflare gives the
/liverequest, and lives only on its socket (serialize_attachment). The Worker setsarchive::COUNTRY/CITYitself, removing any the page sent; never take a place from anywhere the page controls, or anyone can put words in every visitor's top bar. The same goes for the event the Worker passes with it (archive::EVENT). - A socket the new Worker did not place is closed with 1012 after
REPLACE_AFTER_MS(Seen::stale), so a page that reconnected through an old Worker during a deploy gets its place. The Worker setsPLACEDon every/liveit forwards, place or not; drop that header and every page reconnects every ten seconds forever. - "N online" goes out on the alarm, not per socket (
Archive::soon): a page that connects is told the state at once, everyone else withinONLINE_EVERY_MS. Broadcasting on every connect is pages² messages when a deploy reconnects them all. - The
/livesocket is the SharedWorker's (live.js), one per browser per build. It replays only what a joining tab cannot render itself (the build, the count, the places) and passes everything else on as it came; do not replay toasts or feed patches, a joining tab would show them twice or apply stale ones. Every page that has the nav must loadpage.js(/rulesdid not until 2026-10-02, so its badge never lit). - Release files are the code host's alone, and the token never leaves
GitHub's API.
release::fetchfollows the302by hand and fetches the signed address with noauthorization; do not letfetchfollow it itself. A file is returned asServed::File, fromfetchbefore the router: a body that goes through axum loses itsContent-Length(measured on staging). The body is streamed: neverbytes()/to_bytesa release file (80-200 MB against a 128 MB isolate). A path segment isrelease::segmentor it is a 404; a kind two files share is a 404, never a pick. No GitHub text (tag, asset name, message) goes in a log line or an error response. - A download is a
downloadevent, and not also aview.Event::gotskips the file paths (is_download); keep the two in step withrelease::route. No toast for it (the owner, 2026-10-05).