1@README.md 2 3## Notes for agents 4 5- **`Speakable` has a private constructor on purpose.** It is the only type a shell may hand to speech, and only `Conversation::respond` can make one. Never add a public constructor or a `From<String>`: that reopens "speak unchecked text", the one thing this type removes. 6- **`Outcome::Fallback(Fallback)`, not one flat enum.** `Answered` is not a fallback and a fallback is not an answer; the nesting keeps `fallback()` total with no `unreachable!`. 7- **Who the words are for is an `Audience`, from the `SharedProfile`.** Prompts and the guard's age come from it, read once per turn; `Guard::check` takes an `Age` and never a name. With no profile the audience is the youngest age (`Age::YOUNGEST`) with neutral wording. Do not add a prompt, greeting or fixture with a name or an age written in it. 8- **The guard failing is a refusal.** `Err` from `Guard::check` ends the turn. Never map it to `Allow` to keep a conversation going. 9- **The log is written before each step is acted on.** A broken log, or pictures that cannot be kept, stop the turn before the model or guard is called (`LogUnavailable`). The single unlogged utterance is that fallback line and, best effort, the closing `Said`. Do not reorder. 10- **Pictures are judged through a description.** Jev judges words, not pictures, so a separate model call with its own prompt (`describe_pictures_prompt`, no persona) describes them first, the description is logged (`PictureSeen`) and the guard judges it like speech; only then does the cat see them. Any failure keeps the picture from the cat. That call is itself shown the picture and could be talked at by text inside it, which is why its output is judged rather than trusted. 11- **Only turns allowed both ways enter the history, as words.** A refused question or answer never reaches the model again, and a picture is shown once. 12- **A remembered fact is fed back into the prompt, so it is guarded like speech** and logged (`Remembered` / `NotRemembered`). A fact that cannot be logged is forgotten again. Memory ids are never reused (`next_id` is saved with the facts). 13- **A damaged memory file is an error, not an empty memory.** Starting empty would silently drop everything she told it and then overwrite the evidence. 14- **The voice allowance spends whole lines.** `VoiceBudget` never splits a line across voices; a damaged record starts a fresh day (worst case: one day's allowance twice). 15- **Ports are synchronous.** The shell calls `respond` from a background thread. Do not add an async runtime to this crate; it would follow the core into the Android binding. 16- **Reflection and compression run off the conversation lock.** `Reflector` takes its own shared handles; never make `respond` wait on it, or the next turn stalls behind memory work (the "listens but does not reply" bug). 17- **Jev is rate limited machine-wide (30 questions a minute).** Guard facts in one batched check per reflection, and recall's rerank is one Choice for the whole shortlist; do not add a call per fact. 18- **Recall degrades, never fails a turn.** Embedder or ranker errors fall back (recency, then embedding order) and are logged as `MemoryFailed`. 19- **Keep it buildable for wasm.** Code that touches `std::fs`, `SystemTime`, `Instant` or stderr belongs behind `#[cfg(not(target_family = "wasm"))]`, with the pure rule beside it (a merge, a comparison) callable without storage. Check with `cargo check --target wasm32-unknown-unknown -p whiskers-core` inside `nix develop ~/whiskers`. The hubs on the service run the pure merges in `whiskers-ports`' adapters; do not re-implement a merge rule in an adapter. 20- **`TimeKeeper::merge_durably` is for hubs.** A device's `merge` tolerates a failed save (a little lost time); the service's master copy of the grown-ups' choices does not, because a PIN or a limit that vanished on restart is worse than a refused sync. 21- **Hide is not forget, and a prompt never sees a hidden fact.** `Visibility` is a typed last-writer-wins register on the `Fact`; `forgotten` still beats everything. Anything built into a prompt or a search reads `SharedMemory::usable()`; `facts()` is every fact, for the parents' view and the memory's own work (a hidden fact still counts as known, so Whiskers does not file it again). A new reader of facts picks one on purpose. A hide or restore is stamped later than the write it follows (`hidden_after`, `restored_after`), so a slow clock cannot lose a parent's restore. An untouched fact has no `visibility` in its stored or wire form: keep `skip_serializing_if`, the wire is a contract. 22- **A fact's `icon` is an `IconId`, never a string.** It can only name an entry of `crates/whiskers-icons/allowlist.txt`. The memory file reads an unknown name as no icon (`lenient_icon`) instead of failing: a picture lost must never read as a damaged file. Two copies with different icons meet at the smaller name; the first icon given locally is kept (`give_icon`). The merge never takes a picture away. 23- **A fact's picture is chosen off the conversation, and failing to choose is never an error she sees.** `Reflector::give_icons` asks the `IconChooser` (the service: shortlist by meaning, then a Jev Choice) for a few iconless facts per pass and per sync; an `Err` stops the pass and is retried at the next, a "none fits" is remembered for the process. A fact with no picture shows a generic star. Do not add a call per fact on the conversation path (Jev answers thirty questions a minute).