# Oikos — Desktop Mascot Subsystem Model > Companion to [the platform Model](../mbse/README.md) and > [the Framework](../mbse/framework.md). This document is a **subsystem > Model** in Holt's sense — it conforms to the same Framework (Ontology + > Viewpoints, Markdown + Mermaid Notation) rather than restating it, scoped > to a single not-yet-built subsystem of the `web` component: the desktop > mascot ("Cluck"), a pixel-art chicken that lives on the desktop shell. > Where the platform-wide Views in [../mbse/README.md](../mbse/README.md) > and the component View for `web/src` in > [../mbse/components.md](../mbse/components.md#5-web-control-room) speak > at the level of "the SPA," this document goes one layer deeper into one > feature of it — the same relationship [components.md](../mbse/components.md) > has to [README.md](../mbse/README.md), applied recursively. **Status of this Model:** the subsystem it describes is **implemented** in `web/src/lib/mascot/` and `web/public/mascot/` (as of 2026-07-20). Views below are marked **Implemented** where the code matches; a small number of requirements (distinct adult art, a true round radial menu) remain **Planned** as polish items. The corresponding implementation plan is [plans/2026-07-20-desktop-mascot.md](../../plans/2026-07-20-desktop-mascot.md), which carries a deviation note at the top covering the changes made during implementation (hatch-on-naming, PNG-sheet art, button-column radial menu, 60fps loop), and the physics audit/follow-up is [plans/2026-07-20-mascot-physics-audit.md](../../plans/2026-07-20-mascot-physics-audit.md). ## Views in this model | # | View | Concern it addresses | |---|---|---| | [1](#1-mission--system-context) | Mission & System Context | Why a mascot, and what is it never allowed to do? | | [2](#2-requirements) | Requirements | What must it do, traced from the original request? | | [3](#3-structural-view) | Structural View | What modules make it up, and which are the extension points? | | [4](#4-behavioral-view) | Behavioral View | How does it move, live, and react, moment to moment? | | [5](#5-interfaces-view) | Interfaces View | What does it read from the rest of the system, and how does it persist itself? | | [6](#6-extension-guide) | Extension Guide | How does a future engineer add an animation, behavior, menu action, or reaction? | | [7](#7-verification-view) | Verification View | How will we know it works, once built? | ## 1. Mission & System Context **Stakeholders:** the operator (delight, ambient awareness of system state without opening a window); future engineers extending the mascot's behaviors/reactions/menu. **Mission:** give the desktop shell a persistent, living presence that makes background system activity legible at a glance — a chat streaming, a knowledge-graph write, a critical signal — without requiring a window to be open, while doubling as a lightweight tamagotchi for its own sake (delight is a legitimate requirement here, not a side effect). **Boundary — what the mascot is, and is not:** - It is a **purely client-side, read-only observer**. It subscribes to existing `web` stores (chat, activity, events, dashboard summary) the same way any other UI component does. - It **never calls a mutating API endpoint** and is not a new actuation path — it has no relationship to the `run` gate, `Execution`, or `Approval` entities described in [the platform Ontology](../mbse/ontology.md). Its only "mutation" is its own tamagotchi state, stored client-side. - It is scoped entirely inside the `web` component ([../mbse/components.md §5](../mbse/components.md#5-web-control-room)); it introduces no new backend surface, no new MCP tool, no new REST route. ```mermaid flowchart TB subgraph SURFACE["Desktop shell surface (Desktop.svelte)"] ICONS["Icon layer\nz-0"] LAUNCH["Task launcher\nz-10"] WIN["WindowLayer\nz-40"] MASCOT["MascotLayer\nz-45\n(this subsystem)"] MENU["Desktop context menu\nz-50"] end MASCOT -->|subscribes, read-only| EVENTS["stores/events.ts\nliveEvents (SSE)"] MASCOT -->|subscribes, read-only| CHAT["stores/chat.ts\nstreaming"] MASCOT -->|subscribes, read-only| ACTIVITY["stores/activity.ts\nactivityLog"] MASCOT -->|subscribes, read-only| CONTEXT["stores/context.ts\nsummary"] MASCOT -->|reads/writes| LS["localStorage\noikos-mascot"] style MASCOT fill:#fff3e0,stroke:#e65100 ``` ## 2. Requirements Traced from the original feature request. Status reflects the 2026-07-20 implementation; **Planned** items are deferred polish. | ID | Statement | Source | Status | |---|---|---|---| | MASC-1 | The mascot SHALL render as pixel-art from bundled 16x16 PNG sprite sheets (chicken + egg packs), not code-drawn string grids | User request (relaxed from "code-drawn" during implementation — see plan deviation note) | Implemented | | MASC-2 | The mascot SHALL roam the desktop surface autonomously, walking along the ground (surface bottom, above the taskbar, OR the top edge of any non-minimized window beneath it) under gravity | User request + design decision | Implemented | | MASC-3 | The mascot SHALL be draggable with the mouse; releasing it mid-air SHALL trigger a flutter-fall back to the ground | User request + design decision | Implemented | | MASC-4 | Right-clicking the mascot SHALL open an interaction menu supporting nested submenus; rendered as a rounded-button column (relaxed from "round/Sims-style" — see plan deviation note) | User request | Implemented | | MASC-5 | The mascot SHALL have a tamagotchi lifecycle: egg → chick → adult, with a user-assignable name; the egg → chick transition fires on first naming, not on a timed incubation | User request | Implemented | | MASC-6 | The mascot's stage, name, and stats SHALL persist across reloads | User request (implied by "tamagotchi") | Implemented | | MASC-7 | The mascot SHALL have idle states (autonomous behavior when untouched) and interactive states (drag, click, menu) | User request | Implemented | | MASC-8 | The mascot SHALL react visibly to real application activity: chat streaming, knowledge-graph writes, critical signals | User request ("aware of its environment... feels alive and connected") | Implemented | | MASC-9 | Animations, behaviors, menu actions, and reactions SHALL each be defined in a single data-driven registry, so a new one can be added without touching the engine code | User request ("easily expansible") | Implemented | | MASC-10 (NFR) | The mascot's game loop SHALL run via `setTimeout`, not `requestAnimationFrame`, matching the repo's existing [`GraphBackground.svelte`](../../web/src/lib/components/GraphBackground.svelte) convention (rAF suspends in some hidden-tab embeddings); runs at ~60fps (relaxed from 30fps for smoother drag/fall — see plan deviation note) | Codebase convention | Implemented | | MASC-11 (NFR) | The mascot SHALL never write to the API; all mutation is local (localStorage) | Design decision, this document §1 | Implemented | | MASC-12 (NFR) | Persistence writes SHALL be debounced (~300ms), never per animation frame | Codebase convention ([`stores/windows.ts`](../../web/src/lib/stores/windows.ts) wmkit persist) | Implemented | ## 3. Structural View **Stakeholders:** an engineer implementing or extending the mascot. **Why this View earns its place:** MASC-9 (extensibility) is only real if the module boundaries actually separate data (registries) from engine code; this View is the check that they do. ```mermaid classDiagram class types_ts { <> PixelGrid AnimName MascotStage BehaviorId Stimulus RadialAction } class palette_ts { <> PALETTE: char to CSS color } class sprites_ts { <> SPRITES: Stage to AnimName to AnimDef resolveAnim(stage, name) } class render_ts { <> drawFrame(ctx, grid, palette, flip) } class state_svelte_ts { <> MascotModel state grantXp() feed() pet() setName() tickLifecycle() advanceStageIfReady() persist (debounced, oikos-mascot) } class behavior_ts { <> BEHAVIORS: BehaviorId to BehaviorDef stepMascot(rt, model, now, dt) } class stimuli_ts { <> REACTIONS: id to ReactionDef attachStimuli(emit) } class actions_ts { <> MASCOT_ACTIONS: RadialAction tree registerMascotAction() } class Mascot_svelte { <> canvas render loop 30fps pointer drag/click/contextmenu } class MascotLayer_svelte { <> z-45 absolute overlay hosts Mascot + RadialMenu + bubble } class RadialMenu_svelte { <> z-60 fixed, nested rings } class NameDialog_svelte { <> } sprites_ts --> palette_ts : indexes sprites_ts --> types_ts : uses Mascot_svelte --> render_ts : draws frames Mascot_svelte --> sprites_ts : resolves anim Mascot_svelte --> behavior_ts : steps FSM Mascot_svelte --> state_svelte_ts : reads/mutates model MascotLayer_svelte --> Mascot_svelte : hosts MascotLayer_svelte --> RadialMenu_svelte : hosts, on contextmenu MascotLayer_svelte --> stimuli_ts : attaches on mount MascotLayer_svelte --> NameDialog_svelte : hosts, on hatch/rename RadialMenu_svelte --> actions_ts : renders tree stimuli_ts --> behavior_ts : forceBehavior(react) ``` **The four extension registries** (MASC-9's concrete answer — see also [§6 Extension Guide](#6-extension-guide)): `SPRITES` (animations), `BEHAVIORS` (autonomous states), `MASCOT_ACTIONS` (radial menu tree), `REACTIONS` (environment stimuli). Each is plain data; the engine (`behavior.ts`'s `stepMascot`, `Mascot.svelte`'s loop, `RadialMenu.svelte`'s renderer) is generic over whatever the registry currently contains. **Mount point:** two lines in [`Desktop.svelte`](../../web/src/lib/components/desktop-shell/Desktop.svelte) — `` rendered inside the surface `
` (the `relative min-h-0 flex-1 overflow-hidden` element), after ``, so its `absolute inset-0` shares the surface's coordinate space and its ground line is exactly the surface's bottom edge (= the taskbar's top edge). ## 4. Behavioral View **Stakeholders:** an engineer reasoning about "what does the mascot do right now, and why." **Why this View earns its place:** a mascot with an implicit, ad-hoc state machine is unmaintainable the moment a second behavior or reaction is added; this View is the state machine made explicit before any of it is coded. ### 4.1 Behavior FSM (moment-to-moment autonomy) ```mermaid stateDiagram-v2 [*] --> egg egg --> chick : first naming submitted\n(forceHatch: hatchProgress=1) state chick_and_adult_behaviors { [*] --> idle idle --> wander : weighted random pick\non behaviorUntil expiry wander --> idle idle --> peck : weighted random pick peck --> idle idle --> hop : weighted random pick hop --> idle : touchdown\n(off-edge mid-hop hands to falling) idle --> sleep : weighted random pick sleep --> idle wander --> falling : y below ground\n(off a dragged edge, etc.) idle --> dragged : pointerdown + move\npast 5px threshold wander --> dragged : pointerdown + move sleep --> dragged : pointerdown + move\n(interrupts sleep) dragged --> falling : pointerup, released mid-air\n(toss velocity from pointer history) falling --> falling : hard impact\n(one diminished bounce) falling --> land : y reaches ground\n(sideways momentum -> skid) land --> idle [*] --> react : stimulus dispatched\n(priority/cooldown gated) react --> idle : durationMs elapsed,\nreturns to prior-or-idle } chick --> adult : xp reaches ADULT_XP\n(advanceStageIfReady) ``` `dragged` always wins over any autonomous behavior; `sleep` is broken only by a reaction whose `ReactionDef.interruptsSleep` is true (§4.3) or by a drag. Weighted-random idle selection (`weight` field in `BehaviorDef`) picks the next autonomous behavior only when the current one's `next()` returns null past `behaviorUntil` — see [plans/2026-07-20-desktop-mascot.md](../../plans/2026-07-20-desktop-mascot.md) for the concrete weights. **Physics feel (implemented 2026-07-20, second pass):** the fall is a losing attempt at flight, not a drop — wing-beat impulses on a speed-scaled, jittered flap cycle (panic flapping) shave the descent; falls faster than terminal velocity (hard downward tosses) decay back under drag instead of clamping; hard impacts bounce once, squash via a damped-spring render layer scaled by impact speed, and poof a burst of feather pixels; sideways momentum becomes a friction skid on touchdown and ricochets off the surface's side bounds mid-fall; the sprite stretches along its motion in the air and tilts into horizontal velocity (fall, drag, and skid); walking bobs at step frequency. All of it is tuning in `behavior.ts` plus the pure render layer in `Mascot.svelte`'s `updateJuice()` — no new assets, no new states beyond `hop`. ### 4.2 Tamagotchi lifecycle (long-lived state) ```mermaid stateDiagram-v2 [*] --> egg : first load,\ndefaultModel() egg --> chick : first naming submitted\n(forceHatch sets hatchProgress=1)\n+ NameDialog shown chick --> adult : xp >= ADULT_XP (200) adult --> [*] ``` This is a separate state machine from §4.1: §4.1 governs frame-to-frame motion/animation, §4.2 governs the tamagotchi's slow-moving `MascotModel` (persisted, ticked ~1x/sec via `tickLifecycle`, not every frame). The egg → chick transition fires on first naming, not on a timed incubation — see the deviation note in [plans/2026-07-20-desktop-mascot.md](../../plans/2026-07-20-desktop-mascot.md). ### 4.3 Example sequence — an environment stimulus becomes a visible reaction ```mermaid sequenceDiagram participant SSE as stores/events.ts (SSE) participant Stim as stimuli.ts attachStimuli participant Layer as MascotLayer.svelte (emit callback) participant FSM as behavior.ts participant Mascot as Mascot.svelte (canvas) SSE->>Stim: liveEvents updates,\nnew head event severity=critical Stim->>Stim: check REACTIONS['alarmed']\ncooldown + priority Stim->>Layer: emit(reaction) Layer->>Layer: if model.stage === 'egg': drop\n(egg isn't "alive" yet) Layer->>FSM: forceBehavior(rt, 'react', {anim, durationMs}) FSM->>FSM: interrupts current behavior\n(even sleep, interruptsSleep=true) FSM->>Mascot: rt.behavior = 'react', rt.anim = 'react-alarm' Mascot->>Mascot: next ~60fps tick draws\nreact-alarm frame + bubble Note over FSM: after durationMs,\nnext() returns to idle ``` **Egg-stage suppression:** MascotLayer's `emit` callback drops any reaction when `model.stage === 'egg'`. The egg isn't "alive" yet (no name, no hatched chick to react), so stimulus events are silently ignored until the egg hatches — this keeps the egg calm during the naming dialog rather than playing alarm animations behind it. ## 5. Interfaces View **Stakeholders:** an engineer wiring a new store into the mascot's awareness, or auditing what it depends on. | Interface | Direction | Shape | Notes | |---|---|---|---| | [`stores/events.ts`](../../web/src/lib/stores/events.ts) `liveEvents` | consumed | `Writable`, newest-first, ref-counted via `subscribeEvents()` | `OikosEvent.type` families: `approval.*`, `signal.*`, `execution.*`, `health.changed`; `severity: 'info'\|'warning'\|'critical'` | | [`stores/chat.ts`](../../web/src/lib/stores/chat.ts) `streaming` | consumed | `Writable` | false→true edge triggers the `thinking` reaction, held while true | | [`stores/activity.ts`](../../web/src/lib/stores/activity.ts) `activityLog` | consumed | derived `Readable`, **recomputed wholesale** on every emission — not append-only | new entries with `type === 'knowledge'` detected by diffing entry `id`s between emissions, not by treating it as a stream | | [`stores/context.ts`](../../web/src/lib/stores/context.ts) `summary` | consumed | `Writable` | ambient state (open signal counts via `openSignalCount(summary)`) | | `localStorage['oikos-mascot']` | owned | `MascotModel` JSON, `{ version: 1, stage, name, hatchProgress, happiness, xp, hatchedAt, lastPos: {x}, lastSeen }` (hatchProgress is binary 0/1: 0 until first naming, 1 after) | debounced write (~300ms, mirrors [`stores/windows.ts`](../../web/src/lib/stores/windows.ts) wmkit persist) + `beforeunload` flush; `version` field reserved for a future `migrate()`; multi-tab is last-writer-wins (accepted, documented, not solved) | | [`Desktop.svelte`](../../web/src/lib/components/desktop-shell/Desktop.svelte) mount | owned | ``, 2-line insertion | see §3 | No interface in this table is a write path to the Oikos API — consistent with §1's boundary statement (MASC-11). ## 6. Extension Guide **Stakeholders:** a future engineer adding one new animation, behavior, menu action, or reaction — this is the Viewpoint 4's "why" made concrete as a recipe rather than prose (mirrors [../mbse/framework.md §7](../mbse/framework.md)'s Process Set treatment). | To add a... | Touch only | Nothing else changes because | |---|---|---| | **Animation** | Add the name to the `AnimName` union in `types.ts`; add frames to `SPRITES[stage]` in `sprites.ts` | `resolveAnim()` and the renderer are generic over the registry | | **Behavior** | Add the id to `BehaviorId`; add one `BehaviorDef` entry to `BEHAVIORS` in `behavior.ts` | `stepMascot()` and the weighted-random idle selector consume `BEHAVIORS` generically | | **Radial menu action** | Add a `RadialAction` node to `MASCOT_ACTIONS` in `actions.ts` (or call `registerMascotAction()`), optionally nested under `children` | `RadialMenu.svelte` renders whatever tree it's given, including nesting depth | | **Environment reaction** | Add a `ReactionDef` to `REACTIONS` in `stimuli.ts`; wire one `store subscription -> predicate -> emit(reaction)` block inside `attachStimuli()` | priority/cooldown/interrupt dispatch logic in `attachStimuli()` is generic over `REACTIONS` | ## 7. Verification View **Stakeholders:** whoever implements this subsystem and needs to know when it's actually done, not just compiled. Manual browser checklist (no automated test harness planned for v1 — see [plans/2026-07-20-desktop-mascot.md](../../plans/2026-07-20-desktop-mascot.md) for the same list in implementation-order context): - Egg renders grounded at the surface bottom, wiggles gently while the name dialog is open, and survives a reload at the same x (confirm `oikos-mascot` is debounced — no writes fire from mere walking, only from discrete transitions). - Dragging the egg up and releasing triggers a flutter-fall with no tunneling below the taskbar; dragging past the surface edges clamps. - A fresh egg (no name) opens the name dialog on mount; submitting it hatches to chick; the name persists across reload. The debug "Force hatch" action does the same without prompting. - Chick wanders and flips sprite at surface edges, pecks, sleeps autonomously; a plain click (no drag) triggers a pet/hop reaction. - Right-clicking the chicken opens the radial menu centered on it, without triggering the desktop's own right-click menu; a nested submenu (Feed) opens correctly; Escape pops one level then closes; an outside click closes it; the menu stays fully visible when the chicken is near a screen edge or corner. - With one or more windows open (including a maximized one), the chicken visibly walks above them without breaking window drag/resize/close. - Starting a chat and observing it stream triggers the `thinking` reaction for the duration; a simulated knowledge-graph write triggers `eureka` once per cooldown window; a simulated critical signal triggers `alarmed` even while the chicken is asleep. - Resizing the browser viewport re-grounds and re-clamps the chicken. - Both the Terracotta and Carbon themes keep the pixel-art palette legible. - `npm run build` passes with no new errors.