a tiny pet with an even tinier brain, living in your browser
README.md

subtokenling shared rules #

core/ is one Rust crate, tokenling-core, holding every rule the browser and the optional Rust host both apply. The host links it natively (server/); core/wasm/ compiles it to WebAssembly for the page and its brain worker (web/core.ts). A pet therefore behaves the same in local and remote mode, and a transfer moves the same save between two copies of the same engine. train/world.py stays an independent Python rendering of the prompt contract for training; parity checks compare it with both builds.

The crate does no I/O. Callers pass the time, a random source and the deployment's weather seed. Storage, networking, Web Locks, rendering, game physics and inference stay in each runtime, which only decides when to call the engine.

What lives here #

Module Rules
src/world.rs The trained prompt contract: closed word lists, chat/diary headers, lookups ([clock], [look], [recall:…], [yesterday]), event lines, the tag grammar, NOT_CUTE, text cleaning, stop rules (stop, lookup_tail) and context fitting (fit, given the brain's token counter).
src/types.rs Save version 2 and the prompt's world state, with save validation. Unknown fields survive a round trip.
src/save.rs New saves, VERSION, MIGRATIONS (v1 → v2) and reading a stored or imported save; refusals carry newer for the bad egg.
src/calendar.rs Local date and hour. Natively: the save's hosting.timeZone (chrono-tz). In WebAssembly: the device clock (Date), so a local pet follows its owner's current zone. Moving to a host records that zone first.
src/body.rs Needs, poop, grime and sickness, collapsing and waking, growth stages, weather, the one-week catch-up cap and the alert projection.
src/turn.rs Brain requests (Generation: header, lines, prefix, budgets, temperature 0.85, top-k 40, top-p 0.92), the chat line loop with up to three lookups and three filtered tries, and diary pages (doodles, paragraphs, dropping unsafe pages).
src/engine.rs A running pet: the save plus its turn queue, disturbances, effects and accepted game. Owner commands (the host's JSON), the brain's tag effects, offers, the day log, dreams, silent-line trimming, turn scheduling (react, bedtime diary, idle, missed pages), commits and the "came back" notice.

Engine is serialized flattened into the host's stored rows, so their layout did not change when the engine moved here. In the browser the engine lives inside the WebAssembly module; web/app/engine.ts keeps a read-only snapshot in store.state, refreshing only the body fields on a plain tick.

Effects (emote, walk, want, dream, eat, bath, pat, sparkle, gift, remember, grew, sick, diary, sleep, woke) are for the LCD and notices only. The save has already changed. Local changes and host snapshots play them through the same playEffect().

Build and checks #

Use Rust 1.88+. The browser build needs the wasm32-unknown-unknown target and wasm-bindgen-cli at exactly the version core/wasm/Cargo.lock pins; core/wasm/build.sh checks both and fails with the fix. Concurrent builds of one checkout wait for each other where flock exists, and the output directory is replaced by a rename.

rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli --version 0.2.129 --locked
core/wasm/build.sh                       # core/wasm/pkg: module, ES glue, .d.ts (out of Git)
cargo fmt --check --manifest-path core/Cargo.toml
cargo fmt --check --manifest-path core/wasm/Cargo.toml
cargo test --locked --manifest-path core/Cargo.toml
node test/parity.mjs                     # WebAssembly build vs train/world.py
cargo build --locked --manifest-path server/Cargo.toml --bin tokenling-contract
node server/tests/parity.mjs             # native build vs Python, then native vs WebAssembly replay

site/build.sh runs core/wasm/build.sh and copies tokenling_core_bg.wasm into the content-hashed app/<hash>/ directory beside main.js and brain.js. Both bundles load it from there, it enters the app hash, and the page lists it for offline use. The page shows an error instead of the app if the module cannot load.

server/tests/parity.mjs replays a 31-step scenario natively (through tokenling-contract) and in WebAssembly with the same random draws. It covers chat with lookups and a filtered retry, offers, catch, an absence, sleep, dreams, both diary paths and growth. It runs in UTC and in a zone with a daylight-saving change, and requires identical engine JSON after every step.

Size and speed (2026-10-10) #

Retained upstream receipt, measured on one desktop with Node.js 24 before this subtokenling integration; not new integration, phone or download measurements. The release module is 454,762 bytes (167,674 with gzip). Node instantiated it in about 2–3 ms. Reading a deliberately large state (240 chat lines, 60 memories, 365 diary pages) took about 1.0–1.4 ms; the body-only patch read per tick about 0.01 ms, and one body advance about 0.01 ms (averages of 50 runs, three runs).

The build is reproducible across checkouts: core/wasm/build.sh remaps the checkout, Cargo registry and toolchain paths that panic locations embed, and the release profile strips symbols. Builds of one commit from two different checkout paths produced identical module, glue and declaration files.