# Architecture and sessions How Devii is wired at runtime: the hub, per-owner sessions, the websocket, persistence, and the multi-worker model. See also [Devii internals](/docs/devii-internals.html). ## Owner model Every session is keyed by an **owner** tuple `(owner_kind, owner_id)`: - `user` / user uid - a signed-in DevPlace account. The agent authenticates to this instance with that user's API key, so it operates the user's own account. - `guest` / `devii_guest` cookie - an anonymous sandbox session. Owner scoping is the spine of Devii: conversations, tasks, lessons, the usage ledger, and the audit log are all filtered by owner, so nothing is shared between accounts. ## DeviiHub `DeviiHub` holds the process-wide state: one `DeviiSession` per owner (`get_or_create`), the shared `LLMClient`, the `ConversationStore`/`UsageLedger`/`TurnAudit`, and housekeeping (`gc_idle`, ledger prune). For a signed-in user it builds a `PlatformClient` with the user's API key and lazily **rehydrates** the saved conversation; for a guest it builds an unauthenticated client with no persistence. ## DeviiSession A session owns the agent, dispatcher, scheduler, and a **set** of websockets. Key behaviors: - **Multi-tab broadcast.** Every frame (trace, reply, task, status) is sent to all of an owner's connected sockets, so the same content shows on every tab, window, and device. - **History snapshot.** On connect, the session sends a `history` frame so a new tab renders the prior conversation; live frames then stream. - **Turn serialization.** A per-session lock serializes turns; a new tab joining mid-turn still receives live frames. - **Reload-resilient browser calls.** Avatar and client tool requests round-trip to the browser; if the page reloads mid-request, the call waits for reconnection and re-sends, so a turn survives a refresh. ## Persistence and rehydration After each turn the session writes the (compacted) message list to `devii_conversations` (users only). On reconnect (or after a restart) the saved messages are loaded back into the agent, so a signed-in conversation survives reloads and reboots. Guests are never persisted. See [Data and persistence](/docs/devii-data.html). ## Multi-worker model `make prod` runs multiple uvicorn workers, but only one holds the background-service lock. Because hubs are per-process, `/devii/ws` is served **only** by the lock-owning worker (`service_manager.owns_lock()`); other workers close the socket with code 1013 and the auto-reconnecting client lands on the owner. This guarantees a single hub, one broadcast source, and one cost tracker per user. The 24h spend cap is always read from the ledger DB, so it stays correct regardless. See [Multi-worker and concurrency](/docs/production-concurrency.html) for the shared service-lock model this builds on. ## CLI parity The `devii` console script reuses the same agent, tools, and stores. When it runs against the same database, it resolves the logged-in account from its API key and uses that owner, so the CLI shares the same per-account lesson memory and tasks as the web session. See [Configuration and CLI](/docs/devii-config.html). ## Shared browser session The web terminal and the browser are one session. The websocket resolves its owner from the browser's `session` cookie at connect, so the terminal is always whoever the browser is logged in as. When the agent logs in via the terminal, it mints a real platform session and hands the token to the browser through a single-use `GET /devii/adopt` redirect (the `session` cookie is httpOnly, so only an HTTP endpoint can set it); when it logs out, the browser is sent to `/auth/logout`. Both are full navigations, so the page refreshes and the terminal reconnects as the new identity. In the other direction, a login or logout in the browser (including another tab) is detected by a focus check against `/devii/session` and triggers a reload, keeping both sides in lock-step.