lmjtfy, the Worker: the page, the stream that answers a question, and the gate that runs as the visitor types.
POST /ask answers with Datastar events. Each one is the whole transcript
as it stands, so the browser holds no state: it morphs what it is sent.
What happens to an input is decided by rules (rules::RULES). First Jev
is asked, in one request, for every fact the rules want: can it judge the
input, what kind of question is it, is it several, and its answer if it
is one yes-or-no question. That alone ends most queries: a refusal, or a
direct answer. Only a question that needs options or a scale written, or
splitting up, goes to the LLM (Workers AI), and then Jev answers what it
wrote, again in one request. Jev costs a hundredth of what the LLM does.
No call is sent from here. Each goes to the archive
(archive.rs), which returns the response it already holds for that
exact request, or makes the call inside the day's budget and keeps it.
22use ::archive::{Ask, Called, Pot}; 23use ::card::Card; 24use ask::{Judged, Kept, Outcome, Prepared, Wanted}; 25use axum::Router; 26use axum::body::Body; 27use axum::extract::{Extension, Json, Path, Query, State}; 28use axum::http::{HeaderMap, Response, header}; 29use axum::routing::{get, post}; 30use datastar::prelude::{ElementPatchMode, PatchElements, PatchSignals}; 31use futures_channel::mpsc::{UnboundedSender, unbounded}; 32use futures_util::StreamExt; 33use jev_client::Client; 34use jev_protocol::ModelId; 35use jev_worker::{FetchTransport, WorkerRuntime}; 36use llm::Model; 37use rules::{Effect, End, Fact, Network, Next, Note, Value, Want}; 38use serde::Deserialize; 39use tower_service::Service; 40use worker::{Context, Env, HttpRequest, event}; 41 42mod ai; 43pub mod archive; 44mod browse; 45pub mod clone; 46mod diagram; 47pub mod events; 48mod host; 49mod meatproxy; 50pub mod meter; 51mod object; 52mod past; 53mod playground; 54mod release; 55pub mod view; 56 57use view::{Ending, JevCall, LlmCall, LlmOutcome, Tool, View}; 58 59const DATASTAR: &str = include_str!("../../../ds-bundle/datastar.js");
The pixel emoji the site draws (the online list's flags, the vote buttons): SerenityOS's, cut down to those (third-party/serenity-emoji).
62const EMOJI: &[u8] = include_bytes!("../../../third-party/serenity-emoji/emoji.woff2");
The commit this Worker was built from (build.rs). Pages carry it, and the
archive tells every page that connects what it is now.
66pub const BUILD: &str = env!("LMJTFY_BUILD");
What this deploy changed, for the people on the site: the built commit's
Release-Note: trailer, or empty (build.rs).
69pub const NOTE: &str = env!("LMJTFY_NOTE");
The secret's name: lmjtfy's own Jev key, apart from the estate's general one, as jevstrudel has its own.
What one visitor may do in a minute: full answers, and the facts request
that runs as they type. Counted by the budget object (meter::admit).
Cloudflare's own rate limiting binding was tried first and never refused
a request on the live site (2026-10-02: 45 asks in 40 seconds, limit 10).
84#[derive(Clone)] 85struct App { 86 env: Env, 87} 88 89#[event(fetch)] 90async fn fetch(request: HttpRequest, env: Env, context: Context) -> worker::Result<release::Served> { 91 // A release file is answered here, before the router, and as the runtime's 92 // own response, so that its length survives (`release::Served`). 93 let host = request.headers().get(header::HOST).and_then(|host| host.to_str().ok()).unwrap_or_default(); 94 if host::Host::of(host).serves_downloads() 95 && matches!(*request.method(), axum::http::Method::GET | axum::http::Method::HEAD) 96 && let Some(route) = release::route(request.uri().path()).filter(release::Route::is_file) 97 { 98 let event = events::of(&request); 99 return Ok(release::file(&env, event, route, request.method(), request.headers()).await); 100 } 101 route_request(request, env, context).await.map(release::Served::Http) 102} 103 104async fn route_request(mut request: HttpRequest, env: Env, context: Context) -> worker::Result<Response<Body>> { 105 // Each host has its own routes (`host.rs`): what an address does not 106 // serve is not there to be reached, rather than refused one by one. 107 let host = request.headers().get(header::HOST).and_then(|host| host.to_str().ok()).unwrap_or_default().to_owned(); 108 let which = host::Host::of(&host); 109 let redirect = git_redirect(&env); 110 let state = App { env: env.clone() }; 111 let routes = match which { 112 host::Host::Code => code_routes(), 113 // The site itself. With the redirect on, its repositories are at the 114 // code host and the gate below sends every path under one there; a 115 // dev server or staging, which have no code host, and the site 116 // before the redirect is switched on, serve them where they are. 117 host::Host::Home | host::Host::Elsewhere | host::Host::Other => site_routes(which.serves_git(redirect)), 118 }; 119 // Release files are the code host's alone (and a dev server's or 120 // staging's, which have no code host): never the site's addresses. 121 let mut router = if which.serves_downloads() { with_downloads(routes) } else { routes }.with_state(state); 122 // The request as an event, before it is known what came of it 123 // (events.rs). A GET's is kept here, once it has a status, after the 124 // response has gone; a POST's by its handler, which knows how it ended. 125 let headers = request.headers().clone(); 126 let event = events::of(&request); 127 let target = request.uri().path_and_query().map_or("/", |target| target.as_str()); 128 let asked = host::Asked { 129 host: &host, 130 method: request.method(), 131 target, 132 socket: headers.contains_key(header::UPGRADE), 133 own: headers.get("sec-fetch-mode").is_some_and(|mode| mode != "navigate"), 134 }; 135 match host::gate(&asked, redirect) { 136 host::Gate::Pass => {} 137 host::Gate::Moved { to, status } => { 138 let mut event = event.named("moved").with(&host); 139 event.status = f64::from(status.as_u16()); 140 context.wait_until(async move { events::record(&env, event).await }); 141 return Ok(Response::builder() 142 .status(status) 143 .header(header::LOCATION, to) 144 .header(header::CACHE_CONTROL, "public, max-age=86400") 145 .body(Body::empty()) 146 .expect("static headers are valid")); 147 } 148 host::Gate::Refused(why) => { 149 let mut response = clone::refused(axum::http::StatusCode::METHOD_NOT_ALLOWED, &why); 150 response.headers_mut().insert(header::ALLOW, axum::http::HeaderValue::from_static("GET, HEAD")); 151 return Ok(response); 152 } 153 } 154 let got = event.clone().got(); 155 request.extensions_mut().insert(event); 156 let mut response = router.call(request).await?; 157 if let Some(mut event) = got { 158 event.status = f64::from(response.status().as_u16()); 159 // A page given to a browser is a visit, and says when it was counted. 160 let page = response.status().is_success() && response.headers().get(header::CONTENT_TYPE).is_some_and(|kind| kind.as_bytes().starts_with(b"text/html")); 161 if page 162 && let Some(cookie) = event.visit(&headers, js_sys::Date::now()).and_then(|cookie| axum::http::HeaderValue::from_str(&cookie).ok()) 163 { 164 response.headers_mut().append(header::SET_COOKIE, cookie); 165 } 166 // Any page a browser is first given gives it its id, before it can 167 // ask anything, and that page's own event is the id's first: until 168 // 2026-10-03 only the home page gave one, and never said which. 169 if page 170 && event.browser.is_empty() 171 && event.client != "bot" 172 && let Some((id, cookie)) = new_browser() 173 && let Ok(cookie) = axum::http::HeaderValue::from_str(&cookie) 174 { 175 response.headers_mut().append(header::SET_COOKIE, cookie); 176 event.browser = id; 177 } 178 context.wait_until(async move { events::record(&env, event).await }); 179 } 180 Ok(response) 181}
What lmjtfy.fun serves. With git, the repositories too, as a dev
server and staging do (host::Host::Other).
185fn site_routes(git: bool) -> Router<App> { 186 let router = Router::new() 187 .route("/", get(page)) 188 .route("/datastar.js", get(datastar)) 189 .route("/emoji.woff2", get(emoji)) 190 .route("/live.js", get(live_js)) 191 .route("/icons/{name}", get(icon)) 192 .route("/card.png", get(card)) 193 .route(meatproxy::CARD_PATH, get(meatproxy_card)) 194 .route("/robots.txt", get(robots)) 195 .route("/rules", get(rules_page)) 196 .route("/rules.svg", get(rules_svg)) 197 .route("/ask", post(ask)) 198 .route("/gate", post(gate)) 199 .route("/rate", post(rate)) 200 .route("/comment", post(comment)) 201 .route("/seen", post(seen)) 202 .route("/feed", get(feed)) 203 .route("/live", get(live)); 204 if git { with_repositories(router) } else { router } 205}
What code.lmjtfy.fun serves: the repositories, a front page listing
them, and the little the code pages draw themselves with. No /ask, no
archive, no /live, and no /seen: the pages there are told not to open
sockets (Frame::live).
A repository's landing page and the pages under it, and the two requests git makes.
224fn with_repositories(router: Router<App>) -> Router<App> { 225 router 226 .route("/{repo}", get(clone::landing)) 227 .route("/{repo}/", get(clone::landing)) 228 .route("/{repo}/{*path}", get(browse::path)) 229 .route("/{repo}/info/refs", get(clone::refs)) 230 .route("/{repo}/git-upload-pack", post(clone::upload_pack)) 231}
A repository's release files (release.rs): /<name>/latest/<kind>,
/<name>/releases/<tag>/<asset> and the two pages. Everything under those
two prefixes is the handler's, so what is not a route is its 404.
236fn with_downloads(router: Router<App>) -> Router<App> { 237 router 238 .route("/{repo}/latest", get(release::serve)) 239 .route("/{repo}/latest/", get(release::serve)) 240 .route("/{repo}/latest/{*rest}", get(release::serve)) 241 .route("/{repo}/releases", get(release::serve)) 242 .route("/{repo}/releases/", get(release::serve)) 243 .route("/{repo}/releases/{*rest}", get(release::serve)) 244}
Which version of each call the page wants (archive::Pins).
What /card.png is of: a question, or with code, the code.
#[worker::send]: asking the budget object is not Send, and an axum
handler's future must be.
267#[worker::send] 268async fn page(State(app): State<App>, headers: HeaderMap, Query(asked): Query<Asked>) -> Response<Body> { 269 let budget = shared(&app.env).await; 270 let home = archive::home(&app.env).await; 271 let typed = ask::clean(&asked.q); 272 // For the link preview: what Jev said, if this was asked before. 273 let said = if typed.is_empty() { None } else { archive::answer(&app.env, &typed).await }; 274 let declined = said.is_none() && !typed.is_empty() && archive::was_declined(&app.env, &typed).await; 275 let page = view::page(&typed, said.as_ref(), declined, &origin(&headers), &host_of(&headers).code_origin(&origin(&headers), git_redirect(&app.env)), budget, home.as_ref()); 276 respond("text/html; charset=utf-8", "no-cache", page.into_string().into()) 277}
Whether the old addresses send the code to the code host (GIT_REDIRECT,
host::GitRedirect). A value that is neither on nor off is logged as
an error, on every request until it is fixed, and runs as off: the
behaviour that cannot break a clone.
283fn git_redirect(env: &Env) -> host::GitRedirect { 284 let value = env.var(host::GitRedirect::VAR).ok().map(|var| var.to_string()); 285 let (switch, complaint) = host::GitRedirect::resolve(value.as_deref()); 286 if let Some(complaint) = complaint { 287 worker::console_error!("{complaint}"); 288 } 289 switch 290}
Which of the site's addresses the request came to (host.rs).
Where this site is, for the preview's image address, which has to be whole. From the request, so the dev server previews itself.
The picture a link unfurls with: the question and what Jev said, read from the archive. It asks nothing, so a preview bot spends nothing.
307#[worker::send] 308async fn card(State(app): State<App>, headers: HeaderMap, Query(asked): Query<Pictured>) -> Response<Body> { 309 // `?code=<commit>`: the code page's picture. The commit is only there to 310 // change the address; what is drawn is the latest one read. 311 if asked.code.is_some() { 312 let repo = asked.repo.as_deref().and_then(clone::Repo::named).unwrap_or(clone::Repo::Lmjtfy); 313 let latest = clone::latest(&app.env, repo).await; 314 let url = format!("{}/{}", origin(&headers), repo.served()); 315 let meta = clone::meta(&app.env, repo).await; 316 let about = meta.as_ref().map(|meta| meta.about()).unwrap_or_default(); 317 let card = Card::Code { name: repo.served(), url: &url, recurse: !repo.submodules().is_empty(), about, branch: repo.branch(), latest: latest.as_ref() }; 318 return respond("image/png", "public, max-age=3600", ::card::png(card).into()); 319 } 320 let typed = ask::clean(&asked.q); 321 let said = if typed.is_empty() { None } else { archive::answer(&app.env, &typed).await }; 322 let declined = !typed.is_empty() && said.is_none() && archive::was_declined(&app.env, &typed).await; 323 let said = said.map(|entry| entry.answers).unwrap_or_default(); 324 let card = if typed.is_empty() { 325 Card::Home 326 } else if declined { 327 Card::Declined { input: &typed } 328 } else { 329 Card::Question { input: &typed, said: &said } 330 }; 331 respond("image/png", "public, max-age=3600", ::card::png(card).into()) 332}
/card.png at the code host: the code's picture only. A question's
reads the archive, which that host does not.
/live: a page's socket, handed to the archive, which keeps it and
tells it how many pages are open and what happens while it is.
346#[worker::send] 347async fn live(State(app): State<App>, mut request: axum::extract::Request) -> Response<Body> { 348 // Where Cloudflare placed the page, for the online list. The Worker sets 349 // these headers itself, over any the page sent, so a page cannot name its 350 // own place; the archive keeps them on the socket while it is open. 351 // Only a socket is handed on: the archive reads any other request as a 352 // message from this Worker, and a visitor's must never be one. 353 if !request.headers().get(header::UPGRADE).is_some_and(|upgrade| upgrade.as_bytes().eq_ignore_ascii_case(b"websocket")) { 354 return clone::refused(axum::http::StatusCode::UPGRADE_REQUIRED, "Not a socket."); 355 } 356 let place = request.extensions().get::<worker::Cf>().map(|cf| (cf.country(), cf.city())); 357 // Who connected, for the archive to keep (`events`). 358 let event = request.extensions().get::<events::Event>().and_then(|event| serde_json::to_string(event).ok()); 359 let headers = request.headers_mut(); 360 headers.remove(archive::COUNTRY); 361 headers.remove(archive::CITY); 362 headers.remove(archive::EVENT); 363 if let Some(event) = event.and_then(|event| axum::http::HeaderValue::from_str(&view::url_encoded(&event)).ok()) { 364 headers.insert(archive::EVENT, event); 365 } 366 headers.insert(archive::PLACED, axum::http::HeaderValue::from_static("1")); 367 if let Some((country, city)) = place { 368 for (name, value) in [(archive::COUNTRY, country), (archive::CITY, city)] { 369 if let Some(value) = value.and_then(|value| axum::http::HeaderValue::from_str(&view::url_encoded(&value)).ok()) { 370 headers.insert(name, value); 371 } 372 } 373 } 374 let request = match worker::Request::try_from(request) { 375 Ok(request) => request, 376 Err(_) => return clone::refused(axum::http::StatusCode::BAD_REQUEST, "Not a socket."), 377 }; 378 match archive::live(&app.env, request).await { 379 Ok(response) => response.into(), 380 Err(_) => clone::refused(axum::http::StatusCode::BAD_GATEWAY, "The archive is unreachable."), 381 } 382}
What is left of the day's shared budgets, for the page.
385async fn shared(env: &Env) -> Option<view::Shared> { 386 let status = meter::status(env).await?; 387 let jev_per_day = env.var(JEV_DOLLARS_PER_DAY).ok()?.to_string().parse().ok()?; 388 Some(view::Shared { status, neurons_per_day: llm::FREE_NEURONS_PER_DAY, jev_dollars_per_day: jev_per_day }) 389}
The rules as an SVG file, for the READMEs.
What crawlers are asked to leave alone. /rules with facts set is one
page per combination of facts, each linking to more: on 2026-10-03 one
crawler asked for 43,440 of them in a day. The page itself is welcome.
Cloudflare puts its own lines above these.
400const ROBOTS: &str = "User-agent: *\nDisallow: /rules?\n";
The SharedWorker that holds one /live socket for a browser's tabs. Its
address carries the build, so a deploy's pages get a new one; the script
itself does not change with it, so it may be kept.
/icons/<name>.png: one of the explorer's pixel icons
(third-party/jerrys-pixel-icons), from the table build.rs writes. They
do not change under a name, so a browser may keep one for good.
424async fn icon(Path(name): Path<String>) -> Response<Body> { 425 let found = name.strip_suffix(".png").and_then(|name| pictures::ICONS.binary_search_by_key(&name, |(icon, _)| *icon).ok()); 426 match found { 427 Some(at) => respond("image/png", "public, max-age=31536000, immutable", Body::from(pictures::ICONS[at].1)), 428 None => clone::refused(axum::http::StatusCode::NOT_FOUND, "No such icon."), 429 } 430}
/meatproxy/card: the card image of the site the LLM suggestion links
to, from here so the visitor's browser never asks that site for it.
Nothing in the request is read.
The rules with the facts the link sets. Nothing is asked of anyone.
445async fn rules_page(Query(params): Query<Vec<(String, String)>>) -> Response<Body> { 446 let network = Network::lmjtfy(); 447 let play = playground::play(&network, ¶ms); 448 let says = playground::says(&network, &play); 449 let page = view::playground(&network, &play.known, &play.fired, &says, |clicked| playground::link(&play.known, clicked)); 450 respond("text/html; charset=utf-8", "no-cache", page.into_string().into()) 451}
Who is asking, for the visitor limit: Cloudflare's own header for the visitor's address, a key in the budget object's memory for a minute.
Datastar posts the page's signals as JSON; q is the search box.
460async fn ask(State(app): State<App>, Extension(event): Extension<events::Event>, headers: HeaderMap, Json(asked): Json<Asked>) -> Response<Body> { 461 let visitor = visitor(&headers); 462 let browser = browser(&headers); 463 let pins = ::archive::Pins::parse(&asked.pins); 464 stream(move |events| answer(app, asked.q, pins, visitor, browser, event.named("answer"), events)) 465}
A vote on Jev's answer: the search box still holds the question, and the button sets which way.
How many votes a visitor may cast a minute.
477const VOTES_PER_MINUTE: u32 = 30;
/rate: a browser's vote on Jev's answer, and the votes after it, as the
rating element. A browser with no id cannot vote; a question never
answered has nothing to vote on.
482#[worker::send] 483async fn rate(State(app): State<App>, Extension(event): Extension<events::Event>, headers: HeaderMap, Json(rated): Json<Rated>) -> Response<Body> { 484 let input = ask::clean(&rated.q); 485 let browser = browser(&headers); 486 let vote = match rated.vote.as_str() { 487 "up" => Some(::archive::Vote::Up), 488 "down" => Some(::archive::Vote::Down), 489 _ => None, 490 }; 491 let rating = match (&browser, vote) { 492 (Some(browser), Some(vote)) if !input.is_empty() && meter::admit(&app.env, "rate", visitor(&headers), VOTES_PER_MINUTE).await => { 493 archive::rate(&app.env, &input, browser, vote).await 494 } 495 _ => archive::rating(&app.env, &input, browser.as_deref().map(|browser| asker(browser, &input))).await, 496 }; 497 events::record(&app.env, event.named("vote").with(rated.vote.chars().take(8).collect::<String>()).about(&input)).await; 498 let patch = PatchElements::new(view::rating(rating.as_ref(), browser.is_some()).into_string()).into_datastar_event(); 499 // The comment box is open while there is a vote to comment on. A vote 500 // changed leaves what was typed, and it may be saved again for the new vote. 501 let open = PatchSignals::new(serde_json::json!({ "open": rating.is_some_and(|rating| rating.mine.is_some()), "saved": false }).to_string()).into_datastar_event(); 502 respond("text/event-stream", "no-cache", Body::from(format!("{patch}{open}"))) 503}
A comment on the vote: the page's signals, q the search box and
comment the textarea.
How many comments a visitor may save a minute: fewer than votes, since each one is free text kept for the owner to read.
517const COMMENTS_PER_MINUTE: u32 = 5;
/comment: more words on the browser's standing vote, kept beside it
(archive::comment). Answered with the page's notes: thanks, or why not
(fixed words), and never the text. What the visitor typed is only ever
handed to the archive: it is in no log line and no event, which keep how
it ended (saved, empty, ...) and nothing else.
524#[worker::send] 525async fn comment(State(app): State<App>, Extension(event): Extension<events::Event>, headers: HeaderMap, Json(said): Json<Commented>) -> Response<Body> { 526 use ::archive::Unsaid; 527 let input = ask::clean(&said.q); 528 let browser = browser(&headers); 529 let outcome = match (&browser, ::archive::clean_comment(&said.comment)) { 530 _ if !meter::admit(&app.env, "comment", visitor(&headers), COMMENTS_PER_MINUTE).await => Err(Unsaid::Limit), 531 (_, Err(unsaid)) => Err(unsaid), 532 (None, _) => Err(Unsaid::NoVote), 533 (_, Ok(_)) if input.is_empty() => Err(Unsaid::NoVote), 534 (Some(browser), Ok(clean)) => archive::comment(&app.env, &input, browser, clean).await, 535 }; 536 let kept = match outcome { 537 Ok(()) => "saved", 538 Err(Unsaid::Empty) => "empty", 539 Err(Unsaid::TooLong) => "too-long", 540 Err(Unsaid::NoVote) => "no-vote", 541 Err(Unsaid::Limit) => "limit", 542 Err(Unsaid::Failed) => "failed", 543 }; 544 events::record(&app.env, event.named("comment").with(kept).about(&input)).await; 545 let note = PatchElements::new(view::comment_error(outcome.err()).into_string()).into_datastar_event(); 546 let saved = PatchSignals::new(serde_json::json!({ "saved": outcome.is_ok() }).to_string()).into_datastar_event(); 547 respond("text/event-stream", "no-cache", Body::from(format!("{note}{saved}"))) 548}
How many reports a visitor's pages may make a minute.
551const REPORTS_PER_MINUTE: u32 = 120;
/seen: a page's report of itself as it is left, or as a link off the
site is followed (page.js): how long it was in view, how far down, the
screen. Kept as a read or out event; the page is told nothing.
556#[worker::send] 557async fn seen(State(app): State<App>, Extension(event): Extension<events::Event>, headers: HeaderMap, body: String) -> Response<Body> { 558 if let Ok(report) = serde_json::from_str::<::archive::Report>(&body) 559 && meter::admit(&app.env, "seen", visitor(&headers), REPORTS_PER_MINUTE).await 560 { 561 events::record(&app.env, event.seen(&report)).await; 562 } 563 Response::builder().status(axum::http::StatusCode::NO_CONTENT).body(Body::empty()).expect("no headers to be wrong") 564}
The cookie that tells one browser from another, for counting each once
per question. A random id the Worker gives a browser on its first page,
kept a year. askers and ratings get only asker of it; events
keeps it as it is, which is what ties one browser's rows together.
570const BROWSER: &str = "lmjtfy_browser";
This browser's id, if it has one.
573fn browser(headers: &HeaderMap) -> Option<String> { 574 let cookies = headers.get(header::COOKIE)?.to_str().ok()?; 575 cookies 576 .split(';') 577 .filter_map(|pair| pair.trim().split_once('=')) 578 .find(|(name, _)| *name == BROWSER) 579 .map(|(_, id)| id.to_owned()) 580 .filter(|id| id.len() == 36 && id.bytes().all(|b| b.is_ascii_hexdigit() || b == b'-')) 581}
A new browser id, as a Set-Cookie value, for a browser that has none.
584fn new_browser() -> Option<(String, String)> { 585 use wasm_bindgen::JsCast; 586 let crypto = js_sys::Reflect::get(&js_sys::global(), &"crypto".into()).ok()?; 587 let uuid = js_sys::Reflect::get(&crypto, &"randomUUID".into()).ok()?.dyn_into::<js_sys::Function>().ok()?.call0(&crypto).ok()?.as_string()?; 588 let cookie = format!("{BROWSER}={uuid}; Max-Age=31536000; Path=/; HttpOnly; Secure; SameSite=Lax"); 589 Some((uuid, cookie)) 590}
The browser for one question: its id and the question, hashed together, so the archive can tell a browser asking again from a new one and can link nothing else.
595pub(crate) fn asker(browser: &str, input: &str) -> ::archive::Asker { 596 use sha2::{Digest, Sha256}; 597 let digest = Sha256::new().chain_update(browser.as_bytes()).chain_update([0]).chain_update(input.as_bytes()).finalize(); 598 ::archive::Asker(digest.iter().map(|byte| format!("{byte:02x}")).collect()) 599}
Where "asked lately" was scrolled to: the last line shown.
/feed: the page of "asked lately" after the last line shown, put before
the feed's end, and a new end. It reads the archive and asks nothing.
610#[worker::send] 611async fn feed(State(app): State<App>, Query(scrolled): Query<Scrolled>) -> Response<Body> { 612 let after = ::archive::Cursor { asked_ms: scrolled.ms, input: scrolled.q }; 613 // An archive that cannot be read ends the feed here, rather than asking 614 // again for as long as the end is in view. 615 let entries = archive::older(&app.env, after).await.unwrap_or_default(); 616 let (lines, end) = view::older(&entries); 617 let end = PatchElements::new(end.into_string()).selector("#more").mode(ElementPatchMode::Replace); 618 let mut body = String::new(); 619 if !entries.is_empty() { 620 let lines = PatchElements::new(lines.into_string()).selector("#more").mode(ElementPatchMode::Before); 621 body.push_str(&lines.into_datastar_event().to_string()); 622 } 623 body.push_str(&end.into_datastar_event().to_string()); 624 respond("text/event-stream", "no-cache", Body::from(body)) 625}
The same signals, while the visitor is still typing: the facts alone.
An event stream fed by work. Jev and Workers AI calls are not Send
(they hold JS values) and an axum handler must be, so the work runs on the
isolate's own executor and reaches the response through a channel. The
open response body is what keeps the request alive while it runs.
637fn stream<W, F>(work: W) -> Response<Body> 638where 639 W: FnOnce(UnboundedSender<String>) -> F, 640 F: Future<Output = ()> + 'static, 641{ 642 let (events, stream) = unbounded::<String>(); 643 wasm_bindgen_futures::spawn_local(work(events)); 644 respond("text/event-stream", "no-cache", Body::from_stream(stream.map(Ok::<_, Infallible>))) 645}
Sends markup to morph into the page. A closed channel means the visitor left; there is nobody to tell.
Everything a Jev call needs, built once per request.
Why no Jev call can be made at all.
674pub(crate) enum NoJev {
The Jev client. The Worker builds one to prepare requests and to know the site is online; the archive builds one to send them.
691pub(crate) fn jev(env: &Env) -> Result<Jev, NoJev> { 692 let key = env.secret(KEY).map(|secret| secret.to_string()).unwrap_or_default(); 693 if key.trim().is_empty() { 694 return Err(NoJev::Offline); 695 } 696 let var = |name: &str| env.var(name).map(|var| var.to_string()).map_err(|e| NoJev::Broken(format!("{name}: {e}"))); 697 let model = ModelId::pinned(&var(JEV_MODEL)?).map_err(|e| NoJev::Broken(e.to_string()))?; 698 let dollars_per_day: f64 = var(JEV_DOLLARS_PER_DAY)? 699 .parse() 700 .map_err(|_| NoJev::Broken(format!("{JEV_DOLLARS_PER_DAY} is not a number")))?; 701 let client = Client::new(FetchTransport::api(), WorkerRuntime, model.clone(), key.trim()) 702 .map_err(|e| NoJev::Broken(e.to_string()))?; 703 Ok(Jev { client, model, dollars_per_day }) 704}
Jev's answer to a prepared call, from the archive: the response it holds
for this exact request, or the one it gets now. Err means the call was
never made, so the page must not show it.
Which kept response, or whether to send again, is what the page's pins
say for this request (archive::Pins::pick); the version it got comes
back beside the answer.
722async fn judged_by_jev( 723 env: &Env, 724 jev: &Jev, 725 input: &str, 726 wanted: Wanted, 727 prepared: &Prepared, 728 pins: &::archive::Pins, 729) -> Result<(Outcome, Option<view::Versions>), Ending> { 730 let id = ::archive::call_id(jev_protocol::ENDPOINT, &prepared.request); 731 let ask = Ask::Jev { input: input.to_owned(), wanted, request: prepared.request.clone(), pick: pins.pick(&id) }; 732 match archive::call(env, ask).await { 733 Ok(Called::Answered(record)) => Ok(( 734 ask::read( 735 &jev.model, 736 prepared, 737 Kept { 738 body: &record.response, 739 sent: record.sent(), 740 took: Duration::from_secs_f64(record.took_ms / 1000.0), 741 attempts: record.attempts, 742 request_id: record.request_id, 743 }, 744 ), 745 Some(view::Versions { id, version: record.version, versions: record.versions }), 746 )), 747 Ok(Called::Failed { error, request_id, took_ms }) => { 748 Ok((Outcome::Failed { error, request_id, took: Duration::from_secs_f64(took_ms / 1000.0), dollars: 0.0 }, None)) 749 } 750 Ok(Called::Spent(pot)) => Err(spent(pot)), 751 Ok(Called::NotKept) => Err(Ending::NotKept { id }), 752 Err(error) => Err(Ending::Failed { error }), 753 } 754}
Asks Jev for facts about the input, all in one request, and records
what they turned out to be. This is the rules' backfill: every Jev fact
any live rule is waiting on, together.
759async fn learn( 760 env: &Env, 761 jev: &Jev, 762 view: &mut View, 763 facts: Vec<Fact>, 764 events: Option<&UnboundedSender<String>>, 765) -> Result<(), Ending> { 766 let failed = |error: String| Ending::Failed { error }; 767 let wanted = Wanted::Facts(facts.clone()); 768 let prepared = ask::wanted(&jev.model, &view.input, &wanted).map_err(|e| failed(e.to_string()))?; 769 view.facts = Some(JevCall::pending(&prepared)); 770 if let Some(events) = events { 771 show(events, view); 772 } 773 let outcome = match judged_by_jev(env, jev, &view.input, wanted, &prepared, &view.pins).await { 774 Ok((outcome, kept)) => { 775 view.facts.as_mut().expect("just set").kept = kept; 776 outcome 777 } 778 Err(ending) => { 779 view.facts = None; 780 return Err(ending); 781 } 782 }; 783 // Several facts can be read from one answer: each finds its question's. 784 let learned = match &outcome { 785 Outcome::Answered { judged, .. } => facts 786 .iter() 787 .map(|fact| { 788 let id = ask::question_id(*fact); 789 prepared 790 .parts 791 .iter() 792 .position(|part| part.id == id) 793 .and_then(|index| judged.get(index)) 794 .and_then(|judged| ask::learned(*fact, judged)) 795 .map(|value| (*fact, value)) 796 .ok_or_else(|| failed(format!("Jev's answer for {} is not the type that was asked", fact.name()))) 797 }) 798 .collect::<Result<Vec<_>, _>>(), 799 Outcome::Failed { error, .. } => Err(failed(error.clone())), 800 }; 801 view.facts.as_mut().expect("just set").outcome = Some(outcome); 802 for (fact, value) in learned? { 803 view.known.learn(fact, value); 804 } 805 Ok(()) 806}
Jev's probability that it can judge the input, once the facts are in.
816async fn answer( 817 app: App, 818 input: String, 819 pins: ::archive::Pins, 820 visitor: String, 821 browser: Option<String>, 822 mut event: events::Event, 823 events: UnboundedSender<String>, 824) { 825 let began = js_sys::Date::now(); 826 let mut view = View::new(ask::clean(&input)); 827 let browsing = pins.browsing(); 828 view.pins = pins; 829 // Every ask counts towards the visitor's limit, an empty one too. 830 view.ending = Some(if !meter::admit(&app.env, "ask", visitor, ASKS_PER_MINUTE).await { 831 Ending::Slow 832 } else if view.input.is_empty() { 833 Ending::Empty 834 } else { 835 decide(&app.env, &mut view, &events).await 836 }); 837 // The card of the suggestion's site, for the endings that make one. 838 if matches!(view.ending, Some(Ending::NotAQuestion { .. } | Ending::NoQuestion)) { 839 view.preview = meatproxy::head().await; 840 // So a link to this question can say what it is (`Card::Declined`). 841 archive::declined(&app.env, &view.input).await; 842 } 843 show(&events, &view); 844 // What was asked, how it ended and what it took. An old version looked 845 // at is an ask all the same, and says so. 846 let (sent, kept) = view.calls(); 847 event.detail = if browsing { format!("{} (browsing)", events::ending(view.ending.as_ref())) } else { events::ending(view.ending.as_ref()).to_owned() }; 848 (event.sent, event.kept) = (f64::from(sent), f64::from(kept)); 849 event.llm = if view.llm.is_some() { 1.0 } else { 0.0 }; 850 event.took_ms = js_sys::Date::now() - began; 851 events::record(&app.env, event.about(&view.input)).await; 852 // The page keeps the versions it is looking at, and no others. 853 let pinned = PatchSignals::new(serde_json::json!({ "pins": view.pinned(), "comment": "", "open": false, "saved": false }).to_string()).into_datastar_event(); 854 let _ = events.unbounded_send(pinned.to_string()); 855 // The budget and the feed are the home page's, and it is hidden while an 856 // answer is up (page.css), so neither is sent again here. An old version 857 // looked at is not what Jev says now, so it is not kept as the answer. 858 if matches!(view.ending, Some(Ending::NotAQuestion { .. } | Ending::NoQuestion)) { 859 // Declined was kept before the stream was shown, so there is a 860 // record to vote on (`archive::DECLINED`). 861 let who = browser.as_deref().map(|browser| asker(browser, &view.input)); 862 let rating = archive::rating(&app.env, &view.input, who).await; 863 patch(&events, view::rating(rating.as_ref(), browser.is_some())); 864 let open = PatchSignals::new(serde_json::json!({ "open": rating.is_some_and(|rating| rating.mine.is_some()) }).to_string()).into_datastar_event(); 865 let _ = events.unbounded_send(open.to_string()); 866 } 867 if matches!(view.ending, Some(Ending::Answered)) && !browsing { 868 // The rules say whether it may be shown: Jev judged it fit. 869 let listed = Network::lmjtfy().notes(&view.known, Note::List); 870 let who = browser.as_deref().map(|browser| asker(browser, &view.input)); 871 archive::asked(&app.env, &view.input, view.said(), view.llm.is_some(), listed, browser.as_deref()).await; 872 // The votes on the answer as it is now kept. 873 let rating = archive::rating(&app.env, &view.input, who).await; 874 patch(&events, view::rating(rating.as_ref(), browser.is_some())); 875 // A vote already cast on this answer has its box open. 876 let open = PatchSignals::new(serde_json::json!({ "open": rating.is_some_and(|rating| rating.mine.is_some()) }).to_string()).into_datastar_event(); 877 let _ = events.unbounded_send(open.to_string()); 878 } 879}
Does what the rules say until they say the query has ended. The rules
(rules::RULES) decide the order; this only carries each step out and
records what it taught.
884async fn decide(env: &Env, view: &mut View, events: &UnboundedSender<String>) -> Ending { 885 let jev = match jev(env) { 886 Ok(jev) => jev, 887 Err(none) => return none.into(), 888 }; 889 let network = Network::lmjtfy(); 890 loop { 891 let (by, next) = network.decide(&view.known); 892 view.fired.extend(by); 893 let step = match next { 894 Next::Ask(facts) => learn(env, &jev, view, facts, Some(events)).await, 895 // One call writes everything the drafting rules that hold want. 896 Next::Do(Effect::Draft(_)) => drafted(env, view, &network.wants(&view.known), events).await, 897 Next::Do(Effect::Judge) => judge(env, &jev, view, events).await, 898 Next::End(End::NotAQuestion) => { 899 return match answerable(view) { 900 Some(p_yes) => Ending::NotAQuestion { p_yes }, 901 None => Ending::Failed { error: "the rules refused an input Jev was not asked about".into() }, 902 }; 903 } 904 // Nothing is left to do. What to show is whatever the rules noted. 905 Next::Done if view.answered() => return Ending::Answered, 906 Next::Done => return Ending::NoQuestion, 907 }; 908 if let Err(ending) = step { 909 return ending; 910 } 911 show(events, view); 912 } 913}
Asks Jev every question the LLM wrote, in one request.
916async fn judge(env: &Env, jev: &Jev, view: &mut View, events: &UnboundedSender<String>) -> Result<(), Ending> { 917 let drafts: Vec<(String, llm::Draft)> = 918 view.tools.iter().filter_map(|tool| Some((tool.id.clone(), tool.draft()?.clone()))).collect(); 919 let wanted = Wanted::Drafted(drafts); 920 let prepared = 921 ask::wanted(&jev.model, &view.input, &wanted).map_err(|e| Ending::Failed { error: e.to_string() })?; 922 view.judging = Some(JevCall::pending(&prepared)); 923 show(events, view); 924 match judged_by_jev(env, jev, &view.input, wanted, &prepared, &view.pins).await { 925 Ok((outcome, kept)) => { 926 let judging = view.judging.as_mut().expect("just set"); 927 judging.outcome = Some(outcome); 928 judging.kept = kept; 929 } 930 Err(ending) => { 931 view.judging = None; 932 return Err(ending); 933 } 934 } 935 view.known.learn(Fact::Judged, Value::Bool(view.any_judged())); 936 Ok(()) 937}
The LLM's questions for the input, from the archive, as the tool calls it made. A call Jev's protocol would refuse is kept, marked, and not sent.
941async fn drafted(env: &Env, view: &mut View, wants: &[Want], events: &UnboundedSender<String>) -> Result<(), Ending> { 942 let failed = |error: String| Ending::Failed { error }; 943 let id = env.var(LLM_MODEL).map(|var| var.to_string()).map_err(|e| failed(format!("{LLM_MODEL}: {e}")))?; 944 let model = Model::find(&id).ok_or_else(|| failed(format!("{id} is not one of llm's candidates")))?; 945 let request = llm::request(&view.input, wants); 946 view.llm = Some(LlmCall { model: model.id, request: request.clone(), outcome: None, kept: None }); 947 show(events, view); 948 949 let id = ::archive::call_id(model.id, &request); 950 let pick = view.pins.pick(&id); 951 let record = match archive::call(env, Ask::Llm { model: model.id.to_owned(), request, pick }).await { 952 Ok(Called::Answered(record)) => record, 953 Ok(Called::Failed { error, took_ms, .. }) => { 954 view.llm.as_mut().expect("just set").outcome = Some(LlmOutcome { 955 body: error.clone(), 956 neurons: 0.0, 957 took: Duration::from_secs_f64(took_ms / 1000.0), 958 dropped: 0, 959 sent: ask::Sent::Now, 960 }); 961 return Err(failed(format!("the LLM: {error}"))); 962 } 963 Ok(Called::Spent(pot)) => { 964 view.llm = None; 965 return Err(spent(pot)); 966 } 967 Ok(Called::NotKept) => { 968 view.llm = None; 969 return Err(Ending::NotKept { id }); 970 } 971 Err(error) => { 972 view.llm = None; 973 return Err(failed(error)); 974 } 975 }; 976 let parsed = llm::parse(&record.response); 977 view.llm.as_mut().expect("just set").kept = Some(view::Versions { id, version: record.version, versions: record.versions }); 978 view.llm.as_mut().expect("just set").outcome = Some(LlmOutcome { 979 neurons: parsed.as_ref().map_or(0.0, |reply| model.neurons(reply.usage)), 980 took: Duration::from_secs_f64(record.took_ms / 1000.0), 981 dropped: parsed.as_ref().map_or(0, |reply| reply.dropped), 982 sent: record.sent(), 983 body: record.response, 984 }); 985 let reply = parsed.map_err(|error| failed(format!("the LLM: {error}")))?; 986 view.tools = reply 987 .calls 988 .into_iter() 989 .enumerate() 990 .map(|(index, call)| { 991 let refused = call.draft.as_ref().ok().and_then(|draft| { 992 if !llm::takes(wants, draft) { 993 // Jev has answered that reading itself, or the rules did not take it. 994 return Some(format!("a {} was not what the rules asked the LLM for", draft.tool())); 995 } 996 ask::check(draft).err().map(|e| e.to_string()) 997 }); 998 // q1, q2, ...: the question's id in its request and on the page. 999 Tool { id: format!("q{}", index + 1), call, refused } 1000 }) 1001 .collect(); 1002 let any = view.tools.iter().any(|tool| tool.draft().is_some()); 1003 view.known.learn(Fact::Drafted, Value::Bool(any)); 1004 Ok(()) 1005}
The facts alone, for the line under the search box. It is the same
request /ask begins with, so what was typed is already kept by the time
it is asked.
1010async fn glance(app: App, input: String, visitor: String, event: events::Event, events: UnboundedSender<String>) { 1011 let mut view = View::new(ask::clean(&input)); 1012 let admitted = !view.input.is_empty() && meter::admit(&app.env, "glance", visitor, GLANCES_PER_MINUTE).await; 1013 // What the feed has that this could be the start of, first: it asks 1014 // nobody anything, so it is there before Jev has answered. With 1015 // nothing typed, or nothing found, the list is emptied. 1016 let suggested = if admitted { archive::suggest(&app.env, &view.input).await } else { Vec::new() }; 1017 patch(&events, view::suggested(&suggested)); 1018 let seen = if !admitted { 1019 None 1020 } else { 1021 match (jev(&app.env), Network::lmjtfy().next(&view.known)) { 1022 (Ok(jev), Next::Ask(facts)) => learn(&app.env, &jev, &mut view, facts, None).await.ok(), 1023 _ => None, 1024 } 1025 }; 1026 let (sent, kept) = view.calls(); 1027 let mut event = event.with(if seen.is_some() { "seen" } else { "" }); 1028 (event.sent, event.kept) = (f64::from(sent), f64::from(kept)); 1029 events::record(&app.env, event.about(&view.input)).await; 1030 let glance = seen.and_then(|()| answerable(&view)).map(|answerable| view::Glance { answerable, kind: view.reading(), unfinished: Network::lmjtfy().notes(&view.known, Note::Unfinished) }); 1031 patch(&events, view::live(glance)); 1032}
1034#[cfg(test)] 1035mod tests { 1036 use super::*; 1037 1038 #[test] 1039 fn a_browser_is_the_same_asker_of_one_question_and_unrelated_across_two() { 1040 let id = "0b5f2a1e-7c3d-4e8f-9a6b-1c2d3e4f5a6b"; 1041 assert_eq!(asker(id, "is it?"), asker(id, "is it?")); 1042 assert_ne!(asker(id, "is it?"), asker(id, "is it not?")); 1043 assert_ne!(asker(id, "is it?"), asker("1b5f2a1e-7c3d-4e8f-9a6b-1c2d3e4f5a6b", "is it?")); 1044 assert_eq!(asker(id, "is it?").0.len(), 64); 1045 assert!(!asker(id, "is it?").0.contains(id)); 1046 } 1047 1048 #[test] 1049 fn only_a_well_formed_browser_cookie_is_read() { 1050 let with = |cookie: &str| { 1051 let mut headers = HeaderMap::new(); 1052 headers.insert(header::COOKIE, cookie.parse().unwrap()); 1053 browser(&headers) 1054 }; 1055 let id = "0b5f2a1e-7c3d-4e8f-9a6b-1c2d3e4f5a6b"; 1056 assert_eq!(with(&format!("a=b; {BROWSER}={id}")).as_deref(), Some(id)); 1057 assert_eq!(with(&format!("{BROWSER}=<script>")), None); 1058 assert_eq!(with("a=b"), None); 1059 } 1060}