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.