lmjtfy.git / apps / lmjtfy / CLAUDE.md

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. stream therefore 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 reading versions to inserting into running there 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.
  • versions is keyed by the body itself, not a hash of it. The page's call_id is 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.
  • 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 writes asked or counts must call changed, 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.
  • view.rs stays free of worker types 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 JS Map of every JSON object that is not a Rust struct. ai.rs passes JSON text through JSON.parse and back through JSON.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 /live sockets are the archive's, accepted with accept_web_socket (hibernation). Do not hold them in a field or await on them: a hibernating object keeps them only through get_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's erDiagram lacks 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 in budget::Which::key and meter.rs.
  • A page's place is the country and city Cloudflare gives the /live request, and lives only on its socket (serialize_attachment). The Worker sets archive::COUNTRY/CITY itself, 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 sets PLACED on every /live it 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 within ONLINE_EVERY_MS. Broadcasting on every connect is pages² messages when a deploy reconnects them all.
  • The /live socket 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 load page.js (/rules did not until 2026-10-02, so its badge never lit).