# Testing
| Check | Command | Catches |
| -------------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Type checking | `npm run typecheck` | strict checking of every TS/TSX component, controller, canvas renderer and worker |
| Shared rules build | `npm run core` (`core/wasm/build.sh`) | Rust/wasm-bindgen toolchain mismatch; produces `core/wasm/pkg/` for every check below |
| Shared rules (native) | `cargo test --locked --manifest-path core/Cargo.toml` | save reading/refusal, stop and fit rules, the lookup line loop and filtered retries, offers, scheduling (react, bedtime and missed diary, idle), catch-up |
| Engine, body and saves | `node --test test/*.test.mjs` | the WebAssembly build's body rules, offers, sleeping answers and wake rolls, turns and lookups, silent lines, the owner's return, v1-to-v2 local migration, bad eggs and ownership/reset write guards |
| Prompt parity | `node test/parity.mjs` | drift between the core's WebAssembly build and Python in headers, stat words, word lists, look/clock/recall, event lines and offers |
| Rust host | `cargo test --locked --manifest-path server/Cargo.toml` | private access, background turns, both transfers, stale replies, deduplication, persistence, body and sleeping offers |
| Native serving (opt-in) | release `--test native -- --ignored --test-threads=1`, with explicit runtime/model/profile/output env | real CPU/CUDA parity, near-limit prompts, lookup continuation, concurrent private HTTP and in-flight transfer fencing |
| Actual startup fallback (opt-in) | `python3 test/host-startup.py --help` for required inputs | missing CUDA provider keeps real CPU serving, authentication, CORS, no-store and transfers |
| Native parity and replay | build `tokenling-contract` and the WebAssembly rules, then `node server/tests/parity.mjs` | native/Python headers, lookups, 15 lists and wake probabilities across 300 states; a 31-step engine scenario replayed natively and in WebAssembly with identical JSON after every step, in UTC and in a daylight-saving zone |
| Model behavior | `train/eval_pet.py CKPT 40` | invalid tags, unresolved lookups, unanswered offers (including after an opener that skipped the answer), decisions that ignore the tummy, chatty idle turns, diaries that do not end |
| Quantization | `train/quant_eval.py CKPT EXPORT data.jsonl --dtype q4` | Masked held-out loss, EOS and bits/byte for q4/q8/fp32 |
| Experiment acceptance | `python3 test/experiment_rules.py` | Finite measurements, provenance, safety regression rejection and Pareto tradeoffs |
| Semantic boundaries | `python3 test/semantic_rules.py` | Frozen scene isolation, wrong names/numbers/roles, hidden answers, real persistence, interrogative/negated prose and source-bound independent review; native integration cases explicitly skip until `tokenling-contract` is built |
| Data rules | `python3 test/data_rules.py` | sleep/offer rules, corrupt or shifted supervision, equivalent span coverage, diary masks, no-wake/mixed migration and a stand-in sleep teacher |
| Page contract | `node test/contract.mjs` | host mount/config, component ids, documented config keys and accidental imperative rendering |
| Site build | `site/build.sh /tmp/site-out` | unfilled placeholders, a config block that is not valid JSON |
| Script lifecycle | `python3 test/site_tools.py` | failed-build preservation, publication retry/immutability and preview runtime refresh (builds the shared rules once first; a timed-out script is killed with its children) |
| Brain speed and memory | `/bench.html` in every site build | load time, decode tokens/s, prefill time, page memory |
| Browser flow | `playwright-cli` against `test/dev-site.sh` or the live URL | everything the visitor sees |
Run `npm ci` first (Node.js 24+). `npm run format` formats the frontend and
new build/test scripts; `npm run format:check` checks without writing.
`npm test` builds the shared rules to WebAssembly, then runs strict type
checking, prompt parity, the component/host contract, Node's built-in
engine/body/save tests, Python data/experiment rules and script lifecycle checks. It needs
the Rust toolchain from [core](../core/README.md). Node executes the erasable
TypeScript in the pure modules directly and loads the module with
`initCoreSync()`; no separate test transpiler or browser framework is required.
Real browser checks use the `playwright-cli` skill. A repeatable regression
flow, including a real model download, is available for a **fresh, disposable
persistent profile**:
```sh
playwright-cli -s=pet open http://127.0.0.1:8765/pet/?debug=1 --profile=/tmp/pet-test-profile
playwright-cli -s=pet run-code --filename=test/browser-flow.js
```
It checks edited/cleared hatch names, chat, care offers, healthy medicine,
shelf/memory rendering, real diary generation, catch pointer/keyboard controls,
tab keys, all panels at 1280/768/360 px, cached reload and Web Lock takeover.
The shelf/gift and catch setup use explicit debug tags to make UI assertions
independent of model sampling. Use only a test profile: it creates a pet and
changes its state. Error recovery, imports, offline use and upgrades have
additional recipes below.
For the optional Rust host, start it with the configuration in
[its guide](../server/README.md), allow the preview's browser origin and set
`REMOTE_ENDPOINT` when building the preview. Use another fresh disposable profile:
```sh
playwright-cli -s=host open http://127.0.0.1:8765/pet/?debug=1 --profile=/tmp/host-test-profile
playwright-cli -s=host run-code --filename=test/hosting-flow.js
```
This flow deliberately loses an activation acknowledgement, checks remote
creation without a local model download, closes the app page for 20 seconds,
transfers in both directions with the real brains, and recovers during a
simulated outage. On loopback it uses a second hostname as a stand-in for
another host, verifying that the old pending stop/key survives the move.
It checks 1280/768/360 px and preserves newer recovery-branch state when the
stop receipt arrives. Injected network failures produce expected browser
resource errors. Test profiles contain private keys: keep them outside Git.
`test/fixtures/save-v1.json` is an actual browser export from the previous
vanilla app at commit `6c99c23`, captured with generic Sam/Mochi names on
2026-10-07. The save tests
require the current v2 migration to preserve every original pet value while
adding local hosting metadata. The earlier Preact rewrite kept v1.
`test/site_tools.py` uses Python's standard library and the actual shell
scripts with disposable model/runtime stand-ins. It needs no downloaded
weights, teacher, GPU or browser. It exercises failed release/config/render
builds without destroying old output, runtime-publication failure followed by
same-id retry, immutable artifacts and permission overrides, and repeat
previews over HTTP, including obsolete runtime removal and `--rebuild`.
The dated upstream receipts below are retained historical evidence, not checks
run for this subtokenling cloud integration or for our custom models. In
particular, upstream CUDA/HTTPS deployments do not describe this checkout's
hardware or deployment. New integration evidence is recorded separately.
## Subtokenling cloud integration check receipt (2026-10-10)
Integrated upstream `cloud-pet` through `254f1c5` with subtokenling branding,
no analytics, namespace compatibility and the preserved private experiment
pipeline. These are new integration checks, separate from inherited receipts.
- `npm test`: strict types; 300 states / 15 word lists with **0 mismatches**;
host/component contract; 22 Node engine/save tests; 25 data-rule, nine
experiment-acceptance and seven script-lifecycle cases passed.
- Native Rust core tests passed. Native/Python parity had 300 states / 15
lists with **0 mismatches**; the 31-step engine replay was identical in
native and WebAssembly builds in UTC and America/Los_Angeles.
- Real Chromium 153 with disposable profiles ran `test/browser-flow.js`:
download/hatch, chat, offers, memory/shelf, actual diary, pointer/keyboard
catch, keyboard tabs, cached reload and Web Lock takeover. Panels fit
1280/768/360 px; LCD/setup/settings captures were inspected.
- `test/hosting-flow.js` exercised real native/browser inference, lost
activation acknowledgement, body progression with the page closed, both
transfer directions, checkpoint recovery, eventual stopping of the old
host and moving to another loopback host without losing the pending stop.
Identity survived; no uncaught browser errors occurred. Local remained
the setup default and remote creation downloaded no browser model.
- The full local/hosting flows used the **fresh GPT-2 experiment control**,
not inherited v6. The published v6 graph was unavailable locally at the
time of this receipt; these flows do not establish inherited-v6 behavioral
parity or replacement quality.
- Production app workers loaded and generated real hatch turns for GPT-2,
reduced-vocabulary Pythia-31M/8K and Llama/GQA, each in q4/q8/fp32 (nine
graphs). Wire headers and colon-only prefixes survived. Pythia q8 also
reloaded and generated offline with isolation and its saved pet intact.
GPT-2 fp32 first hit Chromium's ephemeral-profile cache-write limitation;
the persistent-profile run cached/loaded/generated without warnings. Other
completed custom runs had no uncaught errors or warnings.
- The PWA cache flow preserved legacy payload bytes/headers, current-entry
precedence and sent alert flags; refreshed install metadata online, retained
it offline and rejected an unavailable manifest instead of serving HTML.
Browser checks also retained exact newer-save bytes across reload and kept
an unreadable current save authoritative over a readable legacy one at
360 px. The flow now waits for the replacement worker's completed activation,
not the earlier controller-change event, before inspecting migrated caches.
- Native CPU generation across the same nine graphs matched eight greedy
tokens/text from the independent Python ONNX cache runner. GPT-2 vocabulary
50,257, Pythia/Llama vocabulary 8,192 and Llama's two KV heads were actual
config/graph inputs, not baseline constants. A declared 32-token native
context trimmed history and rejected an oversized header rather than
exceeding the positional budget.
- Private HTTP startup checks with GPT-2 q4, Pythia q8 and Llama q8 used a
CPU-only ONNX Runtime 1.30.0 library: requested unavailable CUDA left CPU
serving; real replies, authentication, exact CORS, no-store, local transfer
and deletion passed. Native CPU used two intra-op threads on an i9-13900K;
these are functional observations, not speed or capacity measurements.
No weights were trained, selected, published or renamed. The release still
selects immutable `tokenling-v6`; small-model behavioral/cache/quantization
failures in the screening receipt remain disqualifying. No deployment, native
CUDA/ROCm serving, physical phone, Firefox download, real install prompt,
background notification delivery or production sustained-capacity claim is
made by this integration.
## Preact migration check receipt (2026-10-07)
Measured in headless desktop Chromium against a local build, with the real v6
model and vendored runtime; these are functional checks, not new speed results.
- `npm test`: strict types, 300 parity states/15 lists with zero mismatches,
component/host contract, eight body/save tests and ten data-rule tests passed.
`npm run format:check`, clean `npm ci`, root and subpath builds also passed.
- `test/browser-flow.js` passed: fresh download/hatch, chat, food and healthy
medicine offers, memory/shelf, real diary generation, pointer/keyboard catch,
all panels at 1280/768/360 px, cached reload and tab takeover. The regular
flow had no browser console errors or warnings. LCD screenshot inspected.
- Loaded a save from the previous vanilla app; name, owner and transcript
survived. A v99 save stayed untouched after rendering/ticking. Valid save
import, export and hard reset worked in a disposable profile.
- A deliberately missing worker script showed the retry control at 360 px;
retry loaded the real brain and preserved the egg and edited name.
- Upgrade dialog/download/swap/cache cleanup passed using two local aliases of
the same v6 export (a stand-in for two compatible releases). The pet survived
and only the selected model's files remained. This does not establish v5/v6
prompt compatibility; the release's `older` list remains empty.
- With the preview server stopped, reload loaded the cached page, both app
bundles, runtime and brain; isolation stayed enabled and a real chat reply
completed. Static explanations/build details were visible with JavaScript off.
- Not re-tested for this rewrite: physical phones, Firefox's throttled download,
actual install prompts, background alert delivery, or the 90-second
one-thread fallback. Their existing logic remains in the typed controllers.
## PR #1 integration check receipt (2026-10-07)
The fixes were integrated into the Preact/TypeScript app and checked in
headless desktop Chromium against a local build with the real v6 brain.
- Formatting, strict types, 300 parity states/15 lists with zero mismatches,
the page contract, eight body/save tests, 20 data-rule tests and four script
lifecycle tests passed. Root and subpath builds passed.
- `test/browser-flow.js` passed, including real download/generation, diary,
care, catch controls, cached reload, tab takeover and all panels at
1280/768/360 px.
- Controlled-clock checks applied a three-hour awake gap, a one-minute gap,
a half-hour sleeping gap and a ten-day absence exactly once. The ten-day
absence matched the seven-day simulation cap.
- Blocked session storage still booted at 360 px. Injected worker-constructor
and initial-message failures released the worker and showed retry; restoring
the native API loaded v6 while preserving the egg and edited name. No
uncaught errors occurred in these recovery checks.
- These checks did not retrain a model or remeasure speed. Physical phones,
Firefox, offline reload, upgrades, alerts and the watchdog stall fallback
were not re-tested for this merge.
## Optional Rust host check receipt (2026-10-09)
Measured in headless desktop Chromium against a local preview with the real
v6 q4 export. Native CPU used eight threads; the browser used its normal WASM
loader. These are functional checks, not new speed or capacity measurements.
- `npm test` and formatting passed: strict types, 300 JS/Python states and
15 lists with zero mismatches, page contract, eight body/save tests,
20 data-rule tests and four script lifecycle tests. Root and subpath builds
passed. The real v1 fixture migrated to v2 without losing original values.
- Seven Rust tests passed: private access, restart persistence, deduplicated
care, complete-save handoffs, old generation/prepared activation fencing,
sleeping offers/body rules, memory eviction and scripted GPU failure
fallback. Rust/Python parity also had 300 states, 15 lists and zero mismatches.
- `test/browser-flow.js` passed with real local generation, care, diary,
catch controls, cached reload and tab takeover. No unexpected browser
console errors or warnings appeared in the regular flow.
- `test/hosting-flow.js` passed: remote creation without a local model
download; a committed activation whose response was deliberately lost;
body progression with the app page closed; both transfers retaining
name, owner, traits and birth time; offline checkpoint recovery; moving to
another host while the previous host was unreachable; eventual stop without
overwriting newer memories. The alternate host used a loopback hostname
alias and a separate hosted record, rather than another physical machine.
- A separate real native-brain check produced autonomous turns with the
page closed, including walking, emotes, sleep and a dream. API tests use
scripted generators to establish timing and fencing deterministically.
- All panels and hosting controls fit 1280/768/360 px without horizontal
overflow. [Setup](assets/cloud-pet-setup.png) and
[LCD](assets/cloud-pet-lcd.png) screenshots were inspected.
- Not measured: physical phones, native CUDA performance/integration,
simultaneous CPU/GPU capacity, public-host deployment, ATProto sign-in or
multiplayer. No production model/runtime artifact was changed.
## Native CUDA and mixed-serving check receipt (2026-10-09)
Measured with the same immutable v6 q4 file, native ONNX Runtime 1.30.0
(CUDA 13 build) and an RTX 3090, CPU intra-op threads 8, GPU launch threads 1,
inter-op 1 and TF32 disabled. These are brief warm qualification runs with
explicit greedy sampling, not production stochastic or sustained capacity.
- Eight ordinary Rust tests passed, including CPU/background and
GPU/reactive preference with serialized lanes and GPU-failure fallback.
The opt-in native HTTP test passed in CPU, GPU-only and mixed profiles.
- Three real prompt cases had 64, 66 and 976 input tokens and 15, 3 and 15
output tokens. Five repetitions per case/profile matched CPU/CUDA text,
stop reason and token counts. The last case has 991 total tokens; this is
near-limit validation, not a full 1024-token acceptance measurement.
- The private HTTP test ran simultaneous bursts of 1/4/8/16/32 pets, three
rounds each. All 183 turns in each profile completed and parked their
complete saves without failures. Mixed runs recorded overlapping CPU/GPU
calls for every multi-pet burst. Requests queue behind one session per worker;
this does not mean 32 generations run in parallel.
- Actual model-emitted clock lookup/continuation and a handoff during native
inference passed on both devices. Cancellation discarded the stale turn
and kept the GPU worker available. Ordinary tests cover GPU error requeue;
a hardware failure was not induced on the shared GPU.
- CUDA KV values now stay on-device through I/O binding. Only logits return
for sampling. The earlier host-returned-KV candidate was slower, especially
near the context limit; raw timings belong in deployment qualification
receipts, with the machine and thread count recorded.
- `test/host-startup.py` passed first with a stand-in and then with the
actual server, CPU-only ORT and requested CUDA. CPU serving remained ready,
produced a real reply and passed authentication, CORS/no-store, local
transfer and deletion. The private test database stayed outside Git.
- Formatting, types, both prompt parity suites, page contract, eight Node
tests, 20 Python data-rule tests and four script lifecycle tests passed.
Frontend browser flows were not repeated for this native-only change;
their preceding receipt remains above.
- This was one GPU-only/mixed run order, with short identical seven-token
HTTP replies. It does not establish long-response, mixed-length fairness,
batching, many-minute steady load, public-host operation or physical-phone
behavior. Small bursts can finish sooner on GPU alone because CPU work
adds a slower tail; cost-aware routing remains follow-up work.
See [the native-test recipe and variables](../server/README.md#checks-and-source-map).
## Queue-cost routing: CPU check receipt (2026-10-09)
The routing candidate keeps nine whole-turn timing estimates per worker and
plans completion over queued work. Unit cases cover avoiding a late CPU tail,
larger queues using both lanes, unknown/busy shapes, unavailable GPU, and
background aging across context edits. Four pure routing tests and ten host
tests pass, including filtered diaries keeping a healthy GPU available.
The real CPU test passed 1/4/8/16/32-pet short and varied-length bursts,
three rounds each, followed by a 120-second owner-traffic window with eight
pets at production temperature 0.85. All pets progressed and final handoffs
completed. Controlled bursts disable chatter so extra idle turns do not enter
the latency comparison. The steady phase permits ordinary diary/cancellation
behavior. This receipt validates CPU and rules; the GPU and mixed receipt
follows. It is not a serving-capacity claim.
## Queue-cost routing: GPU and mixed receipt (2026-10-10)
Measured with the same immutable v6 q4 file and native ONNX Runtime 1.30.0
(CUDA 13 build) on an RTX 3090: CPU intra-op threads 8, TF32 disabled, KV on
the device. Each window ran two profiles back to back, with greedy bursts of
1/4/8/16/32 pets (short and varied lengths, three rounds) and then 120 seconds
of eight-pet owner traffic at temperature 0.85. Every profile passed its
transfer, deletion and failure assertions, with no failed or cancelled steady
calls; within each run, steady turns per pet differed by at most two.
Medians over the three rounds:
| Profile, position in window | Short 4 | Varied 4 | Short 32 | Varied 32 | Steady turns | Steady p50/p95 |
| ------------------------------ | ------: | -------: | -------: | --------: | -----------: | -------------: |
| GPU-only, first | 117 ms | 207 ms | 932 ms | 1,641 ms | 1,459 | 604/724 ms |
| Mixed, previous router, second | 146 ms | 1,081 ms | 926 ms | 1,586 ms | 1,309 | 672/905 ms |
| Mixed, current router, first | 118 ms | 221 ms | 866 ms | 1,663 ms | 1,447 | 613/726 ms |
| GPU-only, second | 118 ms | 217 ms | 981 ms | 1,684 ms | 1,306 | 667/921 ms |
The previous router sent a long prompt to the idle CPU in every varied
4-pet round (about 1.07 s against about 0.1 s on the GPU), because that shape
had only ever run on the CPU. One unusual early sample (a two-call clock turn)
also kept a GPU estimate high long enough to send a short 4-pet turn to the
CPU. The current router averages the first five samples and estimates a shape
seen on only one worker from shapes both have run. Two unit tests reproduce
these measured cases. After a restart, each new shape still costs one CPU
sample: the first varied 4-pet round took 1,211 ms; the next two took
221 and 204 ms. In steady traffic, the CPU ran 23 of 1,929 calls, all in
the first 7.3 s as conversations grew into new shapes. From 8 pets up, mixed
bursts matched GPU-only or finished up to 8% sooner. At 32 varied pets, the
GPU always finished last, and the difference was within the round-to-round
spread (1,617–1,768 ms).
Position in the window mattered more than the profile. The second profile ran
with this card at 75–78 °C under software thermal slowdown. Median GPU time
per generated token on long steady prompts rose from 5.84–5.85 to 6.08–6.18
ms, and it completed about 10% fewer steady turns, whether GPU-only or mixed.
Compare profiles only at the same position. A first-position GPU-only run on a
second RTX 3090 without thermal slowdown completed 1,478 steady turns (5.85 ms
per token), with bursts within 4%. During the current-router window, an
unrelated CPU-heavy process shared the host. These are two short windows on
one host. They are not sustained or production capacity and do not include
phones.
## Private-network HTTPS deployment receipt (2026-10-10)
The CPU-only host ran as a hardened system service behind an HTTPS reverse
proxy on a private network, with the app served from a different origin on
the same network. Four intra-op threads, no GPU, the immutable v6 graph and
native ONNX Runtime 1.30.0 (CPU build). Health, `no-store`, exact-origin CORS
(a foreign origin's preflight received no allow-origin header) and `401` for
an unauthenticated pet read were checked through the proxy.
`test/hosting-flow.js` passed against the deployed page and host in 31 s.
The loopback-only alternate-host step does not apply to a real hostname and
was skipped. A separate remote pet produced four host-written lines (walks,
emotes, a want) in 63 s with no app page open, and the reopened page synced
them. Panels fit at 1280, 768 and 360 px. Both test pets were deleted through
the API, leaving no pet or operation rows. The service used about 0.6 GiB of
memory. One deployment; no load, multi-day or phone checks.
## Shared rules migration check receipt (2026-10-10)
The rules the browser and the host both apply moved into `core/` (one Rust
crate, native in the host and WebAssembly in the page and worker). It replaced
the browser's TypeScript prompt and body modules, the host's own save/prompt
modules and both runtimes' copies of care, tags, turns, diary and save rules.
The earlier isolated WebAssembly proof (2026-10-09) is superseded and was
removed. Measured on one Linux desktop: headless Chromium, the real v6 q4
export, native CPU ORT with four threads. These are functional checks, not
speed measurements.
- `npm test`: core build, strict types, 300 parity states/15 lists with zero
mismatches through the WebAssembly build, the page contract, 13 Node
engine/body/save tests (the real v1 fixture migrates losslessly), 20
data-rule tests and 4 script lifecycle tests. Formatting checks passed.
- `cargo test` for `core/` (6 tests) and `server/` (17 tests, host tests
unchanged in substance) passed; the release `native` test compiled.
`node server/tests/parity.mjs`: 300 native/Python states and 15 lists with
zero mismatches, and the 31-step engine replay identical in both builds in
UTC and America/Los_Angeles (across the November DST change).
- `test/browser-flow.js` passed against a local preview: fresh download,
hatch and real generation, chat, food and healthy-medicine offers,
memory/shelf, a real diary page (paragraphs kept), catch controls, keyboard
tabs, all panels at 1280/768/360 px, cached reload and Web Lock takeover.
No browser console errors or warnings.
- `test/hosting-flow.js` passed in 32 s against that preview and a local
release host with the real native CPU brain: remote creation with a lost
activation acknowledgement and no local model download, closed-page body
progress, both transfer directions preserving identity, offline recovery,
a second (loopback alias) host and eventual stop, and 1280/768/360 px. The
only console errors were the flow's injected network failures.
- Builds of the same commit from two checkout paths (one through a symlink)
first produced different modules: Rust embedded absolute source paths.
After remapping paths and stripping symbols, both produced identical
files, and the checks above passed again on that build.
- Not re-tested for this change: physical phones, Firefox, install prompts,
background alert delivery, upgrades between two releases, the 90-second
one-thread fallback and CUDA serving.
## Preview server
```sh
export MODEL=$DATA/release/tokenling-v6 RUNTIME=$DATA/release/runtime # from export.sh and vendor.sh
test/dev-site.sh /tmp/dev-site & # stages and serves http://127.0.0.1:8765/pet/
test/dev-site.sh /tmp/dev-site --rebuild # after editing web/ TypeScript/components or the page
```
The script writes a release description for the preview and runs
`site/build.sh` (the same build as a real deploy, including `test/bench.html`),
symlinks the exported model, copies the runtime, and serves everything with
the COOP/COEP headers, so `crossOriginIsolated` is true and WASM threads work.
A full run refreshes the local runtime from `RUNTIME`, removing obsolete files
only after the new copy succeeds. `--rebuild` changes page/app files only and
leaves the staged runtime and models alone.
Options are environment variables, documented in the script header: `PORT`,
`BASE` (the URL base path, default `pet`), `MODEL_ID`, `OLDER` (export
directories staged as older brains), `NEWS`, `FEATURES`, and `TRAINED` and
`SOURCE` (text for the page's details). `DTYPE=q4|q8|fp32` (default q4)
selects the preview graph for both `MODEL` and every `OLDER` directory; each
must contain that graph. The benchmark defaults to the release precision. `REMOTE_ENDPOINT` supplies the optional
default host address. `ISOLATE=0` serves without the headers for A/B tests.
`ONNX_BYTES_PER_SECOND` (default `0`, unlimited)
throttles only `.onnx` responses, including URLs with a query string, for
slow-download checks; other files retain normal speed.
**Speed and memory:** open `http://127.0.0.1:8765/pet/bench.html` and read
`window.res`. Stop the owned preview process when done.
### Small-brain runtime matrix
Use the private model tree, never a published directory. With the preview
already running, stage every completed experiment export without copying
weights or overwriting an existing directory:
```sh
python3 train/experiments.py stage --run "$DATA/full-run" \
--out "$PREVIEW/pet/models/experiments"
# Optional isolated browser tooling; not a frontend dependency.
"$EXP/bin/python" -m pip install playwright==1.63.0
"$EXP/bin/python" -m playwright install chromium
"$EXP/bin/python" test/experiment_bench.py \
--url "http://127.0.0.1:8765/pet/bench.html?model=experiments/pythia31-v8k-q8" \
--export "$DATA/full-run/pythia31-v8k/q8" --dtype q8 \
--out "$DATA/pythia31-v8k-q8-browser.json" \
--threads 1,2,4 --qdq 0,1 --prepack 0,1 --repeats 3 --warmup 1
```
Change the model/dtype together for each export. Registry model IDs have at
most two components (`namespace/name`); the staging command uses flat
`ID-DTYPE` names. The driver verifies that the served quantization receipt's
graph hash/bytes match `--export`; a different model is a failed cell.
Each cell gets a fresh Chromium context and 360 px page. Reports retain the
browser version, host, graph identity, actual isolation/overflow, settings,
each repetition and errors; a completed matrix can contain failed settings.
The CLI returns nonzero if any cell fails. A failed setting is never a speed
result or a reason to silently fall back. Run timings sequentially with other
training/inference jobs stopped; keep load time separate from generation.
The benchmark uses short-idle and long-react wire prompts, greedy and sampled
turns, normal decoded stop/EOS/budget rules, app-written lookup fixtures and
the real text filter/retry limits. `full_turn_ms` includes tokenization,
prefill, decoding and lookup/retry work. `decode_tps` excludes every prefill;
first-visible timing excludes filtered text and can be null. A quick/EOS-only
turn is not evidence of useful pet behavior: inspect actual text and qualify
behavior separately. The cross-scenario timing summary is a convenience, not
a replacement for comparing each scenario and generated-token/byte count.
Queries accept `threads`, `qdq`, `prepack`, `dtype=q4|q8|fp32`, `model`,
`device=wasm|webgpu`, `repeats`, `warmup`, `timeout` (milliseconds),
`prompts=short|long|both`, `mode=greedy|sampled|both` and `seed` (unsigned
32-bit, default 0; also driver `--seed`). Each turn reseeds the pinned runtime
RNG from the seed and stable scenario/mode/repetition ID, so early retries do
not change later draws. This does not claim Torch/JavaScript sample identity.
A page/worker watchdog
makes failures explicit. Private benchmark assets remain same-origin; the
4.3.1 HTTP metadata registry uses this static origin as `remoteHost`, not
Hugging Face or an inference service. This does not alter `web/brain.ts`.
`--memory` calls `window.mem()` while the actual model worker remains resident.
It uses `performance.measureUserAgentSpecificMemory()` for page + worker MB,
not VRAM; unavailable measurements remain an explicit error/null, never zero.
On Linux, `--memory` also records `process_memory`: actual `/proc/PID/smaps_rollup`
PSS/RSS for processes reported by this freshly launched browser's CDP connection,
while the model worker is resident. This is a different measurement, not a
replacement value in the page-memory field: it includes browser/renderer/GPU
host/network processes reported by CDP; unlisted utilities may be excluded.
RSS can double-count shared pages; PSS apportions them. Neither measures GPU
VRAM. Permission/race failures remain separate errors, not partial totals.
Use a new browser per memory probe to avoid retaining previous settings.
`--device webgpu` is an independent experiment, not a shipping engine change.
Absent, unidentified or software/fallback adapters cannot support a hardware
GPU timing claim. An adapter probe alone does not prove every kernel ran on
that GPU. Physical phones and the complete app/offline/upgrade flow still
need their own checks before publication.
For optimized sessions, separately run `eval_pet.py --onnx EXPORT --onnx-dtype
DTYPE --onnx-qdq --onnx-prepack` and `check_onnx.py --qdq --prepack` with the
same full suite/seed as the control (the shipped matrix uses `--seed 0`;
the standalone evaluator's legacy default is 123). Omit either session flag
when disabled. Pass
new report mappings via `experiments.py compare --behavior ... --cache ...`;
comparison matches behavior, cache and timing flags rather than qualifying a
fast fusion setting with a differently configured model's behavior.
### Phase-aware app and benchmark memory
Use a preview configured for the exact export/precision and new external output:
```sh
"$EXP/bin/python" test/experiment_bench.py --profile-memory --surface both \
--url "$PREVIEW_URL/bench.html" --app-url "$PREVIEW_URL/" \
--export "$EXPORT" --dtype q4 --threads 4 --qdq 0 --prepack 0 \
--sample-ms 100 --timeout-ms 120000 --out "$DATA/memory-profile.json"
```
Unlike the ordinary timing matrix, each surface/settings cell launches its own
owned Chromium process and disposable profile. App profiling requires the
explicit `debug=1&profile-memory=1` private path, installs only a synthetic pet,
and submits normal UI chat messages through the real shared engine, scheduler,
filter, lookup loop and model worker. Its kid-stage age is kept consistent to
avoid injecting an unsolicited growth reaction. The benchmark runs its existing
sampled-turn implementation. Production defaults and generation decisions are
unchanged; private worker handshakes retain scalar shapes, never live tensors.
Checkpoints are empty page, runtime imported without a session, loaded/warmed,
short turn, long turn, after 1/10/50 normal turns, and after worker termination.
Linux process PSS/RSS is sampled throughout asynchronous work at a requested
cadence (`elapsed_ms` and measurement overhead expose actual spacing). The
termination observation window defaults to one second (`--termination-ms`).
CDP main-page heap metrics explicitly exclude worker/WASM allocations; supported
page-memory APIs and their errors stay separate. Heap APIs may trigger GC, and
no forced collection or true peak is claimed. Report **maximum sampled PSS**,
checkpoint ranges, process scope, actual fitted prompts and observed cache
dimensions, not graph size as RAM or one snapshot as a stable allocator cost.
The worker's actual graph-resource URL is retained before termination. A fresh
fetch/hash verifies its bytes only **after every workload, termination and
memory sample**. The report keeps source/settings provenance, raw traces,
per-turn workload hashes, shape/last-forward KV payload summaries and explicit
coverage limits. Actual app clock/weather/body effects and unmodified RNG mean
its output trajectory is not identical to the static, independently seeded
benchmark. Compare those surfaces to locate hypotheses, not to assert causality.
`--profile-yield-between-turns --surface benchmark` is one private scheduling
diagnostic: it inserts a task boundary between otherwise unchanged sampled
normal turns. It changes no token settings, allocator, fixed-token workload or
production default. Compare exact prompt/generated-ID workload hashes and
repeat fresh-process pairs before interpreting memory differences. Mixed pairs
are not evidence of a saving, a leak or permission to rewrite the engine.
See the separately dated [quality-first receipt](training.md#quality-first-execution-receipt-2026-10-16).
### Speed and quality ranking
The [2026-10-11 receipt](training.md#speed-and-quality-benchmark-receipt--2026-10-11-utc)
records the historical all-18-variant pilot. The
[2026-10-13 compact improvement receipt](training.md#compact-model-improvement-receipt-2026-10-13)
records 17 trained checkpoints, 51 exports plus the recovered published v6
reference, repeated-seed quality, actual browser/native speed, meaning audits,
process memory and the retained failures. Neither receipt approves a replacement.
`train/experiments.py benchmark` measures existing exports without training,
changing published models or approving a replacement. Stage the exports with
`experiments.py stage` as above, serve the current preview, and build the
native diagnostic with `cargo build --release --manifest-path server/Cargo.toml
--bin brain-probe`. Then run:
```sh
"$EXP/bin/python" train/experiments.py benchmark \
--run "$DATA/full-run" --out "$DATA/benchmark" \
--url "http://127.0.0.1:8765/pet/bench.html" \
--export-python "$EXP/bin/python" \
--native-probe server/target/release/brain-probe \
--ort-library "$ORT_LIBRARY" \
--threads 4 --samples 40 --diaries 8 --cell-samples 10 \
--seed 0 --repeats 5 --warmup 1 --dtypes q4,q8,fp32
```
`ORT_LIBRARY` is the absolute path to the compatible native ONNX Runtime
shared library; it is not the browser WASM runtime. The output must be a new
directory outside the repository. `--models` restricts the comma-separated
candidate IDs; omit it to measure every completed candidate. The staged URL
namespace defaults to `experiments` (`--runtime-prefix`). The benchmark saves
`benchmark.json`, `benchmark.md` and per-variant behavior, browser and native
receipts, logs and failure details. Schema 2 includes the trainer's actual
unique parameter count and preserves whether the source run was screening or
full training; it never infers inherited-brain availability or equivalence.
Measurements run sequentially. Use the
same hardware and thread count, and record background desktop activity; these
are warm resident-model generation measurements, not network download,
hosting concurrency or phone performance results.
Browser `workload=pet|fixed|both` (driver `--workload`, default `pet`) keeps real
pet turns separate from a diagnostic workload. The fixed workload generates
32 greedy tokens from each short-idle/long-react prompt, deliberately ignoring
EOS, newline and lookup stops **only in the benchmark**. This prevents empty
or early-stopped output from winning a speed comparison. Its text is not a
pet response or a quality result. `brain-probe --benchmark-tokens 32` measures
the corresponding native workload; production generation keeps every stop
and safety rule. Prefill and decode throughput are reported separately.
The native pet result is raw greedy generation, not the app's sampled
lookup/filter/retry loop; do not compare its total time as if it were the
same end-to-end browser turn. Native total time excludes fitting/tokenization,
while browser full-turn time includes them. Load includes native startup
warmup or browser tokenizer/session loading, not a cold network transfer.
Tokens/second depends on tokenizer segmentation; equal token budgets do not
mean equal retained text or useful output. Real sampled pet-turn latency and
output counts remain available alongside the fixed workload.
The descriptive quality index is the equally weighted mean of six capability
percentages: valid lines; care (offer answers, late answers, sleeping-offer
validity and medicine answers); memory (saving, header recall and resolved old
recall); sleep (idle, lights-off, message, pat and wake validity); quiet idle;
and raw diary format. Each category averages its named rates, preserving
numerators and denominators. Missing/zero denominators, different scenarios,
graph mismatches or failed measurements cannot produce a ranked result.
Quality rank takes this index first, fixed browser decode speed second and
size third; speed rank is separate. Safety rejections, unsafe raw diaries and
incorrect offer acceptance are **not** hidden inside a comforting composite.
Read the transcripts: this is a behavior-contract index, not a semantic
intelligence test or a publication gate. Historical quantization/cache
failures remain blockers, not new benchmark results. The strict acceptance
and review requirements above still apply.
The current evaluator receipt is schema 4 / contract version 3. It drops
unsafe diaries whole, matching the shared Rust engine; `stored_text` is null
for a rejected/failed diary and raw failures remain available. Lookup requests
are counted in generated fragments before app results are inserted, so a
malformed outer tag cannot hide a resolved call or produce a resolution rate
above one. Older receipts retain their own contracts, not silently rewritten
counts. Production context fitting is preserved: a model whose positional
capacity cannot accommodate its fitted request fails evaluation rather than
silently receiving a shortened history. Diagnostic/native context clamping
alone does not establish browser compatibility for a smaller-context model.
## Compact-model app check receipt (2026-10-13)
A private local preview loaded the actual `pythia14-v8k-short` q4 export under
a new unpublished ID. Its cached graph was 3,145,767 bytes with SHA-256
`70715730ce8fef8fee6f66550e199406cbf1a353669d6b98c17d8fa93b8b9369`.
Chromium 153 used a disposable profile at 360 px; setup and chat, diary,
shelf, settings and debug panels had no horizontal overflow. Isolation was
intact, branding/debug controls remained `subtokenling`, and no old-name API
alias existed. The actual graph loaded, hatched the named pet and generated
chat. A hungry cookie offer was accepted and healthy medicine refused by the
model, not forced tags. Cached reload retained the pet, injected synthetic
memories and generated diary with the same brain ready.
This is also negative quality evidence: the request to remember a cat named
Bo produced a failed recall instead of saving the fact. With an older piano
fact injected through the engine's debug editor, an instrument question
looked up the cat and answered the wrong topic. The app wrote the real lookup
result; it did not manufacture the expected answer. Actual diary generation
included incoherent prose, and a sleeping-pat fixture emitted an awake-style
sleepy emote rather than the trained dream grammar. These were preserved,
not corrected or hidden to pass a smoke. Debug edits set fixture needs/facts,
never the model's replies or decisions. The LCD and diary surface were
captured. The private preview was stopped; no personal save, published brain,
runtime asset or release configuration was changed.
A second private preview exercised native-vocabulary `pythia14-native-short`
q8 under another new ID. Its actual cached 14,712,972-byte graph matched
SHA-256 `2113625ca1a4293c0c46eb9e1638be141045e5ac717e156d45435197d4c3097d`.
Fresh download/hatch, cookie care, actual look/recall round trips, diary and
cached reload ran at 360 px without panel overflow and with isolation intact.
An older injected piano fact was correctly retrieved and answered in visible
prose. However, cat-name capture stored `Momocat` instead of the owner fact,
medicine prose omitted the required answer tag (the body treated it as a
refusal), and its diary invented a star gift and strawberry feeding. These
failures remained in the receipt. The benchmark page itself was also visually
checked at 360 px with actual 32-token short/long output; that concurrent
functional smoke was not used as a timing measurement. Both additional
owned previews and browser tabs were stopped/closed, and the GPU check found
no remaining KFD compute processes.
Formal ONNX measurements and model-assisted held-out meaning reviews remain
separate: all 240 assigned diary outputs and 1,360 assigned memory/recall
outputs were accounted for, with explicit uncertain labels. They are small
synthetic samples, not a human-certified semantic benchmark or equivalence
proof. See the [training receipt](training.md#compact-model-improvement-receipt-2026-10-13)
for corpus/training provenance, rankings and the numerical/behavioral limits.
Raw app observations, graph identity, screenshots and audits remain outside
Git in the experiment receipt directory.
Final integrated checks passed `npm run format:check`, `npm test` (strict types,
300 states / 15 word lists with **0 parity mismatches**, the page contract,
22 Node, 39 data and seven site cases), all 27 experiment cases in the isolated
Torch environment, six shared-core Rust cases and 28 server Rust cases with
locked manifests. The system-Python experiment run skipped its 13 optional
Torch cases; the isolated run exercised all of them. No real-phone or GPU
inference performance was measured, and an observed graph-size saving is not
an accepted resident-memory or behavioral-quality saving.
## Quality-first execution check receipt (2026-10-16)
The [quality-first training receipt](training.md#quality-first-execution-receipt-2026-10-16)
records the pilot stop gate, learning diagnostics, source-grounded development
observations and phase-aware memory measurements. These are not a promoted
replacement or a new published-model speed/quality claim.
The actual default v6 app completed the existing full browser flow in fresh
Chromium 153: real download/hatch and generation, care, memory/shelf, diary,
pointer/keyboard game, all panels at 1280/768/360 px, cached reload and Web Lock
takeover. An additional actual cached offline reload generated a pet reply.
The 360 px page and LCD were captured and visually inspected. Requests remained
same-origin, no page errors or horizontal overflow occurred, and the private
memory hook was absent without its explicit opt-in query. Separate actual app
and benchmark memory cells completed all checkpoints through 50 normal turns
and worker termination, with actual graph bytes verified only afterward.
Final integrated checks passed:
- `npm run format:check` and `npm test`: strict types, 300 states / 15 word
lists with **0 parity mismatches**, page contract, 22 Node cases, 49 data
cases, 15 semantic cases and seven site lifecycle cases.
- All 27 experiment cases in the isolated Torch environment. System Python
explicitly skipped its 13 optional Torch cases; it did not claim coverage
of them.
- Locked-manifest Rust tests: six shared-core cases and 28 server cases,
with one existing opt-in server case ignored; both Cargo format checks.
- The rebuilt native contract and WebAssembly matched across 31 engine steps
in both UTC and America/Los_Angeles, alongside Rust/Python prompt parity.
- Actual native 70M fp32 export checks: unchanged numerical and 32-step cache
tolerances, five real Python/worker tokenizer probes, Chromium WASM
generation, and current native CPU `brain-probe` generation/context fitting.
This does not claim a full private-HTTP hosting qualification.
Owned teacher and preview processes and browser contexts were stopped. No
production scheduling default, immutable runtime/model asset, save format or
release configuration was changed by this execution. Raw synthetic fixtures,
source-linked reviews, graphs and screenshots stay outside Git.
## Debug panel
Open the page with `?debug=1` (or `#debug`) to get a **debug** tab after the
others. It is not saved or shown otherwise, and it sends nothing anywhere.
- **Pet:** the running brain, stage, day and exact age, and whether an offer
is open; buttons that set the age to the start of each stage; an age in days
to set directly (the stage follows from the age and no "grew" line is
written, so going back to baby works); sliders for the four stats; a sick
switch; current wake probabilities and persistence count.
- **Write today's page:** debug-only write/rewrite, disabled while the brain
is busy or unavailable or a game runs. It uses today's log so far even with
fewer than two entries. An early page suppresses automatic bedtime writing;
rewrite later to include later events. A failed rewrite retains the old page.
- **Context:** the last 20 prompts sent to the brain (chat lines, lookups and
diaries), newest first, each exactly as the worker built it after trimming
to the context window, with the model's output highlighted and the stop
reason. "copy latest" copies the newest prompt and output; "copy full chat"
copies the current header and the whole stored chat (more than the model
sees once the chat is long). Prompts are kept in memory until the page
reloads.
Use it to see why the pet did something: copy the prompt and replay it
against a checkpoint, or look at which lines the context window dropped.
## Browser recipes
These are procedures to run, not new verification or performance results for
the rebrand. The dated receipts above are inherited upstream observations;
report separately which recipes a changed build actually exercises.
### Rebrand compatibility and namespace precedence
Use disposable **persistent** profiles and the same origin/path before and
after the rebrand. Seed localStorage from a non-app page in scope (for example
`assets/OFL-nunito.txt`) with pet tabs closed; a running owner tab would save
over the fixture. Take an exported backup and retain the original raw strings.
These are test procedures, not claims that a new model has been measured.
- **Native legacy pet and archive:** capture a real pre-rebrand save under
`tokenling.v1`, its `tokenling.bad` archive and actual Cache Storage entries.
With both new keys absent, open the rebranded page. Preserve pet/owner names,
chat, memories, shelf and diary; ordinary elapsed-time simulation may still
change stats. The bridge must first copy each raw string byte-for-byte to
`subtokenling.v1`/`subtokenling.bad`, then remove its source only after
successful copy. Instrument storage writes if inspecting valid-save bytes,
since normal saves resume after loading. Cached baseline weights must not be
renamed or redownloaded just because the product name changed.
- **Broken and newer legacy saves:** repeat with broken JSON, then a native
save whose `v` is 99, only in `tokenling.v1`. After a successful namespace
move, `subtokenling.v1` must contain exactly the source bytes and the bad egg
must show without any saves, even after waiting/reloading. Export must return
those bytes. This is not a save-version migration. Start-anew archives them
at `subtokenling.bad` and clears both live keys before a fresh egg can load.
- **Current namespace wins:** seed distinct native pets in `tokenling.v1` and
`subtokenling.v1`, and distinct archive strings in both `.bad` keys. The
current pet/archive win; old bytes remain and must not replace the targets.
Repeat with a broken or newer _current_ save and a readable old one: show the
bad egg, never silently fall back to the old pet. A failed copy into a missing
current key must retain its legacy source; inject a quota failure for that
write and confirm a readable old pet still loads. Restore storage, let a
save succeed, and check that only then is that source retired. Unreadable
fallback data remains unsaved. Approved import retires the obsolete live key
only after a successful current-key write; failed import writes keep it.
Start-over and start-anew clear both live keys so reload cannot resurrect an
old pet. Inject a legacy-key removal failure during reset: it should report
the storage failure without reloading, not resurrect the fallback source.
Restore storage and retry before checking the fresh egg.
- **Fresh rebrand and API:** an empty profile downloads/hatches normally and
writes only `subtokenling.v1`, not an obsolete save key. The page title,
heading, accessible labels and install text say subtokenling. Inspect
`window.subtokenling.state()` and `window.subtokenling.brain`, exercise the
regular debug recipes, and confirm no legacy-name debug global alias exists.
The prompt still starts with the exact frozen `## tokenling chat` or
`## tokenling diary` marker; those trained wire bytes are intentionally not
branding. The release still selects the actual inherited immutable artifact.
- **Mixed-version tabs:** keep the legacy app open in one tab, then open the
new page on the same origin/path. Only one tab may tick/save: both use the
stable compatibility lock `tokenling.v1`. Exercise "use it here" and confirm
the former owner stops; the new takeover marker is `subtokenling.steal`.
Also rerun the ordinary two-current-tabs check.
- **Cache/alert bridge and offline:** seed real legacy `tokenling-app` and
`tokenling-alerts` entries plus distinct destination entries. Activate the
new worker: all old entries transfer without overwriting destination entries,
every `/tokenling-alerts.json` suffix maps to `/subtokenling-alerts.json`,
and an old cache disappears only after all its copies succeed. Verify the
copied plan retains `sent` flags and a previously delivered due alert is not
shown again; a pre-existing destination plan must win. Inject a `Cache.put`
failure to check that its old cache remains recoverable. Where
authorized periodic sync is available, transfer its registered old
`tokenling-alerts` tag by registering `subtokenling-alerts` before unregistering
the old one; do not invent permission or claim the browser fired it. Reload
after the worker controls the rebranded page, stop the server and check
isolation, cached model load and actual chat/dream inference. Exercise due
alert delivery once, suppression while visible, the new notification tag
prefix, tap-to-open and turning alerts off using the recipe below.
- **Install metadata and 360 px surface:** fetch the manifest and inspect
`name`/`short_name` (`subtokenling`), icons, unchanged relative `start_url` and
`scope`, Apple app title and the page's manifest link. Seed an old cached
manifest name and run `test/pwa-cache-flow.js` to verify online refresh,
offline metadata and rejection of an uncached manifest rather than an HTML
fallback. At 360 px, inspect the
fresh egg, download/retry, brain notice, chat/actions, bad egg, diary/settings
and no-JavaScript notes; assert no horizontal overflow and capture the LCD
and full page. Repeat cached/offline reload with the migrated profile.
### Slow Firefox download and recovery
Start a fresh preview/profile with `ONNX_BYTES_PER_SECOND=2000000` in the
preview server's environment and open it with
`playwright-cli -s=slow open URL --browser=firefox --profile=/tmp/slow-profile`.
Wait for `navigator.serviceWorker.controller` before getting an egg. The
97 MB model transfer must run past 30 seconds and reach a ready brain; before
the fix it ended with `Error in input stream` at roughly 30 seconds on Linux
Firefox. Leave the storage-persistence prompt unanswered: it must not delay
the download. Check cached reload, hatch/chat and offline reload afterward.
For recovery, interrupt a model response before completion. The egg must
remain unhatched with the error and `setup-retry` visible. Restore the network,
click retry, and check that the same egg and entered pet name survive and
the download reaches ready. Check worker script failure too: its error must
release the worker so retry can start. Test the retry row at 360 px.
An interrupted response may instead report `protobuf parsing failed`; check
that the bad ONNX entry is evicted and retry downloads a fresh copy.
### Catch-up and restricted browser APIs
Use a disposable profile for controlled-clock checks. Freeze `Date.now()`,
let an animation frame establish the live tick baseline, then advance it and
dispatch `visibilitychange` while the document is visible. Set an awake pet's
energy to 80 with other needs comfortable and no poop due: after a three-hour
return energy should be 65 and stay there on the next frame, not decay twice.
A one-minute gap should still be applied once by the live tick. With lights
off, a sleeping pet at energy 10 reaches 21 after half an hour, without a
second recovery on the next frame. Also check that a ten-day absence consumes
only the capped seven-day replay and is not reapplied by the live tick. Reload
and discard the test save after replacing the clock.
Before submitting "get an egg", inject a throwing `Worker` constructor, then
repeat with a real worker whose initial `postMessage` throws. Both should
reach `brain.state === "error"` immediately, release the worker and expose
`#setup-retry`. Restore the native API and retry: a new worker must start with
the same egg and edited pet name. This exercises startup recovery without
requiring model weights; successful download/inference needs the regular
model preview below.
For blocked storage, use `page.addInitScript` to make the `sessionStorage`
getter throw `SecurityError` before loading the page. The app must still boot
without an uncaught error or horizontal overflow at 360 px. Check normal
two-tab ownership and "use it here" separately with session storage available.
### Regular flows
Use a **named session with a persistent profile** whenever the brain is
involved. An in-memory profile cannot store the model: Cache Storage `put`
fails with "Unexpected internal error" and the brain reloads every time.
```sh
playwright-cli -s=pet open http://127.0.0.1:8765/pet/ --profile=/tmp/pet-profile
playwright-cli -s=pet fill "#owner-name" "Sam" --submit
```
The owner step ("get an egg") starts the brain download. **Wait for it, then
hatch:**
```sh
playwright-cli -s=pet run-code "async page => {
await page.waitForFunction(() => ['ready','error'].includes(window.subtokenling.brain.state), null, {timeout: 300000});
await page.fill('#pet-name', 'Mochi'); await page.click('#hatch-btn'); }"
playwright-cli -s=pet --raw eval "document.querySelector('#brain-status').textContent"
```
**Offers and pats:** `window.subtokenling.act('feed', 'cookie')` (or `bath`,
`medicine`, `play`, `pat`), wait for `!window.subtokenling.brain.busy`, then read
the chat: the offer line should be followed by a pet line with `[yes]` or
`[no]`, and `state().today.log` shows "ate a cookie" or "said no to a
cookie". Idle turns start a few seconds after the brain is ready; pause them
by keeping the tab hidden or reading `state().chat` right after a turn.
**Talk to it and read the transcript:**
```sh
playwright-cli -s=pet run-code "async page => {
const say = async (m) => { await page.fill('#chat-input', m); await page.press('#chat-input', 'Enter');
await page.waitForTimeout(500);
await page.waitForFunction(() => !window.subtokenling.brain.busy, null, {timeout: 180000}); };
await say('what time is it?'); await say('my favorite animal is the frog');
return await page.evaluate(() => window.subtokenling.state().chat.join('\n')); }"
```
`window.subtokenling` exposes `state()` (a read-only snapshot of the engine's
save; with `?debug=1`, change it with `debug.edit((s) => { s.stats.tummy = 15 })`), `brain` (including `id`, the running model, and `tps` and
`prefillMs` of the last turn), `upgrade`, `view`, `act(kind, arg)`,
`petTurn(kind?)`, `wantTurn(kind?)`, `renderAll()` and
`writeDiary(dateKey?, rewrite?)` (`rewrite=true` is honored only for today in
debug mode; tests can call this or seed `history` with a past day through
`debug.edit` and let the engine schedule the missed page).
**Sleep:** in debug mode, `tokenling.debug.setWakeRoll(() => 0.5)` fixes
only disturbance randomness; `setWakeRoll(null)` restores it. Reset the counter
with `resetDisturbances()`. At energy 10 a first message stays asleep with
that roll; at energy 70 it wakes. A roll of 0.99 leaves pats and lights asleep.
Offer a cookie while hungry and while stuffed: the offer itself never wakes;
only an accepted reply writes `woke up` and eats. Debug also exposes
`applyTags` (the engine applies the tags as if the pet wrote them), `simulate`
and `edit` for body checks. Confirm the sleep grime timer pauses and
resumes, and no more than two dream entries crowd the day log.
Optional `eval_pet.py CKPT 80 REPORT.json` saves metrics and all new probe
transcripts. The v6 eval adds idle/lights-off/message/pat dream validity, mumble length,
dream spread, awake replies after waking, sleeping offer answer/acceptance
rates by energy, tummy and cookie versus carrot, and `ugh`/`literally` and
positive hunger claims while stuffed by stage. It also probes medicine offers
while healthy/sick and awake/asleep. They run after the earlier
metrics. Use the same eval version for both checkpoints; results are samples,
not guarantees for every pet.
**Animations:** each CLI call takes about a second, longer than most
animations. Trigger and capture inside one `run-code`:
```sh
playwright-cli -s=pet run-code "async page => {
await page.evaluate(() => { window.subtokenling.debug.edit((s) => { s.stats.tummy = 20; }); window.subtokenling.act('feed','rice ball'); });
await page.waitForTimeout(300);
await page.locator('#pet-lcd').screenshot({path: 'eat.png'}); }"
```
**Mobile:** open with `--mobile` (360 px wide) and check that
`document.documentElement.scrollWidth === innerWidth`. Take a full-page
screenshot and look at the chat input, the action grid and the notes.
**Catch controls:** use a real pointer or touch gesture to hold each pad button,
slide off it, and release; movement should continue while held and stop on
release, cancellation, lost capture, blur or a hidden page. Try overlapping
directions, rapid taps, arrow/A/D keys, and Space/Enter on a focused pad button.
The pad should neither select text nor zoom. Check page scrolling and pinch
zoom outside it too. For the brain reminder, check download, upgrade, progress
and error states above the device at 360 px and desktop widths, including
after switching tabs.
**Offline:** after the brain has loaded once, stop the preview server and
reload: the page, app and runtime come from the service worker, the brain
from Cache Storage, and `crossOriginIsolated` stays true.
`playwright-cli network-state-set offline` is a weaker test (it left
`navigator.onLine` true here).
**Upgrade:** start the preview with only the older model
(`MODEL=$DATA/release/tokenling-v2`), hatch a pet, then restart it with
`MODEL=…/tokenling-v3 OLDER=…/tokenling-v2` (same port, same profile) and
reload. The pet should wake on v2 with the upgrade dialog open; "upgrade"
downloads v3 while v2 runs (`window.subtokenling.upgrade`), swaps, and leaves only
v3's files in `transformers-cache`. `playwright-cli requests` should show one
request for the new `.onnx`.
For the v5 → v6 format change, stage v6 with no `OLDER` instead: the saved
pet must doze until v6 is downloaded. Check that its name, memories, chat and
diary survive the download, and that cached v5 is never loaded by the v6 app.
**Feature gating:** give a component a `brainHas("x")` disabled/class/title
gate and `data-needs="x"` for inspection, set
`window.subtokenling.brain.id` to an older id that lacks `x`, call
`window.subtokenling.renderAll()`, and the control is disabled with the class
`needs-brain`; set it back and it is enabled again.
**Two tabs:** open the page in a second tab: it shows "open in another tab"
and does not run the pet. "use it here" moves the pet there, and the first tab
shows the notice.
**Bad egg:** write a broken or newer save from a same-origin page that does
not run the app (the running app saves every second and would overwrite it),
for example `goto /assets/OFL-nunito.txt`, then
`eval "() => { localStorage.setItem('tokenling.v1', JSON.stringify({...JSON.parse(localStorage.getItem('tokenling.v1')), v: 99})); return 1 }"`,
and go back to the page. Check that the save is untouched, then try "start
anew" (it moves the save to `tokenling.bad`) and importing the old save.
**Alerts:** grant permission with
`run-code "async page => page.context().grantPermissions(['notifications'], {origin: ORIGIN})"`,
tick `#alerts-toggle`, and read the plan from the `tokenling-alerts` cache.
Headless Chromium always reports the page as visible, so the worker never
alerts while it is open; to test delivery, write a plan with a past item,
navigate the tab to another page in scope, post `{type: "alerts"}` to the
worker and read `registration.getNotifications()`.
**Install:** the console shows "Banner not shown: beforeinstallpromptevent.preventDefault() called"
when the page is installable; after hatching, the install suggestion shows in
the chat.
**Console:** `playwright-cli console` and `console warning`. Anything there,
especially `ERR_BLOCKED_BY_RESPONSE`, is worth chasing.
**Close up:** `playwright-cli close-all`. Do not leave sessions or profile
directories running.
## What to check after a change
- **Art or animation:** screenshot `#pet-lcd` at each stage; check eyes, mouth,
decorations, the shadow and nothing clipped at the LCD edges.
- **Simulation:** set stats through `window.subtokenling.debug.edit((s) => …)` and call `act`;
check meters, event lines and the day log.
- **Prompt or tools:** parity test, then `eval_pet.py`, then a live chat that
triggers each lookup and checks the app wrote the right `{result}`.
- **Page layout:** desktop (≥1280), tablet and 360 px; keyboard-only through
the tabs and action buttons; the explanation section reads without
JavaScript.
- **Saves, tabs, PWA:** the upgrade, two tabs, bad egg, offline and alerts
recipes above after touching `load()`, the boot sequence, `sw.js` or the
brain loader.
- **Brain loading:** fresh profile (the egg's download), cached reload, and
`bench.html` for speed and memory after any change to `quantize.py` or the
session options in `brain.ts`.
- **After deploy:** the same flow against the live URL with a fresh persistent
profile, plus the live checks in `deployment.md`.
## Phones
Automated checks use desktop Chromium with a 360 px viewport. Speed and memory
on real phones have not been measured; open `bench.html` on one to do so.