|
<div class="docs-content" data-render>
|
|
# 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.
|
|
|
|
## Stopping a turn: stop and reset
|
|
|
|
Each user message runs as its own cancellable asyncio task (`spawn_turn`), tracked in `_turns`. The
|
|
websocket receive loop keeps reading while a turn runs, so an interrupt is honoured immediately even
|
|
mid-task. Typing `stop` or `reset` (or sending a `stop` / `reset` frame) is intercepted server-side
|
|
in the websocket loop before anything else, so the interrupt is authoritative and works regardless of
|
|
the client.
|
|
|
|
- `stop` cancels the running turn and keeps the conversation.
|
|
- `reset` cancels the running turn and clears the conversation.
|
|
|
|
Cancellation is bulletproof by construction. Tool calls run as awaited coroutines (the react loop
|
|
gathers them; sub-agents are awaited inline), so cancelling the turn task propagates `CancelledError`
|
|
through every await with no orphaned work. On top of that, `cancel_turns` first increments a
|
|
`_turn_epoch`; a turn only emits its reply or records its turn while its captured epoch still matches
|
|
the session's, so a turn that survives cancellation for a moment, or finishes the instant after an
|
|
interrupt, can never deliver output or persist anything afterward. A cancelled turn never runs
|
|
`_record_turn`, so it cannot save a half-finished message list. `stop` then repairs the kept history
|
|
(`_repair_history` trims any trailing assistant tool-call block whose tool responses are incomplete)
|
|
so the next turn always starts from a valid conversation.
|
|
</div>
|