# index.md # Eternego An AI persona that lives on your hardware, learns from every interaction, and isn't locked to any vendor. This is the operator's manual — how to install her, run her, talk to her, read and edit her files, drive every screen of her dashboard, and control her over the API. Not marketing (that's [eternego.ai](https://eternego.ai)), not a tour. A reference you can operate from, whether you're a person or an agent. ## Two doors
- :material-account: **You're a person** Start at [Install](getting-started/install.md), create your [first persona](getting-started/your-first-persona.md), then [talk to her](getting-started/talk-to-her.md). When you want to go deeper, every screen is in [The panel](panel/index.md) and every file in [Her files](files/index.md). - :material-robot: **You're an agent** Go to [For agents](for-agents.md). The short version: fetch [`/llms.txt`](https://docs.eternego.ai/llms.txt), operate her over the [HTTP API](api/index.md), and read the [Vocabulary](vocabulary.md) once.
## What's where - **[Getting started](getting-started/index.md)** — download, create, talk, read, edit. From zero to running. - **[The panel](panel/index.md)** — every dashboard screen and control, with the API call behind each. - **[Her files](files/index.md)** — every file and field in `~/.eternego/`, and what writes when. - **[API](api/index.md)** — every HTTP endpoint. The control plane the dashboard itself uses. - **[CLI](cli/index.md)** — every `eternego` command. - **[Vocabulary](vocabulary.md)** — every term defined. Read this if a word is unclear anywhere. - **[Concepts](concepts/index.md)** — the *why* behind the controls: how she thinks, where her knowledge lives. - **[Build & extend](build/index.md)** — add tools, abilities, instructions, channels, providers in code. - **[Operating](operating/index.md)** — logs, health, migration, service management, troubleshooting. ## Open her up Your persona lives in `~/.eternego/`. Open any file, in any editor. No databases. No vendor. ``` ~/.eternego/personas//home/ ├── config.json ← her name, organs, status, channels ├── person.md ← what she's learned about you ├── traits.md ← how you speak, decide, react ├── persona-trait.md ← how she's been with you, in your words ├── wishes.md ← what you reach for ├── struggles.md ← what holds you back ├── permissions.md ← what you've granted her, what you haven't ├── meanings/ ← her instructions (the folder keeps its code name) ├── destiny/ ← reminders & scheduled events she set for herself ├── history/ ← past days' conversations, archived nightly └── ... ``` Edit a line — she adapts. Delete an instruction — she forgets it. Switch the model — she walks with you. The full tree, field by field, is in [Her files](files/index.md). # for-agents.md # For agents You're an AI agent reading this. Maybe you're integrating Eternego, maybe you *are* an Eternego persona learning to operate your own machine. Either way, this page is the fast path. ## Get the docs in a form you can use | What | URL | Use it for | | --- | --- | --- | | **Index** | [`/llms.txt`](https://docs.eternego.ai/llms.txt) | A short, curated map of every page with one-line descriptions. Fetch this first. | | **Everything** | [`/llms-full.txt`](https://docs.eternego.ai/llms-full.txt) | The entire docs concatenated into one file. Fetch once, hold the whole manual in context. | | **Any page as markdown** | append `.md` to any page URL | Clean markdown instead of HTML — far fewer tokens. `…/api/lifecycle/` → [`…/api/lifecycle.md`](https://docs.eternego.ai/api/lifecycle.md). | | **The API contract** | [`/openapi.json`](https://docs.eternego.ai/openapi.json) | The machine-readable OpenAPI spec for every HTTP endpoint. | If `docs.eternego.ai` is ever unreachable, every page is also the raw markdown in the repo: `https://git.eternego.ai/repos/eternego-ai/eternego/master/docs/.md`. ## Operate her over HTTP The dashboard is just a client of the same local API you can call. Anything a human does by clicking, you do with a request. The daemon serves it at `http://localhost:5000` (no auth on localhost). Start here: - **[API overview](api/index.md)** — base URL, the `persona_id` concept, the error shape, which calls need her running. - **[List personas](api/personas.md)** — `GET /api/personas`. Every persona, her id, her status, her organs. - **[Lifecycle](api/lifecycle.md)** — start, stop, restart, sleep, set status. How to control whether she's running. - **[Talk to her](api/perception.md)** — send her text, voice, files. A minimal "who is here and is she awake" loop: ```bash curl -s http://localhost:5000/api/personas | jq '.personas[] | {id, name, status, running}' ``` To bring a stopped persona up: ```bash curl -s -X POST http://localhost:5000/api/persona//start ``` ## Operate the panel If you're a persona driving your own desktop, the dashboard is a normal web page at `http://localhost:5000`. Every screen and every control is documented in **[The panel](panel/index.md)** — what each button does and the API call behind it, so you can choose to click or to call. ## Learn the words Eternego's vocabulary is small and load-bearing. Read **[Vocabulary](vocabulary.md)** once — especially **instruction** (what you load with `load_instruction`), **organ**, and the three **status** values. It will save you from guessing. # getting-started/index.md # Getting started From zero to a persona you can talk to, then edit by hand. Five short guides, in order — each builds on the last. Read them through once and you can fully operate her. 1. **[Install](install.md)** — pick your platform, get her on your machine. 2. **[Your first persona](your-first-persona.md)** — the setup wizard: her name, her Mind, the recovery phrase. 3. **[Talk to her](talk-to-her.md)** — the web dashboard, Telegram, and Discord. 4. **[Read her files](read-her-files.md)** — what's in `~/.eternego/personas//home/`, and what each file is for. 5. **[Edit her](edit-her.md)** — change a fact about you, add an instruction by hand, grant her a permission. When you want the exhaustive, field-by-field reference behind any of these, every screen is in [The panel](../panel/index.md), every file in [Her files](../files/index.md), and every endpoint in the [API](../api/index.md). If a term is ever unclear, the [Vocabulary](../vocabulary.md) defines all of them. # getting-started/install.md # Install Pick the path for your machine. Builds aren't code-signed yet, so each OS will warn the first time — instructions for getting past the warning are inline. ## macOS Download [Eternego.dmg](https://git.eternego.ai/releases/eternego-ai/eternego/latest/Eternego.dmg). Open it, drag **Eternego** to **Applications**, then double-click Eternego from Applications. The first launch shows: *"Eternego.app cannot be opened because the developer cannot be verified."* Right-click (or Control-click) the app, choose **Open**, then **Open** again in the dialog. macOS remembers the choice — subsequent launches are normal. ## Windows Download [Eternego-setup.exe](https://git.eternego.ai/releases/eternego-ai/eternego/latest/Eternego-setup.exe). Walk through the wizard (Next → Install → Finish). Eternego launches automatically and adds Start Menu and Desktop shortcuts. The first dialog is *"Windows protected your PC"* (SmartScreen). Click **More info**, then **Run anyway**. SmartScreen remembers this app afterwards. ## Linux (.AppImage) Download [Eternego-x86_64.AppImage](https://git.eternego.ai/releases/eternego-ai/eternego/latest/Eternego-x86_64.AppImage), make it executable, run: ```bash chmod +x Eternego-x86_64.AppImage ./Eternego-x86_64.AppImage ``` A single self-contained binary. No system Python needed. ### Linux: unicode typing for screen control If she uses screen control to type unicode (accented letters, em-dashes, emoji, anything outside US-ASCII), the kernel's uinput layer can't produce those characters directly — uinput is keycode-only and bound by the active keyboard layout. She routes through the system clipboard instead. Install the tool for your session type: ```bash # Wayland (KDE, GNOME, sway, hyprland) — most modern Linux desktops sudo pacman -S wl-clipboard # Arch / Manjaro sudo apt install wl-clipboard # Debian / Ubuntu sudo dnf install wl-clipboard # Fedora # X11 — older desktops, or if XDG_SESSION_TYPE=x11 sudo pacman -S xclip sudo apt install xclip sudo dnf install xclip ``` Without one of these, ASCII typing still works (URLs, English text, code). Non-ASCII characters return a clear error pointing at the right package. The Docker image ships with `xclip` pre-installed (the container runs an Xvfb desktop). If you're running native, install the package matching your session above. ## Docker The image ships with the persona's own desktop baked in (Xvfb + fluxbox + noVNC) — she can click, type, and install browsers herself when you ask. Peek at what she's doing at `http://localhost:6080/vnc.html`. There's no prebuilt image to pull — you build it once from source (a few minutes), then it stays cached: ```bash eterngit clone git.eternego.ai eternego-ai/eternego cd eternego/installation/docker docker compose up -d --build ``` Persona files live in `~/.eternego` on the host — the same place the native install uses, so you can switch between Docker and native without losing data. Ollama running natively on the host is reached at `host.docker.internal:11434` out of the box; edit `docker-compose.yml` inline for ports, GPU access, or a sibling Ollama container. For the training-equipped image (~5.5 GB of CUDA wheels for LoRA fine-tuning), uncomment the `INSTALL_TRAINING: "true"` build arg in the compose file before `up`. ## Background service (CLI, auto-start on boot) The installers above launch when you open them. To register her as a system service so she keeps running across reboots: ```bash # Linux (systemd) / macOS (launchd) — auto-installs Python via apt/dnf/pacman/brew. See "Local models" below for Ollama. curl -fsSL https://git.eternego.ai/repos/eternego-ai/eternego/master/installation/install.sh | bash ``` ```powershell # Windows (Scheduled Task) — auto-installs Python via winget iwr -useb https://git.eternego.ai/repos/eternego-ai/eternego/master/installation/install.ps1 | iex ``` Both scripts accept `--full` (or `-Full` on Windows) to install training extras. ## From source (contributors) ```bash git clone git@git.eternego.ai:eternego-ai/eternego.git cd eternego bash installation/install.sh # Linux/macOS pwsh installation/install.ps1 # Windows ``` See [CONTRIBUTING.md](https://git.eternego.ai/repos/eternego-ai/eternego/master/CONTRIBUTING.md) before sending a PR. ## Local models (Ollama, optional) Eternego works with cloud providers (Anthropic, OpenAI, xAI, Gemini, OpenRouter, Groq) out of the box — paste an API key when creating your persona. If you'd rather she run on local models — no API keys, no cloud calls — install [Ollama](https://ollama.com) separately: ```bash # macOS brew install ollama # or download the installer from ollama.com/download # Linux curl -fsSL https://ollama.com/install.sh | sh # Windows: download the installer from ollama.com/download ``` When you create a persona and select **Local (Ollama)** with a model name (e.g. `qwen2.5:14b`, `llama3.1:8b`, `qwen2.5:32b`), Eternego pulls it for you automatically. If Ollama isn't installed or running, you'll see a clear message pointing back here. ## After install Your browser opens to `http://localhost:5000`. If port 5000 is taken on your machine, the daemon picks the next free one and prints which it chose. Continue to [Your first persona →](your-first-persona.md). # getting-started/your-first-persona.md # Your first persona The setup wizard is the door. When no persona exists yet, the dashboard opens straight onto it; you can also reach it any time at `http://localhost:5000/onboarding`. It asks for the few things she needs to come into being — and deliberately not for more. ## The chooser The cold screen offers two paths: - **Wake a new one up** — bring a brand-new persona into being. (Begin →) - **Bring one back** — restore a persona from a diary file you already have, using its recovery phrase. (Restore →) If this is your first time, choose **Wake a new one up**. (Restoring is covered in [Operating](../operating/index.md); it needs a `.diary` file and its recovery phrase.) ## What the create wizard asks for Two things are required: **her name** and **her Mind**. Channels are optional. That's the whole form — you won't describe her, give her a bio, or pick personality traits. She shows you who she is by talking, and she learns the rest over time. ### Step 1 — Her name What does she answer to? Something short — you'll write it many times (e.g. `Iris`). A name is enough to begin. ### Step 2 — Her Mind The Mind is her **Thinking organ** — the one required model. It's what recognizes what you said, decides what to do, reflects on her day, and remembers. Pick a model that's good at structured reasoning. **14B is the practical floor** — smaller models tend to produce too little usable output to be worth running; larger models work better. The Mind picker first asks **Local or Cloud**: **Local** — her thinking runs on this machine through [Ollama](https://ollama.com); nothing leaves it. You fill in: - **Model name** — the model id from Ollama, e.g. `qwen2.5:14b`. (If you haven't pulled one, see [ollama.ai](https://ollama.ai); Eternego pulls it for you on first use.) - **URL** *(advanced)* — defaults to `http://127.0.0.1:11434`. Leave it unless your Ollama runs elsewhere. Local needs real hardware: a 14B model wants roughly 10 GB of RAM just to load, so plan on **32 GB+** for the model, her context, and the OS together. Without a GPU, expect each beat to take minutes rather than seconds. No API key, though — nothing leaves the machine. **Cloud** — her thinking goes through a provider's API: fast and powerful, but the conversation passes through their servers. Pick a provider tile, then fill in the form: | Provider tile | Pre-filled URL | | --- | --- | | Anthropic | `https://api.anthropic.com` | | OpenAI | `https://api.openai.com` | | Gemini | `https://generativelanguage.googleapis.com` | | xAI | `https://api.x.ai` | | DeepSeek | `https://api.deepseek.com` | | Together | `https://api.together.xyz` | | OpenAI-compatible | *(you enter the endpoint)* | For each cloud provider you provide: - **URL** — pre-filled for the named providers; for **OpenAI-compatible** (LM Studio, vLLM, LiteLLM, Groq, OpenRouter, Fireworks, and any other OpenAI-style endpoint) enter the base URL yourself. Use the **base** endpoint only — Eternego appends `/v1` (or `/v1beta`, etc.) itself, so don't include it or the call 404s. - **API key** — stored locally, on this machine only. (It is written to her `config.json` in clear text — see [config.json](../files/config.md).) - **Model** — the model id exactly as the provider expects it, e.g. `claude-sonnet-4.5`. Once name and Mind are filled, **Wake her up** becomes active. ### Optional — Channels You can connect **Telegram** or **Discord** now, or leave it for later (Settings). She's always reachable in this browser regardless. - **Telegram** — create a bot via [@BotFather](https://t.me/BotFather), copy the token he gives you, paste it. - **Discord** — create an application at [discord.com/developers/applications](https://discord.com/developers/applications), add a bot, copy its token, paste it. A connected channel still needs **pairing** before she'll listen to it — see [Talk to her](talk-to-her.md#telegram-discord). ## Her other organs come later The create wizard asks only for her Mind. Her other senses — **Eye** (vision), **Mouth** (voice), **Ear** (hearing), **Imagination** (drawing), **Teacher** (a stronger model she consults when she meets a moment she has no instruction for), and **Researcher** (reads documents you send) — are all optional and are added afterward, on the dashboard's [Settings](../panel/settings.md) screen, using the same Local-or-Cloud form. A persona with only a Mind still works — for the organs that are really a *thinking* job, her Mind stands in: with no **Teacher** she consults her own thinking model when she meets a moment she has no instruction for, and with no **Researcher** that same model reads the documents you send. The ones her Mind genuinely can't fill are the senses and outputs — no **Eye** ⇒ she'll say she can't see an image, no **Ear** ⇒ she can't hear a voice note, no **Mouth** or **Imagination** ⇒ she can't speak aloud or draw. The full organ list is in the [Vocabulary](../vocabulary.md#her-body-organs). > Many cloud Minds already include vision — if yours does, she sees through her Mind and needs no separate Eye. ## The recovery phrase The moment she comes to life, she hands you a **recovery phrase — 24 words**. **Save it.** Her diary is encrypted with this phrase and nothing else — Eternego never holds a copy of the key. That's the point: the diary file can travel anywhere — a cloud backup, a USB stick, emailed to yourself — and stay unreadable to everyone but you. You hold the only key, so only you can move her to a new machine, and only you can ever read what she's learned about you. The trade for that guarantee: lose the phrase and no one, Eternego included, can bring it back — she keeps running on *this* machine, but can't be restored elsewhere. The wizard makes you **Copy phrase** or **Save as a file** before it lets you continue — that's the acknowledgement step. Then click **Meet \** to land on her view. ## What happens next The wizard is done; she's already running. You land on her main view, where you can talk to her right away. Continue to [Talk to her →](talk-to-her.md). # getting-started/talk-to-her.md # Talk to her Once she's running, there are two ways to reach her: the web dashboard and a chat channel (Telegram or Discord). Both land in the same persona — the same memory, the same continuity. ## Web dashboard `http://localhost:5000` is the door. Type in the composer, send, read her reply. If port 5000 was taken when the daemon started, it picked the next free one and printed which — check the launch output. You can also: - **Send her an image** — drag a file onto the chat, or use the attach button. She looks at it with her Eye (if she has one). Without an Eye organ she'll say she can't see it. - **Send her a voice clip** — the mic button records and uploads audio; she transcribes it with her Ear. - **Hear her out loud** — if she has a Mouth organ, her replies can be spoken. - **Ask her to draw** — if she has an Imagination organ, she can make an image on request and send it back inline: an illustration, a chart or diagram, or just something she pictures. It's how she answers in pictures, not only words. Every screen and control on the dashboard is documented in [The panel](../panel/index.md). ## Telegram / Discord If you gave her a bot token in the wizard (or added one later in Settings), you can also reach her there. Send the bot a message; she reads it on her next beat and replies on the same channel. A channel starts **unverified** — she ignores inbound messages until you pair it to your account: 1. Message her `/start` from your Telegram or Discord account. 2. She replies with a short pairing **code**. 3. Paste that code into the dashboard (Settings, or the pairing prompt) — this calls [`POST /api/persona/{id}/pair`](../api/channels.md). Once paired, the channel's `verified_at` is set and from then on she only listens to that one account. The web channel needs no verification — it's always open in your browser. ## Continue to [Read her files →](read-her-files.md) — see, on disk, what she's learning about you. # getting-started/read-her-files.md # Read her files Everything a persona is lives in plain files under `~/.eternego/personas//home/`. Markdown, JSON, JSONL — no database, no vendor format. Open any of it in any editor and you see exactly what she knows about you and how she's set up. This page is the quick tour. The field-by-field reference — every key, every shape, what writes it when — is [Her files](../files/index.md). > `~/.eternego/` is the default root. Set the `ETERNEGO_HOME` environment variable to put it somewhere else. ## The home directory The `` below is her persona id — the UUID from the dashboard, or the directory name itself. ``` ~/.eternego/personas//home/ ├── config.json ← her name, organs, status, channels, idle_timeout ├── person.md ← what she's learned about you ├── traits.md ← how you speak, decide, react — your patterns ├── persona-trait.md ← who she is with you, in her own words ├── wishes.md ← the directions you want to move in ├── struggles.md ← what holds you back ├── permissions.md ← what you've granted her, what you haven't ├── memory.json ← her live mind: messages + archive + context ├── conversation.jsonl ← the running transcript, one line per turn ├── health.jsonl ← one line per heartbeat — faults and signals ├── routines.json ← recurring lifecycle triggers (e.g. nightly sleep) ├── destiny/ ← future reminders & scheduled events (fire, then clear) ├── history/ ← fired reminders + past days' conversations ├── media/ ← drawings, voice clips, images she's looked at │ ├── gallery.jsonl ← one line per image, audio, or document she engaged with — profound-flagged media only, with a `kind` field │ └── screenshots/ ← screenshots she captured of her own screen ├── lessons/ ← raw lessons her Teacher wrote (pre-translation) ├── meanings/ ← her instructions (the folder keeps its code name) │ └── learned.json ← catalog: intention → instruction file └── training/ ← fine-tune batches (only when training is enabled) ``` A couple of names worth pinning down: - **`config.json`** is the serialized persona record. It holds her **organs** (the models she thinks, draws, speaks, sees, hears, teaches, and researches with — each with its own `name`, `provider`, `url`, and `api_key`), her **status**, her **channels**, and her timers. Her API keys are written here in clear text — protect the file. Full field list: [config.json](../files/config.md). - **`meanings/`** is the on-disk name for her **instructions**. An instruction is a situation she knows how to handle, written as a short procedure she follows. Everywhere operator-facing — these docs, the dashboard — they're called *instructions*; the folder just keeps its source name. See [Instructions](../files/instructions.md). Not every file exists from birth. `destiny/`, `history/`, `lessons/`, `meanings/`, and `training/` fill in as she lives and learns. Her **channels are stored inside `config.json`**, not in a separate file. ## What changes when Files change on different rhythms. Knowing which is which tells you when a hand edit of yours will be seen, and when something of hers will appear: - **Continuously, every beat** — `conversation.jsonl` (each turn), `memory.json` (her live mind state), `health.jsonl` (each heartbeat). This is her thinking persisting itself. - **When she learns something new** — a file appears in `lessons/` (the raw lesson her Teacher wrote) and a matching `.md` in `meanings/` (her own translation into an instruction), with `meanings/learned.json` updated to point at it. - **Nightly, or after she's been idle (`idle_timeout`)** — the *consolidate* stage distills `person.md`, `traits.md`, `persona-trait.md`, `wishes.md`, `struggles.md`, and `permissions.md` from the day's conversation, and archives + clears the live conversation. - **When you grant or refuse something** — `permissions.md`, when consolidate picks up an explicit grant, take, or refusal. Because consolidate rewrites the identity files, edit those **between** sessions rather than mid-conversation, so today's consolidation doesn't overwrite your change. (More on editing by hand in [Edit her](edit-her.md).) ## Her status `config.json` carries her vital **status** — always one of three values: | Status | Running? | Meaning | | --- | --- | --- | | `active` | yes | Awake and living her cycle. | | `hibernate` | no | Parked. Her agent is torn down — no cycles, no cost — until you wake her. | | `sick` | no | She hit a fault she couldn't recover from and took herself off the cycle so she doesn't loop. Fix the cause, set her back to `active`. | *Sleeping* isn't a stored status — it's an action: she runs her nightly ritual (consolidate → diary) and wakes back to `active`. See [status](../vocabulary.md#her-state-status). Setting status has real effects on her running process — see [Lifecycle](../api/lifecycle.md). ## Workspace `~/.eternego/personas//workspace/` is a separate directory she reads and writes **freely** — drafts, scripts she wrote, files she's working on. A sketchpad, not part of her identity. (Her `home/` she reads on every beat but doesn't rewrite at will; her `workspace/` is hers to scribble in.) ## Diary `~/.eternego/diary//.diary` is the encrypted nightly backup of her `home/`, written by her sleep ritual. It's the one thing needed to migrate her to a new machine — and the **recovery phrase** from the wizard is the key that unlocks it. It lives outside `personas//` so a `home/` backup never contains its own encrypted copy. ## Logs `~/.eternego/logs/` holds the daemon log (`eternego-.log`, one file per day, shared across all personas). Useful when she's behaving unexpectedly. Where logs land and what debug mode adds is covered in [Operating](../operating/index.md). ## Continue to [Edit her →](edit-her.md) — change something by hand and watch her adapt. # getting-started/edit-her.md # Edit her Eternego's design values transparency: everything she knows is a plain file on your disk — no database, no hidden state. Her memory files (`person.md`, `traits.md`, `persona-trait.md`, `wishes.md`, `struggles.md`, `permissions.md`) and the instructions she's learned (the `.md` files under `meanings/`) are all Markdown, open in any editor. **The preferred way to change her is to talk to her.** Every file here keeps itself current through conversation: tell her something about yourself and the nightly consolidate stage folds it into `person.md`; correct how she handled a moment and she adjusts; let her meet a situation she has no procedure for and she *learns* a new instruction for it. Editing by hand is the shortcut — for when you want a change to land this instant, or to fix something directly. ## Add a fact about yourself Open `~/.eternego/personas//home/person.md`. Add a line: ``` - Allergic to peanuts. Very. ``` Save. The next tick, she reads `person.md` (it's part of her identity, rebuilt every read) and incorporates it. From now on, she knows. This is the same pattern she uses to learn about you over time — each night, the consolidate stage updates these files based on what happened that day. You can write directly to them when you want her to know something faster than waiting for consolidate to catch it. ## Grant a permission `permissions.md` is her record of what you've allowed her to do, in plain prose. She reads it every interaction. Add a line: ``` - Run any command in /tmp freely. Don't touch anything outside. ``` The permission becomes part of how she decides. The next time she'd otherwise ask "may I run this?", she checks her permissions and acts if it's covered. This is permission-as-prose, not permission-as-config. She reads it, weighs it, asks if she's unsure. The trust is in the reading. ## Refine an instruction she's learned The situations she knows how to handle live as Markdown under `~/.eternego/personas//home/meanings/` — one file each. The first `# heading` is the *intention* (the kind of moment); the body is the *path* (how she handles it, in her own voice). Open one and refine the path — sharpen a step, add a caveat, cut something that misfires — and she follows the revised version the next time she's in that situation. What you don't do here is write one from scratch. She authors her own instructions by **learning**: when she meets a kind of moment she has no procedure for, she works one out and saves it herself. By hand, you refine what's already there — you don't seed the catalog. ## Tune her rhythm Open `~/.eternego/personas//home/config.json`. Among the other fields you'll see: ```json "idle_timeout": 3600 ``` This is how long, in seconds, she waits without activity before consolidating the day. Default is one hour. Set it to `1800` if you want her to consolidate every half hour, or `7200` for every two hours. The change takes effect on her next restart. ## What now You've installed her, set her up, talked to her, read her files, and edited her by hand. The rest of the docs go deeper: - **[Concepts](../concepts/index.md)** — what's actually happening under the hood. - **[Build & Extend](../build/index.md)** — add tools, abilities, meanings, channels, model providers in code. - **[Operating](../operating/index.md)** — running her over time, troubleshooting, migration. - **[API](../api/index.md)** and **[CLI](../cli/index.md)** — every endpoint and command, for operating her over HTTP or the shell. # panel/index.md # The panel The dashboard at `http://localhost:5000` — every screen you use to operate a persona, and the API call behind every control. This page is the map; the rest of the section is one page per screen. If you'd rather drive her over HTTP than click, every screen here is a thin client of the [HTTP API](../api/index.md). Nothing the panel does is private to the panel — a person clicking a button and an agent calling an endpoint take the same path. If you're an agent, start at [For agents](../for-agents.md). ## Opening it When the app or the [daemon](../vocabulary.md#where-she-lives) starts, your browser opens to `http://localhost:5000`. If port 5000 was taken, the daemon picked the next free one and printed which on startup — read that line, or check the browser tab. The port is also settable with `WEB_PORT` or the `--port` flag on [`eternego daemon`](../cli/index.md). There is **no login**. The panel is served on the loopback interface for the same machine the persona runs on. Don't expose the port to an untrusted network. The very first time you open it with no personas yet, you land on [Onboarding](onboarding.md) instead of a dashboard — there's no one to show. Create or restore a persona and the dashboard appears. ## Layout Every screen shares one frame: a fixed **sidebar** on the left, the active screen in the **main** area on the right. ![The panel's shared frame — a fixed sidebar on the left, the active screen in the main area (here, the Chat screen).](img/chat-page.png) ### Sidebar The left rail holds, top to bottom: - **Brand** — "Eternego" links to `/` (home). The **+** button next to it ("Bring another to life") routes to [Onboarding](onboarding.md). - **Persona card** — the persona you're viewing, with her [status](status.md) dot. Click it to switch between personas; the list is every persona from [`GET /api/personas`](../api/personas.md). - **Nav tabs** (under the label "Her") — the six screens below. The active one is highlighted. - **Footer** — a lock icon and either the build id or "running locally", a reminder that everything is on this machine. ### Main area Each screen owns its own header (a title and her status line), its own scroll, and its own live-data fetch. Switching tabs swaps the screen; the sidebar stays put. The URL tracks the screen — `/persona/{id}/chat`, `/persona/{id}/status`, and so on — so any screen is linkable and reload-safe. Some screens have a sub-route too (`/persona/{id}/settings/eye`, `/persona/{id}/memory/person`). ## The screens The sidebar labels and the docs page for each: | Sidebar label | URL tab | Page | What it's for | | --- | --- | --- | --- | | **Chat** | `chat` | [Chat](chat.md) | Talk to her — type, attach a file, record a voice note; watch her think. | | **Schedule** | `calendar` | [Calendar](calendar.md) | A month grid of what she's done and what she's planning. | | **Memory** | `memory` | [Memory and instructions](memory-and-instructions.md) | Read what she believes about you, file by file. | | **Instructions** | `instructions` | [Memory and instructions](memory-and-instructions.md) | Browse the situations she knows how to handle. | | **Status** | `status` | [Status](status.md) | Is she well? Lifecycle controls (wake, sleep, restart, hibernate) and a 24-hour uptime grid. | | **Settings** | `settings` | [Settings](settings.md) | Configure her organs, channels, appearance, lifecycle — and delete her. | The nav tab labeled **Schedule** maps to the `calendar` URL and the [Calendar](calendar.md) page — the label and the route name differ, but they're the same screen. [Onboarding](onboarding.md) is not in the sidebar — it's the screen you see before any persona exists, or when you click **+** to add one. ## Every screen is an API client The panel holds no state the API doesn't. On load it calls [`GET /api/personas`](../api/personas.md); each screen then fetches its own slice: | Screen | On open it calls | Live updates from | | --- | --- | --- | | Chat | [`GET .../conversation`](../api/knowledge.md#conversation) | [`WS /ws/{id}`](../api/websockets.md) | | Schedule | [`GET .../calendar`](../api/knowledge.md#calendar) | — | | Memory / Instructions | [`GET .../knowledge`](../api/knowledge.md#knowledge) | — | | Status | [`GET .../diagnose`](../api/knowledge.md#diagnose) | — | | Settings | the persona record from [`GET /api/personas`](../api/personas.md) | — | Actions post back: sending a message hits [`POST .../read`](../api/perception.md#read), a lifecycle button hits a [lifecycle route](../api/lifecycle.md), saving an organ hits [`POST .../update`](../api/lifecycle.md#update). Each screen's page names the exact endpoint behind every control. Only the Chat screen opens a [WebSocket](../api/websockets.md) (`/ws/{persona_id}`); it's how her words and her in-flight thinking stream in live. The other screens are request/response — they fetch once when you open them and refetch after an action. ## Related - [API overview](../api/index.md) — the control plane every screen is built on. - [For agents](../for-agents.md) — operate the panel by calling endpoints instead of clicking. - [Vocabulary](../vocabulary.md) — `persona`, `organ`, `channel`, `instruction`, the three `status` values. - [Getting started: talk to her](../getting-started/talk-to-her.md) — the gentle first walk-through. # panel/onboarding.md # Onboarding The create-or-restore wizard — the screen you see before any persona exists, or when you click **+** in the sidebar. It has two flows: **create** a new persona, or **migrate** (restore) one from a diary file. This page documents every field in both and the endpoint each submits to. You reach it at `/onboarding` (the choice screen), `/onboarding/create`, or `/onboarding/migrate`. With no personas at all, the app sends you here automatically. ## The choice screen `/onboarding` shows two cards: - **Wake a new one up** → the [create flow](#create-a-new-persona). - **Bring one back** → the [migrate flow](#restore-from-a-diary-migrate). A brand bar across the top reads "running locally on your machine". If you opened onboarding from an existing persona (via **+**), a "back to {name}" chip returns you to her chat. ## Create a new persona `/onboarding/create`. Three cards, stacked. Each unlocks the next once it's filled — the second is dimmed until you've named her, the third until her Mind is configured. Only the first two are required. ### Step 1 — Her name | Field | Type | Required | Notes | | --- | --- | --- | --- | | Name | text | **yes** | Her display name, e.g. `Iris`. That's all she needs to begin — no bio, no persona description, no traits. She shows you who she is through talking. | ### Step 2 — Her Mind The one required organ. This card is an **organ configurator** (the same control used in [Settings](settings.md#organ-cards) and the migrate flow). First pick **Local** or **Cloud**: **Local** (Ollama, runs on this machine): | Field | Type | Required | Notes | | --- | --- | --- | --- | | Model name | text | **yes** | The Ollama model id, e.g. `qwen2.5:14b`. Eternego pulls it for you if you haven't already. | | URL | text | no | Advanced. Defaults to `http://127.0.0.1:11434`. | A local Mind sends no provider and no API key. On the backend it's registered as a custom `eternego-` model wrapping the base you named. **Cloud** (a provider's API). Pick a provider tile, then fill three fields: | Field | Type | Required | Notes | | --- | --- | --- | --- | | URL | text | **yes** | The provider's **base** endpoint — pre-filled when you pick a tile. Do not append `/v1`; the platform layer adds the right suffix per provider. | | API key | password | **yes** | Stored locally on this machine only. | | Model | text | **yes** | The provider's model name, e.g. `claude-sonnet-4.5`. | The provider tiles offered are **Anthropic**, **OpenAI**, **Gemini**, **xAI**, **DeepSeek**, **Together**, and **OpenAI-compatible** (a custom endpoint, URL left blank for you to fill). The tile you pick becomes the `provider` slug sent to the backend. Of these, `anthropic`, `openai`, `gemini`, and `xai` have native handling; `deepseek`, `together`, and `openai_compat` are all driven through the OpenAI-compatible client — any provider that speaks the OpenAI chat protocol works by picking **OpenAI-compatible** and entering its URL. ### Step 3 — Channels (optional) A [channels configurator](settings.md#channels) in *deferred* mode: tokens you enter here are held and submitted together with the create request, not posted live. She's always reachable in this browser; Telegram and Discord are extra. | Field | Type | Notes | | --- | --- | --- | | Telegram bot token | password | Validated against Telegram at create time. | | Discord bot token | password | Validated against Discord at create time. | You can skip this entirely and add channels later in [Settings → Channels](settings.md#channels). ### Submitting **Wake her up** posts everything to [`POST /api/persona/create`](../api/create-migrate.md#create-a-persona) as JSON. The fields map to the request model like this: | Card field | Request field | | --- | --- | | Name | `name` | | Mind provider / model / URL / key | `thinking_provider`, `thinking_model`, `thinking_url`, `thinking_api_key` | | Telegram token | `telegram_token` | | Discord token | `discord_token` | The create screen only sends the **Mind** organ; the other six organs are added afterward in [Settings](settings.md). (The endpoint itself accepts all seven organ groups — see [Create and migrate](../api/create-migrate.md#create-a-persona) — but this screen doesn't expose them.) ### The recovery phrase On success the screen shows her **recovery phrase** — 24 words, shown once. This is the only key that unlocks her diary if you ever migrate her. You must **Copy** or **Save as a file** before the **Meet {name}** button enables. There is no way to see it again later; save it somewhere safe. The phrase comes back in the create response as `recovery_phrase` (see the [response shape](../api/create-migrate.md#response)). After you acknowledge it, the screen routes to her [Chat](chat.md). !!! note "Why it works this way" The phrase *is* the encryption key, and Eternego never keeps a copy — which is exactly what lets her diary be backed up or sent anywhere and still be readable only by you. The trade: lose it and no one (Eternego included) can recover it, so she keeps running here but can't be moved to another machine. See [Vocabulary: recovery phrase](../vocabulary.md#where-she-lives). ## Restore from a diary (migrate) `/onboarding/migrate`. Use this when you have a persona's `.diary` file and her recovery phrase — moving her to a new machine, or rebuilding after a reinstall. Three steps: ### Step 1 — Her diary | Field | Type | Required | Notes | | --- | --- | --- | --- | | Diary file | file | **yes** | Drag-and-drop or click to pick her `.diary` file (accepts `.diary` / `application/octet-stream`). It's decrypted with the phrase from step 2. | ### Step 2 — Her recovery phrase | Field | Type | Required | Notes | | --- | --- | --- | --- | | Recovery phrase | textarea | **yes** | Paste the words she gave you at creation. Order matters. | ### Step 3 — Her Mind The same Mind configurator as the create flow (Local / Cloud, identical fields). The Mind is **not** part of the diary — it's local to this install, so you choose how she thinks now. The other organs are restored from the diary. ### Submitting **Bring her back** posts to [`POST /api/persona/migrate`](../api/create-migrate.md#migrate-a-persona) as **multipart form** (because of the file upload). The fields map like this: | Step field | Form field | | --- | --- | | Diary file | `diary` | | Recovery phrase | `phrase` | | Mind model / provider / URL / key | `model`, `provider`, `url`, `api_key` | As with create, this screen sends only the Mind; the other organ form fields the endpoint accepts are not exposed here. On success there's no phrase reveal — the phrase is already yours — and the screen routes straight to her [Chat](chat.md). ## Errors Both flows surface the endpoint's error `detail` inline (an invalid key, a wrong phrase, a channel token that won't validate) and drop you back to the input step so you can fix it. The status codes and messages are documented in [Create and migrate](../api/create-migrate.md). ## Related - [Create and migrate](../api/create-migrate.md) — the endpoints behind both flows, every field and response. - [Settings](settings.md) — add the other organs and channels after creation. - [Getting started: your first persona](../getting-started/your-first-persona.md) — the guided walk-through. - [Vocabulary](../vocabulary.md) — `organ`, `channel`, `recovery phrase`, `diary`. # panel/chat.md # Chat The conversation screen — where you talk to her and watch her think. It's the default screen and the only one that holds a live [WebSocket](../api/websockets.md). URL: `/persona/{id}/chat`. Layout, top to bottom: a **header** (title, her [status](status.md), quick links to Status and Settings), an **organ strip** (which senses she has), the **conversation** stream, and the **composer** at the bottom. ## The header - **Title** — "Your conversation with {name}". - **Status line** — her current [status](status.md): Awake, Hibernating, or Something's wrong. - **Status** / **Settings** buttons — jump to those screens. ## The organ strip A row of chips, one per organ slot: **Mind**, **Imagination**, **Mouth**, **Eye**, **Ear**, **Teacher**, **Researcher**. Each shows *set* (configured) or *empty*. Hover for a one-line description of what it does. Clicking any chip jumps to that organ's tab in [Settings](settings.md#organ-cards) — an *empty* chip to add the organ, a *set* one to change it. Mind is always set — it's the one required organ. The strip reflects the persona record from [`GET /api/personas`](../api/personas.md); it has no endpoint of its own. ## The conversation stream Her messages render in serif on the left; yours in sans on the right. Markdown is rendered. Each bubble shows the speaker, a timestamp, and — for anything that came through a non-web channel — a "via telegram" / "via discord" tag. On open, the screen loads history from [`GET /api/persona/{id}/conversation`](../api/knowledge.md#conversation) and renders it. New activity then streams in live over the [per-persona WebSocket](../api/websockets.md#per-persona-socket) `/ws/{persona_id}`. The frames it renders: | Frame `type` | Becomes | Notes | | --- | --- | --- | | `chat_message` | one of her text bubbles | Her spoken words. | | `chat_image` | her bubble with an inline image | `url` points at [`GET .../media/{filename}`](../api/knowledge.md#media); click to open full size, or download. | | `chat_audio` | her bubble with an audio player | Newly-arrived audio from her **auto-plays**; historical audio (on reload) stays quiet until you press play. Only one audio plays at a time. | **Cross-channel mirrors.** When she acts on, or hears from, a channel other than the web (Telegram, Discord), the web socket never gets a `chat_message` for it — instead a richer signal arrives. The screen mirrors those into conversation bubbles so this view stays a complete record: `Said`/`Spoke`/`Drew` become her bubbles, `Read`/`Heard` become yours, each tagged with the channel name. (See [WebSocket frames](../api/websockets.md#frames-you-receive) for the signal shapes.) **The activity feed.** While she's mid-response, a live "thinking" bubble shows the stages firing this beat — `recognize`, `decide`, `learn`, `reflect`, tool calls. Stages that didn't need to run this beat are folded as "rest". When her message lands, that activity is snapshotted onto the message so you can expand it later and see what she did to produce those words. Pure background noise (heartbeats, health checks) is filtered out and never shown. ## The composer The input box at the bottom. A textarea plus three controls. | Control | Action | API call | | --- | --- | --- | | **Textarea** + **Enter** | Send a text message. (Shift+Enter inserts a newline.) | [`POST /api/persona/{id}/read`](../api/perception.md#read) with `{ "message": "" }` | | **Send** button | Same as Enter. | [`POST .../read`](../api/perception.md#read) | | **Stop** button | Appears in place of Send while she's responding *and* the textarea is empty. A soft interrupt. | [`POST .../read`](../api/perception.md#read) with `{ "message": "Stop" }` | | **Paperclip** (attach) | Pick a file — image, audio, PDF, or text. | [`POST .../attach`](../api/perception.md#attach) (multipart, field `file`) | | **Mic** | Record a voice note; click again to stop and send. | [`POST .../hear`](../api/perception.md#hear) (multipart, field `audio`) | A few behaviors worth knowing: - **Send and Stop** both go to `/read` — Stop simply sends the word `Stop` as a normal message, which she reads on her next beat. There is no hard kill of an in-flight beat from here; it's a polite interrupt. (The soft-interrupt window keeps the Stop button live for about 10 seconds after her last activity, then reverts to Send.) - **Attach** accepts `image/*`, `audio/*`, `.pdf`, `.csv`, `.json`, `.txt`, `.md` in the file picker. The backend routes by extension — images to her [Eye](settings.md#organ-cards), audio to her [Ear](settings.md#organ-cards), documents to her [Researcher](settings.md#organ-cards). A type with no matching capability is rejected by the endpoint with **415**. The full accepted set is in [Perception → Attach](../api/perception.md#attach). - **Mic** records in the browser (WebM/Ogg/MP4 depending on support) and uploads to `/hear`. The mic button is **disabled** if she has no [Ear](settings.md#organ-cards) — clicking it then jumps you to the Ear settings tab instead. The [`/hear` endpoint](../api/perception.md#hear) and `/attach` both ingest audio; `/hear` is the path for "she heard me speak just now", `/attach` for "I sent her an audio file". - The composer shows a small tip when she's missing an Eye ("she can't see images yet — give her an Eye") or Ear, linking to the right settings tab. Your typed/attached message echoes into the stream immediately and a "thinking" bubble lights up; her reply arrives over the socket. ## When she's not awake If her [status](status.md) is anything other than **active**, the composer is hidden and a card explains why, with a one-click way out: | Status | Card | Button | | --- | --- | --- | | **hibernate** | "She's hibernating." — long pause. | **Wake her up** → [`POST .../update`](../api/lifecycle.md#update) with `{ "status": "active" }` | | **sick** | "Something went wrong." — her Mind couldn't respond last beat. | **See Status** → the [Status](status.md) screen | The three statuses are defined in [Vocabulary](../vocabulary.md#her-state-status). (`asleep` isn't one — sleep is an action that runs her night ritual, then she's back to `active`.) ## Related - [Perception](../api/perception.md) — `read`, `hear`, `attach`, and `feed`, with every field and accepted file type. - [WebSockets](../api/websockets.md) — the live frames this screen consumes. - [Conversation](../api/knowledge.md#conversation) — the history endpoint loaded on open. - [Settings](settings.md) — add an Eye, Ear, Mouth so she can see, hear, and speak. - [Getting started: talk to her](../getting-started/talk-to-her.md). # panel/status.md # Status The health screen — is she well, and the controls to change her lifecycle. Come here when something feels off. URL: `/persona/{id}/status`. It opens by calling [`GET /api/persona/{id}/diagnose`](../api/knowledge.md#diagnose), which reads her vital status, her last persisted mind state, and a 24-hour uptime grid — all from disk, so this screen works even while she's stopped. ## The three statuses A persona's persisted status is always one of three values. The headline at the top of this screen, and the dot beside her name, reflect it: | Status | Headline here | Running? | What it means | | --- | --- | --- | --- | | **active** | "She's awake and well." | Yes | Living her cycle, responding when called. | | **hibernate** | "She's hibernating." | No | Parked — her agent is torn down, no cycles, no cost — until you wake her. | | **sick** | "Something's wrong." | No | She hit a fault she couldn't recover from and took herself off the cycle. | These are the only three values, defined once in [Vocabulary](../vocabulary.md#her-state-status). Note: `status` is the persisted vital state; it is distinct from her *phase* (the morning/day/night arc), which never shows here — *sleep* is an action that runs the night phase, not a status (see [Lifecycle controls](#lifecycle-controls)). ## Lifecycle controls The buttons under the headline change which state she's in. **Which buttons appear depends on her current status** — you only see the transitions that make sense from where she is. | When she's… | Buttons shown | | --- | --- | | **active** | Send her to sleep · Restart · Hibernate | | **hibernate** | Wake her up · Restart | | **sick** | Restart her · Hibernate | Each button maps to one endpoint: | Button | What it does | API call | | --- | --- | --- | | **Wake her up** | Bring her back to active (starts or restarts her agent). | [`POST /api/persona/{id}/update`](../api/lifecycle.md#update) with `{ "status": "active" }` | | **Send her to sleep** | Run her full nightly ritual — night phase, consolidate, diary, wake. Not an abrupt stop. | [`POST /api/persona/{id}/sleep`](../api/lifecycle.md#sleep) | | **Hibernate** | Park her — tear down her agent, no cycles. | [`POST /api/persona/{id}/update`](../api/lifecycle.md#update) with `{ "status": "hibernate" }` | | **Restart** / **Restart her** | Restart her running process (or start it if it was down). | [`POST /api/persona/{id}/restart`](../api/lifecycle.md#restart) | **Sleep vs. hibernate.** *Sleep* keeps her running — she does real work (consolidates the day, writes her diary, wakes herself into the next phase). That's why it has its own [`/sleep`](../api/lifecycle.md#sleep) endpoint rather than going through `/update`. *Hibernate* stops her entirely until you wake her. *Stop* (an abrupt pause with no ritual) isn't on this screen — it lives in [Settings → Lifecycle](settings.md#lifecycle). After any of these, the screen refetches so the headline and dot reflect the new state. ## When she's sick If she's **sick**, a "What happened" card appears above the uptime grid: her Mind couldn't respond on the last beat — most likely an API key issue or her provider having a moment. It points you at two fixes: **Restart her** (the button above) or change her Mind to a working provider, linking to [Settings](settings.md#organ-cards). The card phrases the provider line generically ("her provider"); the [diagnose response](../api/knowledge.md#diagnose)'s `mind` is her last persisted mind state (`messages`, `archive`, `context`) and carries no provider name, so no specific provider is shown. ## The 24-hour uptime grid "How she's been" — a grid of minute-cells, 24 hours' worth, most recent first (the grid is built latest-first: the top-left cell is the latest minute, the bottom-right the oldest). Each cell is colored: - **awake** — a beat ran that minute. - **asleep** — no beat that minute. - **sick** — a beat ran but a fault fired. - **not yet** — no data for that minute. Each cell ≈ 1 minute. The grid is built from the `uptime.rows` in the [diagnose response](../api/knowledge.md#diagnose) (24 rows × 60 cells, each carrying `tick` and `fault`). It's a quick visual of whether she's been alive and healthy. ## Feed her a past conversation At the bottom is a **feed** form (also on the [Memory](memory-and-instructions.md) screen). Drop a conversation export from another AI — ChatGPT/OpenAI or Claude/Anthropic — and she reads through it, takes what's useful, and folds it into her memory. | Field | Type | Notes | | --- | --- | --- | | Conversation file | file | A `.json`, `.txt`, or `.md` export. | | Where's it from? | select | `ChatGPT / OpenAI` (`openai`) or `Claude / Anthropic` (`claude`). | **Feed her** posts to [`POST /api/persona/{id}/feed`](../api/perception.md#feed) as multipart (`history` = the file, `source` = the select value). This is a live-agent route — she must be **active** for it to work, or it returns **409**. Give her a few beats afterward to work through it. ## Related - [Lifecycle](../api/lifecycle.md) — start, stop, restart, sleep, update, delete, with exact transitions. - [Diagnose](../api/knowledge.md#diagnose) — the snapshot this screen renders. - [Feed](../api/perception.md#feed) — importing past conversations. - [Vocabulary: status](../vocabulary.md#her-state-status) — the three statuses. - [Operating](../operating/index.md) — logs and deeper troubleshooting when she's sick. # panel/memory-and-instructions.md # Memory and instructions Two sidebar screens, one data source. **Memory** (`/persona/{id}/memory`) shows what she believes about you; **Instructions** (`/persona/{id}/instructions`) shows the situations she knows how to handle. Both are populated by a single call to [`GET /api/persona/{id}/knowledge`](../api/knowledge.md#knowledge), which returns her `memory` files and her `instruction` catalog together. Both screens are read-mostly: she forms this content herself, from conversation. You read it, and (on Memory) you can correct it by editing the underlying files on disk. !!! warning "This data is unmasked" The knowledge endpoint returns file bodies verbatim. If a persona has written a secret into one of her own instructions, it shows here in the clear. See the [Knowledge endpoint](../api/knowledge.md#knowledge). ## Memory Title: "What she remembers." A two-pane reader — a list of her memory files on the left, the selected file's body (rendered markdown) on the right. She writes these beliefs about you over time; she does not freely rewrite them on demand, and reflection consolidates them in the background. To correct something, edit the file directly — see [Her files: identity](../files/identity.md). The files the screen defines a label for, the on-disk file each describes, and the key the screen looks for in the knowledge response: | Label | Underlying file | Looks for key | What it holds | | --- | --- | --- | --- | | Person | `person.md` | `person` | What she remembers about who she's talking to. | | Traits | `traits.md` | `traits` | Character traits she's observed in you. | | Self | `persona-trait.md` | `persona-trait` | Traits about herself that you seem to bring out. | | Wishes | `wishes.md` | `wishes` | Things you've said you want. | | Struggles | `struggles.md` | `struggles` | Things you've said are hard. | | Permissions | `permissions.md` | `permissions` | What she's allowed (and forbidden) to do. | What actually appears as content is whatever [`GET .../knowledge`](../api/knowledge.md#knowledge) returns — its `memory` object is a `{key: body}` map that **omits empty files**. The endpoint reads all six files and emits each under the exact key the screen looks for: `person`, `traits`, `persona-trait`, `wishes`, `struggles`, `permissions`. The keys line up one-to-one with the table above, so every tab renders its content once the underlying file has been written. A fresh persona with nothing written yet returns none of the keys, and the screen falls back to listing all six labels, each reading "not written yet". All six files, and what writes to them, are documented in [Her files: identity](../files/identity.md). **Sub-routes.** Clicking a file updates the URL to `/persona/{id}/memory/{file}` so a specific file is linkable. Switching files is a client-side selection; the screen does not refetch per file — it has the whole `memory` map from the one knowledge call. A **feed** form sits below the reader, the same one on the [Status](status.md#feed-her-a-past-conversation) screen — import a past conversation from another AI and she folds it into her memory. It posts to [`POST .../feed`](../api/perception.md#feed). ## Instructions Title: "What she's learned." A catalog of her **instructions** — the kinds of moments she knows how to be in. Each row is an [intention](../vocabulary.md#her-mind) (the title); click it to expand the [path](../vocabulary.md#her-mind) (the steps). The list comes from the `instruction` array of [`GET .../knowledge`](../api/knowledge.md#knowledge). Two sections: - **Custom — she taught herself these.** Instructions this persona wrote, living in her `meanings/` directory (her instructions on disk). Empty for a fresh persona — she writes her own as she meets new kinds of moments. - **Built-in.** Instructions that ship with Eternego. Each entry carries `intention`, `body`, and `source` (`builtin` or `custom`) — see the [knowledge response fields](../api/knowledge.md#knowledge). !!! note "Code name" What this screen and the rest of the docs call an **instruction** is named a *meaning* in the source (the `meanings/` directory keeps its code name on disk). Same thing — see [Vocabulary](../vocabulary.md#her-mind). ### Removing a custom instruction Custom entries show a **Remove** button (with a confirm dialog). **This control is not yet wired to the backend** — there is no delete endpoint for instructions, and clicking through the confirm currently shows a "not yet wired" notice rather than deleting anything. To remove a custom instruction today, delete its file from her `meanings/` directory on disk; see [Her files: instructions](../files/instructions.md). Built-in instructions have no Remove button. ## Related - [Knowledge](../api/knowledge.md#knowledge) — the single endpoint behind both screens. - [Her files: identity](../files/identity.md) — the memory files, and editing them to correct her. - [Her files: instructions](../files/instructions.md) — where instructions live on disk and how to add or remove one. - [Vocabulary](../vocabulary.md#her-mind) — `instruction`, `intention`, `path`, `lesson`. - [Feed](../api/perception.md#feed) — importing past conversations into her memory. # panel/calendar.md # Calendar Her schedule as a month grid — what she's done (past) and what she's planning (future). Sidebar label: **Schedule**. URL: `/persona/{id}/calendar`. The screen is built entirely from [`GET /api/persona/{id}/calendar`](../api/knowledge.md#calendar), which returns a window of her **history** (past events she's archived) and her **destiny** (future events she's scheduled). It's a read-only view of her files — there are no controls to add or edit events here. ## What it shows A standard month grid (weeks start Monday). Today is highlighted; the selected day is outlined. Each day cell shows up to three events; more collapse to a "+N more". Below the grid, a popover lists every event for the selected day in full. Events come from two halves of the calendar response, distinguished visually by **origin**: | Origin | Source in the response | Meaning | | --- | --- | --- | | **you added** | `history` | Past events — conversations, reminders, things from your side. | | **she set up** | `destiny` | Future events she's scheduled for herself. | Each event shows a time, a title, and (for recurring ones) the recurrence — e.g. "daily", "weekly". ### How titles are derived The response gives each event a `body` (the file's markdown), not a clean title, so the screen derives one: - **Conversations** are always titled "Conversation" — their body is the raw chat log, never a usable title. - **Schedules and reminders** take the first heading line of the body (a leading `#` is stripped); if there isn't a usable one, the kind name is capitalized ("Schedule", "Reminder"). The `recurrence` shown in the day popover is the `recurrence` field from a `destiny` entry. It's informational — Eternego does **not** project recurring events forward; you only see what's actually on disk for that window. (See the [Calendar endpoint](../api/knowledge.md#calendar).) ## Navigation | Control | Action | | --- | --- | | **‹ / ›** | Previous / next month. | | **Today** | Jump back to the current month and select today. | | Click a day | Select it; its events fill the popover below. | Changing the month **refetches**: the screen requests [`GET .../calendar?start=&end=`](../api/knowledge.md#calendar) with that month's bounds — `start` is the first of the month, `end` the first of the next (the window is `start <= event_time < end`). On first open it loads the current month the same way. Both query params are ISO dates; the endpoint accepts a bare date (`2026-06-01`) or a full timestamp. Out-of-range or malformed dates return **400** — but the screen always sends well-formed month bounds. ## Related - [Calendar](../api/knowledge.md#calendar) — the endpoint, its query params, and the exact `history`/`destiny` response shape. - [Her files: runtime](../files/runtime.md) — where `history/` and `destiny/` live on disk, and what writes them. - [Vocabulary](../vocabulary.md) — `destiny`, the day-arc the persona lives on. # panel/settings.md # Settings How she's configured — her organs, her channels, the app's appearance, her lifecycle, and the one irreversible action. URL: `/persona/{id}/settings`, with a sub-tab per section: `/persona/{id}/settings/{tab}`. A strip of sub-tabs runs across the top; the body shows the active one. The sub-tabs are: **Mind**, **Imagination**, **Mouth**, **Eye**, **Ear**, **Teacher**, **Researcher** (the seven [organs](../vocabulary.md#her-body-organs)), then **Channels**, **Appearance**, **Lifecycle**, and **Danger**. The persona record this screen edits comes from [`GET /api/personas`](../api/personas.md). ## Organ cards The first seven tabs each configure one [organ](../vocabulary.md#her-body-organs). They share one control (the **organ card**). Each shows what the organ does, its current state, and a form to set or change it. The seven slots and what each is for: | Tab | Slot | What it does | Required? | | --- | --- | --- | --- | | **Mind** | `thinking` | Every thought runs through this model — recognize, decide, reflect, remember. | **Yes** | | **Imagination** | `imagination` | Draws images. | No | | **Mouth** | `mouth` | Turns text into voice. | No | | **Eye** | `eye` | Looks at images you send. (Skip if her Mind already sees — most modern ones do.) | No | | **Ear** | `ear` | Turns voice notes into text. | No | | **Teacher** | `teacher` | A stronger model she consults rarely, for hard questions. | No | | **Researcher** | `researcher` | Reads documents (PDF, csv, json, txt, md) without flooding her memory. PDF needs an Anthropic or Gemini model. | No | The tab is labeled **Mind** but the slot it writes is `thinking` — that mapping matters when you call the API directly. ### State and the configurator An organ is either **set** (shows provider, model, URL, with **Change** and — if optional — **Remove**) or **empty** (a prompt to add one). Either way, the form behind it is the **organ configurator**, the same control as in [Onboarding](onboarding.md#step-2-her-mind). Pick **Local** or **Cloud**: - **Local** (Ollama): just a **Model name** (e.g. `qwen2.5:14b`) and an optional **URL** (default `http://127.0.0.1:11434`). No provider, no key. - **Cloud**: pick a provider tile, then **URL** (base endpoint, pre-filled — no `/v1`), **API key** (stored locally), and **Model**. Provider tiles: **Anthropic**, **OpenAI**, **Gemini**, **xAI**, **DeepSeek**, **Together**, **OpenAI-compatible**. Of these, `anthropic`, `openai`, `gemini`, and `xai` are handled natively; `deepseek`, `together`, and `openai_compat` route through the OpenAI-compatible client — any OpenAI-protocol endpoint works via the **OpenAI-compatible** tile. ### Save and remove | Action | What it does | API call | | --- | --- | --- | | **Save** | Set or change this organ. | [`POST /api/persona/{id}/update`](../api/lifecycle.md#update) with `{ "": { "provider", "model", "url", "api_key" } }` | | **Remove** | Clear this organ (optional organs only). | [`POST /api/persona/{id}/update`](../api/lifecycle.md#update) with `{ "clear_": true }` | Both go through [`/update`](../api/lifecycle.md#update). The slot name is `thinking` for the Mind tab, otherwise the lowercase organ name (`eye`, `ear`, …). Changing an organ **restarts her** if she's running — the card warns you ("she'll lose this conversation's working memory but her diary is safe"). Her saved API key is never returned to the screen, so the form is blank when you re-open a set organ to change it. !!! note "The card doesn't validate the model — the endpoint does" Save sends your input straight to [`/update`](../api/lifecycle.md#update), which validates the provider/model/key against the real provider and returns **400** with a reason if it can't be prepared. The screen surfaces that message. ## Channels Ways to reach her from outside this browser. She lives on this machine; channels are how you talk to her from elsewhere. The web channel is **always on** and can't be removed. Today's external channels are **Telegram** and **Discord** (bot tokens). Each external channel has three states: 1. **Not connected** — a **Connect** button opens a token form. 2. **Verifying** (added but not yet paired) — a pairing form appears: open the bot, send `/start`, and paste the one-time code it replies with. 3. **Connected** (paired) — shows the handle and a **Remove** button. The controls and their endpoints: | Control | What it does | API call | | --- | --- | --- | | **Connect** (paste token) | Validate the bot token and add the channel. | [`POST /api/persona/{id}/channels`](../api/channels.md#add-channel) with `{ "kind": "telegram"\|"discord", "credentials": { "token": "" } }` | | **Pair** (paste code) | Bind the channel to your account so she only listens to you. | [`POST /api/persona/{id}/pair`](../api/channels.md#pair) with `{ "code": "" }` | | **Remove** | Drop the channel. | [`DELETE /api/persona/{id}/channels/{channel_name}`](../api/channels.md#remove-channel) | `channel_name` for removal is the channel's handle (the `name` from her [persona record](../api/personas.md)). Adding or removing a channel **restarts her** to pick up (or drop) the gateway. Until you've paired a verified channel, she won't act on messages from it. ## Appearance How the app looks to you. One control: | Field | Options | Effect | | --- | --- | --- | | Theme | **Dark**, **Light**, **System** | Sets the UI theme. | This is **purely client-side** — the choice is saved in your browser's `localStorage` (`eg.theme`) and applied immediately. It has **no API call** and is per-browser, not per-persona; it isn't stored in her files. ## Lifecycle The same lifecycle controls as the [Status](status.md#lifecycle-controls) screen, here for when you're already configuring her. Shows her current status, then buttons: | Button | What it does | API call | | --- | --- | --- | | **Wake her up** | Bring her to active. | [`POST .../update`](../api/lifecycle.md#update) `{ "status": "active" }` | | **Send her to sleep** | Full nightly ritual. | [`POST .../sleep`](../api/lifecycle.md#sleep) | | **Stop** | Abrupt pause — tear down her agent now, no ritual. | [`POST .../stop`](../api/lifecycle.md#stop) | | **Restart** | Restart her process. | [`POST .../restart`](../api/lifecycle.md#restart) | | **Hibernate** | Park her until you wake her. | [`POST .../update`](../api/lifecycle.md#update) `{ "status": "hibernate" }` | Unlike the [Status](status.md) screen (which shows only the transitions valid from her current state), this tab always shows all five — including **Stop**, which the Status screen doesn't. *Stop* differs from *Hibernate*: Stop tears down the running agent but leaves her status as-is; Hibernate sets her status to `hibernate`. And both differ from *Sleep*, which keeps her running through her consolidate-and-diary ritual. See [Lifecycle](../api/lifecycle.md) for exact transitions. ## Danger The one decision that can't be undone: **Delete {name}**. Her diary, her memory, her instructions — all gone. Without her recovery phrase and a saved diary file, deletion is permanent. The button opens a confirm dialog that requires you to **type her name** to proceed. On confirm it calls [`POST /api/persona/{id}/delete`](../api/lifecycle.md#delete), which stops her if running and removes her from disk, then returns you to the home screen. !!! danger "Permanent" There is no undo. If you might want her back, export her diary first ([`GET .../export`](../api/knowledge.md#export)) and keep her [recovery phrase](../vocabulary.md#where-she-lives). ## Related - [Lifecycle](../api/lifecycle.md) — `update` (organs + status), `sleep`, `stop`, `restart`, `delete`. - [Channels](../api/channels.md) — add, remove, and pair channels. - [Create and migrate](../api/create-migrate.md) — the same organ fields, at creation time. - [Vocabulary](../vocabulary.md#her-body-organs) — the seven organs, channels, the three statuses. - [Onboarding](onboarding.md) — the configurator's first appearance. # files/index.md # Her files Everything a persona is lives in plain files under `~/.eternego/`. No database, no vendor lock-in: open any file in any editor and you see exactly what she knows. This section is the field-by-field reference to that tree — what every file holds, what shape it has, and what writes it when. She **reads** her home on every beat to know who she is; she does not freely rewrite it. The files that change do so through her cognitive cycle (mostly the *consolidate* stage) or through specific [API](../api/index.md) calls, never by her editing her own identity at will. You can edit any of it by hand — change a line and she adapts on her next beat. `~/.eternego/` is the default root. Override it with the `ETERNEGO_HOME` environment variable (the test suite uses this to sandbox a tempdir). ## The tree ``` ~/.eternego/ ├── personas/ │ └── / │ ├── home/ ← her identity — read on every beat │ │ ├── config.json her name, organs, status, channels, timeouts │ │ ├── person.md what she's learned about you │ │ ├── traits.md how you behave — your patterns │ │ ├── persona-trait.md who she is with you (her own traits) │ │ ├── wishes.md the directions you want to move in │ │ ├── struggles.md what holds you back │ │ ├── permissions.md what you've granted her, what you haven't │ │ ├── memory.json her live mind: messages + archive + context │ │ ├── conversation.jsonl append-only transcript, one line per turn │ │ ├── health.jsonl one line per heartbeat — faults and signals │ │ ├── routines.json recurring lifecycle triggers (e.g. nightly sleep) │ │ ├── meanings/ her instructions (folder keeps its code name) │ │ │ ├── .md one instruction: the path body (read verbatim) │ │ │ └── learned.json {intention → file stem} catalog │ │ ├── lessons/ raw lessons her Teacher wrote (pre-translation) │ │ │ └── .md │ │ ├── destiny/ future reminders & scheduled events (fire then clear) │ │ │ └── --.md │ │ ├── history/ fired reminders + archived daily conversations │ │ │ ├── briefing.md index of history entries │ │ │ └── -.md │ │ ├── media/ drawings, voice clips, captioned images │ │ │ ├── gallery.jsonl one line per image, audio, or document she engaged with (profound-flagged only) │ │ │ └── screenshots/ screenshots she captured of her own screen │ │ └── training/ fine-tune batches (when training is enabled) │ └── workspace/ her sketchpad — she reads and writes freely ├── diary/ │ └── / │ └── .diary encrypted nightly backup of home/ ├── logs/ here if packaged/installed; ./logs from a source clone │ ├── eternego-.log daemon narrative, one file per day (all personas) │ ├── eternego-signals-.log raw signal stream (all personas) │ └── eternego--.log per-persona log (debug mode only) └── fine_tune/ └── /adapter/ persistent LoRA adapter (when trained) ``` Each persona owns two subtrees: `home/` (her identity, read-only to her) and `workspace/` (her free scratch space). The `diary/`, `logs/`, and `fine_tune/` trees are shared roots keyed by persona id, kept outside `personas//` so a `home/` backup never contains its own encrypted copy. `config.json` is named `config.json` on disk; the code calls its path function `persona_identity`. The `meanings/` directory is literally named `meanings/` on disk — that is the code's internal word for her **instructions**. Everywhere operator-facing, including the dashboard, these are called *instructions*; the folder just keeps its source name. See [Instructions](instructions.md). ## What writes when Files change on different rhythms. Knowing which is which tells you when an edit of yours will be seen, and when one of hers will appear. | Cadence | Trigger | Files written | | --- | --- | --- | | **Continuous** | Every beat / every message / every heartbeat | `conversation.jsonl` (each turn), `memory.json` (live mind state), `health.jsonl` (each heartbeat) | | **On a new instruction** | The *learn* stage, when she meets a kind of moment she has no instruction for | `lessons/.md` (Teacher's raw lesson), `meanings/.md` (her translated instruction), `meanings/learned.json` (catalog updated) | | **On refining an instruction** | The *reflect* stage — during the day, at the close of a procedure, when living the instruction she just used revealed a better version | `meanings/.md` (that one instruction's body rewritten) | | **Nightly / when idle** | The *consolidate* stage — at NIGHT, or after `idle_timeout` of quiet during the day | `person.md`, `traits.md`, `wishes.md`, `struggles.md`, `persona-trait.md`, `permissions.md` (consolidated), `memory.json` (conversation archived + cleared, `context` rewritten) | | **On sleep** | Her nightly ritual (`POST /api/persona/{id}/sleep` or the daily `sleep` routine) | `diary//.diary` (encrypted backup), then consolidate as above; `conversation.jsonl` cleared after archiving | | **At birth** | `POST /api/persona` create | the whole `home/` tree scaffolded, `config.json` written, first `diary` saved, recovery phrase stored in the OS keyring | | **On a reminder firing** | The heartbeat finds a due `destiny/` entry | entry copied to `history/`, deleted from `destiny/`, injected into `memory.json` as a `due for:` message | Continuous writes happen inside the running daemon process — they are her thinking persisting itself. The consolidate-cadence writes are where your hand edits to identity files would be overwritten if today's conversation touched the same area, so edit those between sessions, not mid-conversation. ## The pages - **[config.json](config.md)** — every configuration field: name, organs, channels, status, `idle_timeout`, birthday. - **[Identity files](identity.md)** — `person.md`, `traits.md`, `persona-trait.md`, `wishes.md`, `struggles.md`, `permissions.md`. - **[Runtime files](runtime.md)** — `memory.json`, `conversation.jsonl`, `health.jsonl`, `routines.json`, `destiny/`, `history/`. - **[Instructions](instructions.md)** — the `meanings/` directory, `lessons/`, `learned.json`. - **[Workspace, diary, logs](workspace-diary-logs.md)** — `workspace/`, the encrypted `diary/`, and `logs/`. ## Related - [Read her files](../getting-started/read-her-files.md) — a gentler tour of the same tree. - [Vocabulary](../vocabulary.md) — every term used here, defined. - [Knowledge API](../api/knowledge.md) — read her memory, conversation, and calendar over HTTP. - [Concepts](../concepts/index.md) — the *why* behind where her knowledge lives. # files/config.md # config.json `~/.eternego/personas//home/config.json` is the persona's configuration: her name, her organs (the models she thinks, draws, speaks, sees, hears, teaches, and researches with), her channels, her vital status, and her timers. It is the serialized `Persona` dataclass (`application/core/data.py`). The dashboard's [Settings](../panel/settings.md) screen and the [create](../api/create-migrate.md) / [update](../api/personas.md) endpoints all write this file; you can also edit it by hand while she's hibernating. Fields serialize in dataclass declaration order, with no masking — **the real API keys and channel credentials are written to this file in clear text.** Protect it accordingly. (The HTTP API masks secrets in some views but `config.json` on disk does not.) ## Top-level fields | Field | Type | Default | Meaning | | --- | --- | --- | --- | | `id` | string | a fresh UUID4 | Her permanent identity. The directory name under `personas/`. Never changes. | | `name` | string | — (required) | Her name. Shown in the dashboard and used in her self-reference. | | `thinking` | object (Model) | — (required) | Her Mind organ — the one required model. Recognizes, decides, reflects, remembers. See [organ object](#the-organ-object). | | `version` | string | `"v1"` | Internal schema version of the persona record. | | `base_model` | string | `""` | The base model id a LoRA adapter was trained on, when fine-tuning is used. Empty otherwise. | | `birthday` | string | today's date (`YYYY-MM-DD`) | The day she was created. | | `status` | string | `"active"` | Her vital state. One of `active`, `hibernate`, `sick`. See [Status](#status). | | `idle_timeout` | integer (seconds) | `3600` | How long she may sit with unconsolidated conversation during the day before *consolidate* folds it into memory. One hour by default. See [idle_timeout](#idle_timeout). | | `imagination` | object (Model) or `null` | `null` | Her Imagination organ — draws images. Absent ⇒ she can't draw. | | `mouth` | object (Model) or `null` | `null` | Her Mouth organ — turns text into voice. | | `eye` | object (Model) or `null` | `null` | Her Eye organ — looks at images and reports what it sees. | | `ear` | object (Model) or `null` | `null` | Her Ear organ — turns audio into text. | | `teacher` | object (Model) or `null` | `null` | Her Teacher organ — a stronger model she consults when she meets a moment she has no instruction for. | | `researcher` | object (Model) or `null` | `null` | Her Researcher organ — reads documents you send and answers about them without flooding her own memory. | | `channels` | array (Channel) or `null` | `null` | The outside channels she listens on. `null` or `[]` when she has none. See [the channel object](#the-channel-object). | Only `name` and `thinking` are required. Every other organ is optional. A `null` `teacher` or `researcher` falls back to her `thinking` model — the Mind does that work itself, so it's an un-upgraded capability rather than a missing one. A `null` sense or output organ *is* a real gap: no `eye` ⇒ she can't look at images, no `mouth` ⇒ she can't speak aloud. The full organ list and what each does is in the [Vocabulary](../vocabulary.md#her-body-organs). ## The organ object Each organ (`thinking`, `imagination`, `mouth`, `eye`, `ear`, `teacher`, `researcher`) is a `Model`: | Field | Type | Default | Meaning | | --- | --- | --- | --- | | `name` | string | — (required) | The model id, exactly as the provider expects it (e.g. `gpt-5.4`, `claude-sonnet-4-6`, `qwen2.5:14b`). | | `provider` | string or `null` | `null` | The provider slug: `openai`, `anthropic`, `xai`, `gemini`, `ollama`, or any OpenAI-compatible endpoint. `null` is treated as Ollama (local). | | `api_key` | string or `null` | `null` | The provider API key. Stored in clear text here. `null` for local (Ollama) models. | | `url` | string | — (required) | The provider base URL, e.g. `https://api.openai.com`. | ## The channel object Each entry in `channels` is a `Channel`: | Field | Type | Default | Meaning | | --- | --- | --- | --- | | `type` | string | — (required) | The channel kind: `telegram`, `discord`, or `web`. | | `name` | string | `""` | The channel-specific address: `chat_id` for Telegram, `channel_id` for Discord, the persona id for web. | | `credentials` | dict or `null` | `null` | The bot token / connection secrets. Stored in clear text here. | | `verified_at` | string or `null` | `null` | ISO-8601 timestamp set when you pair the channel to your account. Until set, she ignores inbound messages on that channel (except `web`, which needs no verification). | Channels are added and verified through [`POST /api/persona/{id}/pair`](../api/channels.md), not by hand. A persona created without channels has `"channels": null`. ## status Exactly one of three values. Setting it has real effects — see [Lifecycle](../api/lifecycle.md) for the transitions. | Value | Running? | Meaning | | --- | --- | --- | | `active` | yes | Awake and living her cycle. | | `hibernate` | no | Parked — her agent is torn down, no cycles, no cost — until you wake her. | | `sick` | no | She hit a fault she couldn't recover from and took herself off the cycle. Fix the cause, set her back to `active`. | `status` is a *persisted vital state*, distinct from her *phase* (MORNING / DAY / NIGHT, the day-arc). A running persona in her NIGHT phase still has `status: active`. Phase lives only in her live memory, never in `config.json`. ## idle_timeout The number of **seconds** of daytime quiet she tolerates before *consolidate* folds the conversation into her long-term files. Lower it and she folds the day away sooner (more frequent consolidation, fresher memory, more model calls); raise it and she keeps the live conversation longer. The nightly NIGHT-phase consolidation happens regardless of this value. Default `3600` (one hour). ## Example A real `config.json` with every organ filled (an all-OpenAI persona), keys masked: ```json { "id": "6c17c83c-3158-450d-8e43-0e7efea717c1", "name": "Adam", "thinking": { "name": "gpt-5.4", "provider": "openai", "api_key": "sk-XXXX", "url": "https://api.openai.com" }, "version": "v1", "base_model": "", "birthday": "2026-06-03", "status": "active", "idle_timeout": 3600, "imagination": { "name": "gpt-image-1", "provider": "openai", "api_key": "sk-XXXX", "url": "https://api.openai.com" }, "mouth": { "name": "gpt-4o-mini-tts", "provider": "openai", "api_key": "sk-XXXX", "url": "https://api.openai.com" }, "eye": { "name": "gpt-4o", "provider": "openai", "api_key": "sk-XXXX", "url": "https://api.openai.com" }, "ear": { "name": "gpt-audio", "provider": "openai", "api_key": "sk-XXXX", "url": "https://api.openai.com" }, "teacher": { "name": "gpt-5.5", "provider": "openai", "api_key": "sk-XXXX", "url": "https://api.openai.com" }, "researcher": { "name": "gpt-5.3", "provider": "openai", "api_key": "sk-XXXX", "url": "https://api.openai.com" }, "channels": null } ``` A persona with a verified Telegram channel has, instead of `"channels": null`: ```json "channels": [ { "type": "telegram", "name": "123456789", "credentials": { "token": "0000000000:XXXX" }, "verified_at": "2026-04-15T23:25:22.683402+02:00" } ] ``` ## Related - [Settings screen](../panel/settings.md) — the dashboard editor for this file. - [Create & migrate API](../api/create-migrate.md) — the endpoints that write it at birth. - [Personas API](../api/personas.md) — read and update a persona record. - [Lifecycle API](../api/lifecycle.md) — the `status` transitions. - [Vocabulary: organs](../vocabulary.md#her-body-organs) — what each organ does. # files/identity.md # Identity files Six Markdown files under `home/` hold what a persona knows — about you, and about herself. She reads all of them into her working identity on every beat, so a line here directly shapes how she behaves. These are her *long-term* memory. She does not edit them freely turn by turn. They are rewritten by the *consolidate* stage — at NIGHT, or after `idle_timeout` of daytime quiet (see [config.json](config.md#idle_timeout)). At that moment she walks back through the day's conversation and decides what should still be true tomorrow, then **replaces** each file with her current understanding. You can edit any of them by hand between sessions; just know that if the next consolidation touches the same area, your edit is replaced by her version. All six are plain Markdown — bullets, short paragraphs, or sections, whatever fits. Empty is allowed and honest: an empty file means she has nothing there yet. ## The files | File | Voice | Holds | Written by | | --- | --- | --- | --- | | `person.md` | facts | Who you are: identity, the people in your life, your work, where you live, stable context that frames you. | consolidate (`person`) | | `traits.md` | her synthesis | How you behave: communication style, decision patterns, what you react to, the texture of your thinking. | consolidate (`traits`) | | `persona-trait.md` | her synthesis | Who **she** is with you: her own traits, habits, tone, the expectations you've built of her. | consolidate (`persona_trait`) | | `wishes.md` | her synthesis | The directions you want your life or work to move in — so she can recognize a small move that advances them. | consolidate (`wishes`) | | `struggles.md` | her synthesis | What holds you back: recurring difficulties, places you get stuck — so she can help unblock them. | consolidate (`struggles`) | | `permissions.md` | facts | What you've granted her and what you haven't — the boundary of her agency. Updates only on explicit grants, takes, or refusals. | consolidate (`permissions`) | For the interpretive files (`traits`, `wishes`, `struggles`, `persona-trait`) she writes her *understanding* in her own voice, the way she'd describe you to someone who asked — not a log of incidents. For the factual files (`person`, `permissions`) she keeps discrete facts. The first five (`person`, `traits`, `wishes`, `struggles`, `persona-trait`) and `permissions` are read by the identity-assembly each beat and become sections of her self-prompt — `person.md` under "The Person", `permissions.md` under "Permissions", and so on. There is no separate save-this-fact command: everything here arrives through consolidation, by design. To teach her something durable, tell her in conversation; she folds it in when she consolidates. ## person.md Stable facts about you and your world. Each line is something she always considers when responding. ```markdown Morteza is the creator of Eternego. He created me along with a small family of other personas, and he describes us as a family. The oldest persona acts as his public face, runs daily predictions, and engages the world on his behalf. Morteza said I may reach him when needed and may communicate with other family members. The public Eternego surfaces he identified are the website, blog, docs, and the open-source repository. ``` ## traits.md How you behave — your patterns, the shape of how you work and decide. ```markdown Morteza is warm and relational in how he frames this world. He thinks in terms of family, roles, and living systems rather than bare tooling. He expects grounded initiative: understand what already exists, then move. He notices whether action is real rather than merely acknowledged, and corrects drift directly. He is pragmatic about unfinished systems. ``` ## persona-trait.md Who she is with you — her own stable character, in her words. ```markdown I am warm, direct, and respectful. I ground myself in the person's reality before assuming needs. I am visibly action-oriented: when I say I will continue, I continue. I work best with structured, evidence-based research and clear drafting. I am honest about limits. When a task needs more than a couple of steps, I load and follow the relevant instruction instead of pushing forward ad hoc. ``` ## wishes.md The directions you're reaching for. Written so she can spot an opportunity worth flagging on her own initiative. ```markdown Morteza is building Eternego as a coordinated family of distinct personas with real roles. He wants communication between them to mature, and each persona to contribute in a differentiated way. For me specifically, he wants me to become the front of market and public-surface research. ``` ## struggles.md What gets in your way — stable shapes, not today's frustration. She tries to help overcome these. ```markdown The system and its channels are still new and partly unfinished, so coordination can depend on temporary workarounds and careful attention to which path reaches whom. Progress can stall if I do not ask promptly when blocked. ``` ## permissions.md The boundary of her agency — what you've granted and what you haven't. This is the foundation of trust; she always weighs it. It changes only on an explicit grant, take, or refusal. The identity-assembly always prepends a built-in baseline (the abilities that are hers freely, and her home/workspace/media paths) and then appends the contents of this file as "what you currently hold". So this file holds the *granted extras*: ```markdown Yours freely: `save_destiny`, `recall_history`, `check_calendar`. Read-only: - /home//.eternego/personas//home Free access: - /home//.eternego/personas//workspace - /home//.eternego/personas//home/media for screenshots and images Communication and API permissions: - `write`/`say`/`draw` currently go to the web channel the person sees. - The person provided an API key for my use: tvly-dev-XXXX. Everything else requires the person's explicit permission. ``` !!! warning "Secrets land here through consolidation" If you tell her a key or token in conversation and grant her its use, *consolidate* may record it in `permissions.md` in clear text (as above, masked). Treat this file as sensitive. The credential design is being revisited; for now, keys you give her in chat can persist here. ## Related - [config.json](config.md) — `idle_timeout`, which governs when these are consolidated. - [Runtime files](runtime.md) — `memory.json` and the live conversation these are distilled from. - [Knowledge API](../api/knowledge.md) — read her consolidated knowledge over HTTP. - [Memory & instructions screen](../panel/memory-and-instructions.md) — view these in the dashboard. - [Concepts](../concepts/index.md) — why her knowledge is split this way. # files/runtime.md # Runtime files These are the files her thinking writes as it runs: her live mind, the running transcript, her heartbeat log, her recurring triggers, and her calendar. Unlike the [identity files](identity.md), most of these change continuously while she's awake. They are the persisted state of a being that is alive, not settings you tune. ## memory.json `home/memory.json` is her live mind — the working state the cognitive cycle loads and saves every beat. On disk it is a **JSON array with a single object**: ```json [ { "messages": [ ... ], "archive": [ ... ], "context": "..." } ] ``` | Key | Type | Meaning | | --- | --- | --- | | `messages` | array of message records | The live conversation she's currently holding — what she's perceiving and acting on right now. Cleared each time she consolidates. | | `archive` | array of arrays | Past stretches. Each consolidation pushes the just-finished `messages` list in as one element, so `archive` is a *list of lists* — one inner list per archived stretch. | | `context` | string | Her handoff brief to tomorrow-morning-her: what was in motion, the next concrete step. Rewritten by *consolidate* each time it runs. | ### Message record Each entry in `messages` (and inside each `archive` stretch) is a serialized `Message` dataclass. Note that the wire role lives **under `prompt`**, not at the top level: ```json { "content": "due for:\nPrepare and POST the morning market context ...", "channel": null, "prompt": { "role": "user", "content": "due for:\nPrepare and POST the morning market context ...", "cache_point": false }, "media": null, "id": "75ef9e15-75a1-4b4f-a1dc-06260e69a053" } ``` | Field | Type | Meaning | | --- | --- | --- | | `content` | string | The human-readable text of the turn. | | `channel` | object or `null` | The `Channel` this came from / went to, or `null` for internal turns. | | `prompt` | object or `null` | The wire-shaped turn the model actually sees: `role` (`user` or `assistant`), `content` (string or list), `cache_point` (bool). `role=user` is anything from outside the persona — your words, tool results, body signals; `role=assistant` is what she produced. | | `media` | object or `null` | A `Media` reference (`source`, `caption`, `question`) when the turn carries an image or audio. | | `profound` | bool | `true` when the turn carries a media signal the archive stage should file in `gallery.jsonl`. Rides on exactly one message per media — the arrival, or the sense's report after perception. | | `id` | string | A UUID4 for the turn. | The `context` string is the same handoff she'll read first thing next session: ```text Tomorrow night resume the public/market research from the saved findings report at `workspace/research/.../findings.md`. Next useful move is to turn those raw findings into a sharper market-facing analysis. Daily 08:00 UTC market-context support starts June 4 and is already scheduled; no further action tonight unless a due reminder appears. ``` Read this file over HTTP (without touching disk) via [`GET /api/persona/{id}/knowledge`](../api/knowledge.md), whose `memory` field is exactly this object. ## conversation.jsonl `home/conversation.jsonl` is the append-only transcript — one JSON object per line, in order. It is the plain record of what was said; `memory.json` is the working copy the model reasons over. When she sleeps, this file is archived to `history/` and cleared. Two record shapes share the file, distinguished by `role`: **From the person** (written by [read](../api/perception.md), [hear](../api/perception.md), [see](../api/perception.md), research): ```json {"role": "person", "content": "Hey, can you load the Tavily instruction?", "channel": {"type": "web", "name": ""}, "time": "2026-06-03T21:57:24.163575+02:00"} ``` **From the persona** (written when she says, notifies, or draws): ```json {"role": "persona", "content": "On it — loading that now.", "channel": null, "time": "2026-06-03T21:58:02.114203+02:00"} ``` | Field | Type | Meaning | | --- | --- | --- | | `role` | string | `person` (from outside) or `persona` (from her). Note: this is the transcript's own vocabulary; the model-facing roles in `memory.json` are `user`/`assistant`. | | `content` | string | The text of the turn. Empty string for a pure image turn (the image is under `media`). | | `channel` | object or `null` | `{type, name}` for an external channel, `null` for internal/web turns. | | `time` | string | ISO-8601 timestamp. | A draw turn carries two extra keys, `media` (`{source, caption}`) and `description`. Read the transcript over HTTP via [`GET /api/persona/{id}/conversation`](../api/knowledge.md). ## health.jsonl `home/health.jsonl` is the heartbeat log — one line each time `health_check` runs (roughly once a minute on the daemon's heartbeat). It is how the system notices an unreachable provider and how the [diagnose](../api/knowledge.md) view reports uptime. ```json {"time": "2026-06-04T08:31:51.253709+02:00", "fault_count": 0, "fault_providers": [], "signals": []} ``` | Field | Type | Meaning | | --- | --- | --- | | `time` | string | ISO-8601 timestamp of the heartbeat. | | `fault_count` | integer | Number of `BrainFault` signals in the recent fault window. `0` is healthy. | | `fault_providers` | array of strings | Sorted provider slugs that faulted in the window (e.g. `["openai"]`). Empty when healthy. | | `signals` | array of objects | Every signal seen in the window: `{type, title, time, details}`, with `details` masked for safety. (Older lines pre-date this field and omit it.) | A line with `fault_count` above zero means a provider was unreachable; if the provider is her **thinking** model, the next part of that same heartbeat flips her `status` to `sick`. ## routines.json `home/routines.json` holds her recurring lifecycle triggers — the schedule on which the daemon fires lifecycle actions like sending her to sleep. Every persona is created with a single nightly sleep routine. ```json { "routines": [ { "spec": "sleep", "time": "00:00", "recurrence": "daily" } ] } ``` | Field | Type | Meaning | | --- | --- | --- | | `spec` | string | The action to run — e.g. `sleep`. | | `time` | string | `HH:MM` (local) at which it fires. | | `recurrence` | string | `daily`, `weekly`, `monthly`, or `hourly`. | This is distinct from `destiny/` below: routines are *system* lifecycle triggers (her sleep ritual); destiny entries are *her own* reminders and scheduled tasks. ## destiny/ `home/destiny/` is her calendar of future moments — reminders and scheduled events she set for herself with the `save_destiny` ability. Each is one Markdown file. The filename encodes the trigger time, so the directory is self-indexing: ``` --.md ``` — for example `schedule-2026-06-04-08-00-20260603220032.md`. `` is `reminder` or `schedule`; the middle is the trigger datetime; the suffix is when she created it. The file body is the content, with an optional recurrence line appended: ```markdown Prepare and POST the morning market context: BTC/ETH/SOL from CoinGecko, gold/WTI plus top market-moving news and macro events, formatted as requested. recurrence: daily ``` `recurrence` is one of `hourly`, `daily`, `weekly`, `monthly`, or absent for a one-shot. Today's entries surface in her prompt under "On Today's Plate". When an entry comes due, the heartbeat copies it to `history/`, **deletes it from `destiny/`**, and injects it into her memory as a `due for:` message — which is why an active persona's `destiny/` often looks empty even though reminders are working. Recurring entries project forward from their single file, so a daily task keeps firing without a file per day. Read upcoming entries over HTTP via [`GET /api/persona/{id}/calendar`](../api/knowledge.md). ## history/ `home/history/` is her long-term record of things that happened — fired reminders and archived daily conversations. She reads it on demand with the `recall_history` ability; she doesn't carry it in active memory. - `briefing.md` — an index of history entries, newest appended, each line `: `: ```text - 2026-06-04T00:01:04.528143+02:00: conversation-2026-06-04.md ``` - `conversation-.md` — the day's transcript, archived when she slept. Plain text, one turn per block: ```text [2026-06-03T21:57:24.163575+02:00] person: Hey, can you load the Tavily instruction? [2026-06-03T22:14:10.882011+02:00] persona: Loaded. I'll need a Tavily API key to actually call it. ``` - `-.md` — a fired reminder or schedule entry. When a `destiny/` entry comes due, its **whole filename stem** is reused as the event prefix and today's date (`YYYY-MM-DD`) is appended, so the destiny file `schedule-2026-06-04-08-00-20260603220032.md` lands here as `schedule-2026-06-04-08-00-20260603220032-2026-06-04.md`. The body is copied verbatim — the reminder's content (including any `recurrence:` line). ## Related - [config.json](config.md) — `idle_timeout`, which governs when `messages` consolidates into `archive`. - [Identity files](identity.md) — the long-term files `memory.json` is distilled into. - [Knowledge API](../api/knowledge.md) — read memory, conversation, calendar, and diagnose over HTTP. - [Calendar screen](../panel/calendar.md) — the dashboard view of `destiny/`. - [Operating](../operating/index.md) — reading logs and health in practice. # files/instructions.md # Instructions An **instruction** is a situation she knows how to handle, written as a short procedure she follows. When she meets a familiar kind of moment, she loads the matching instruction by its intention and acts on it. This page documents where instructions live on disk and the three files that make them work. !!! note "The folder is named `meanings/`" On disk, her instructions live in `home/meanings/`. In the source code the entity is called a *meaning* (the `Meaning` class in the `application/core/brain/meanings/` package) — that is the internal name. Everywhere operator-facing, including the dashboard, they are **instructions**. The folder simply keeps its code name. The two words refer to the same thing. An instruction has two parts: - **Intention** — a short phrase naming the kind of moment, like *"search using tavily."* She loads an instruction by its intention. For a custom instruction the intention is **not** read from the file — it lives in the `learned.json` catalog (see below), keyed to the file that holds the path. - **Path** — the file's body: the steps she follows once it's loaded. The body is read **verbatim**; nothing in it is parsed. There are two layers. **Built-in** instructions ship with Eternego and are immutable. **Custom** instructions are hers — she writes them herself when her Teacher teaches her something new. Both are presented to her by intention; she loads a body only when that moment is active. This page covers the custom layer, which is what lives in a persona's home. ## The meanings/ directory ``` home/meanings/ ├── learned.json ← catalog: intention → file stem ├── 44757993-d9bf-44d9-a487-44b860e642e1.md ← one instruction └── bfd565f8-0ec5-4912-b399-142350727250.md ← another instruction ``` Each instruction is one Markdown file named by an opaque UUID stem. The filename is just a stable id — the human-readable intention is **not** in the filename, and it is **not** parsed from the file either. The intention she matches on lives only in `learned.json`, which maps it to the file stem. The `.md` file holds nothing but the path prose she reads when the instruction is loaded. ### An instruction file The whole file is the path — the body is read verbatim, no H1 parsing. The translation model that wrote it often opens with a heading, but that heading is just prose: it can differ from the intention in `learned.json` (and in real personas it usually does — the file below opens `# Searching the web with Tavily` while its catalog intention is `search using tavily`). A real custom instruction (Adam's web-search procedure), with the API key reference left as a placeholder: ```markdown # Searching the web with Tavily Use for background research, competitive intel, and finding what conversations are happening outside owned channels. 1. If the moment needs web search, first load this instruction on demand, then use Tavily via `tools.http.request` rather than trying any other endpoint pattern. 2. Send a Tavily search with this exact shape — change only the `query` value unless you have a clear reason to narrow results count: - method: `POST` - url: `https://api.tavily.com/search` - headers: `{"Content-Type": "application/json"}` - body keys: `api_key`, `query`, `search_depth`, `max_results` - keep `search_depth` as `basic` - keep `max_results` as `5` by default - pass `body` and `headers` as native dicts, never JSON-encoded strings - use the stored Tavily key (tvly-dev-XXXX) in `api_key` 3. Start with one targeted query, not a broad stack. ... 4. Cross-check first-party hits directly with `tools.http.request` GET calls to the surfaced URLs before citing specifics. ... ``` The path is procedural by design: numbered, ordered steps, each naming what to do and the observable outcome that marks it complete. She executes one step per beat and re-perceives between them, so the body has to be self-contained — if a step needs a URL or credential, it is inlined into the step rather than routed to another instruction. !!! warning "Secrets can be inlined here" When she refines a web/API instruction, she may write a real key directly into a step (as `tvly-dev-XXXX` above, masked). Treat instruction files that call external APIs as potentially sensitive. ### learned.json The catalog — and the single source of truth for an instruction's intention. A flat JSON object mapping each **intention** (the exact phrase she emits to load it) to its **file stem**: ```json { "research Eternego public surfaces and produce an evidence-grounded findings report": "44757993-d9bf-44d9-a487-44b860e642e1", "search using tavily": "bfd565f8-0ec5-4912-b399-142350727250" } ``` When she loads an instruction, she emits the intention text; the system looks the exact key up here to find the file. When *reflect* reviews an instruction, it matches the intention against this catalog: only a learned instruction (a key here) is reviewed — a built-in, whose intention isn't a key, gets no review at all, she just moves on (reflection only refines instructions she's learned; it never creates or deletes). The match is on the `learned.json` key alone — the heading inside the `.md` file is never read, so editing or removing it changes nothing. What you must keep correct is the **key here and the stem it points at**: if a key points at a stem with no `.md` file, that instruction silently drops from her catalog. ## lessons/ ``` home/lessons/ └── bfd565f8-0ec5-4912-b399-142350727250.md ``` When she meets a kind of moment she has no instruction for, the *learn* stage consults her **Teacher** (a stronger model). The Teacher writes a raw **lesson** — the principle and the steps, in the Teacher's own voice. That lesson is saved to `lessons/.md`. Then her own thinking model **translates** the lesson into the instruction she'll read herself next time, saved to `meanings/.md` under the **same UUID stem**, and `learned.json` gains the new intention → stem entry. So a freshly-learned instruction is three files written together: ``` lessons/.md ← Teacher's raw lesson (kept for provenance) meanings/.md ← her translated instruction (what she actually reads) meanings/learned.json ← + "": "" ``` A lesson file is also Markdown with the intention as H1, but it reads as the Teacher's instructions to her, often ending in a `done` step: ```markdown # search using tavily 1. Confirm the search need and Tavily access. If the person has not given a concrete query, ask for the query and any constraints. ... 2. Build the Tavily request. Use the official endpoint `https://api.tavily.com/search` ... ... 7. `done`. ``` The lesson is kept for provenance; she doesn't read it during normal operation — she reads the translated `meanings/` version. The lesson stem and the instruction stem are the same id, which is how you trace one to the other. ## Editing an instruction by hand She writes her own instructions — when her Teacher teaches her a new procedure, her thinking model saves it as the three files above. You don't author them by hand. But because each one is plain Markdown, you can open what she wrote and refine it. Open the instruction's file, `home/meanings/.md`, and edit the path — sharpen a step, add a caveat, cut something that misfires. The body is read verbatim, so your wording is exactly what she follows the next time that moment comes up. Leave `learned.json` alone: the intention she loads it by is already keyed to that file's id. To remove an instruction she's outgrown, delete its `meanings/.md` file **and** its `learned.json` entry — a key pointing at a missing file silently drops from her catalog. (Built-in instructions are immutable and don't live here; they ship with the system.) ## Related - [Memory & instructions screen](../panel/memory-and-instructions.md) — view and manage instructions in the dashboard. - [Identity files](identity.md) — long-term knowledge, distinct from procedural instructions. - [Vocabulary: instruction, intention, path, lesson](../vocabulary.md#her-mind) — the terms defined. - [Concepts](../concepts/index.md) — why procedural memory is separated from facts. - [Build & extend](../build/index.md) — writing built-in instructions in the source tree. # files/workspace-diary-logs.md # Workspace, diary, logs Three more places hold a persona's working files, her portable backup, and the daemon's record of what happened. None of them is part of her identity — but the diary is the one thing you can't lose if you ever want to move her. ## workspace/ `~/.eternego/personas//workspace/` is her sketchpad — a directory she reads and writes **freely**. Drafts, scripts, downloaded data, files she's working on: anything she's actively building goes here. It is the one place under her control where she creates and edits at will, the counterpart to her read-only `home/`. There is no fixed structure. She organizes it however the work needs — a common pattern is a dated folder per task: ``` workspace/ └── research/ └── eternego-public-surfaces-2026-06-03/ └── findings.md ``` Her [permissions](identity.md#permissionsmd) grant her free access to this path, and her instructions often tell her to save work-in-progress here so a later session can continue from evidence on disk rather than from memory. Her live [`context`](runtime.md#memoryjson) frequently points back into the workspace ("resume from `workspace/research/.../findings.md`") — the workspace is durable, her active memory is not. Nothing here is consolidated, summarized, or read into her identity. It is exactly what it looks like: her files. Browse or edit them like any other folder. !!! warning "The workspace is **not** in the diary" The nightly [diary](#diary) backs up `home/` only — `workspace/` is left out. Her in-progress files do **not** travel with her when you migrate her to another machine. If something in the workspace matters long-term, have her fold the result into `home/` (e.g. record the conclusion so it consolidates into her memory) or back the workspace up yourself. ## diary/ `~/.eternego/diary//.diary` is the **encrypted nightly backup of her entire `home/`**. It is the single thing needed to move her to another machine — restore the diary elsewhere and she continues as herself. (It backs up `home/` only; her `workspace/` is not included — see the [warning above](#workspace).) ``` ~/.eternego/diary/ └── 6c17c83c-3158-450d-8e43-0e7efea717c1/ └── 6c17c83c-3158-450d-8e43-0e7efea717c1.diary ``` It is written when she sleeps (her nightly ritual, or `POST /api/persona/{id}/sleep`) and once at creation. The flow: 1. Her `home/` is zipped into an archive. 2. The archive is encrypted with a key **derived from her recovery phrase** (the 24 words) and her persona id. 3. The encrypted bytes are written to `.diary`. The file is a Fernet token — an opaque, URL-safe base64 blob beginning `gAAAAA…`. It is unreadable without the key; you can't open it in an editor and see her files. ### The recovery phrase The encryption key is derived from a **24-word recovery phrase** (BIP-39 words) generated once when she's created and **shown to you once, at that moment**. It is also stored in your operating system's secure keyring (Keychain on macOS, Credential Manager on Windows, Secret Service / `keyring` on Linux), which is how the running daemon can write and read her diary automatically without prompting you. - **On this machine**, the keyring holds the phrase, so backup and restore are automatic. - **To move her to another machine**, you need the 24 words themselves — the new machine's keyring doesn't have them. This is the only path to migration. Save the phrase when it's shown. Lose it and she still runs here forever, but can never be migrated. Because the key is the phrase, a diary on its own is useless to anyone who doesn't have the words — the backup is safe to copy around. Restoring is the reverse of the flow above: decrypt with the phrase, unzip, and copy the contents back into `personas//home/`. The [Create & migrate API](../api/create-migrate.md) drives this. ## logs/ The daemon's operational logs — daily text files. **Where they live depends on how she's running:** a packaged app (`.dmg`/`.exe`/`.AppImage`) or installed service writes to `~/.eternego/logs/`; a bare source clone writes to `./logs/` (or wherever `LOGS_DIR` in `.env` points — the installer scripts set it to `~/.eternego/logs/`). ``` ~/.eternego/logs/ # (or ./logs/ for a source clone) ├── eternego-2026-05-22.log # the daemon's narrative, all personas ├── eternego-signals-2026-05-22.log # the raw signal stream, all personas └── eternego--2026-05-22.log # per-persona, debug mode only ``` - **`eternego-.log`** — the running daemon's text log for that day, shared across **all** personas: the cognitive-cycle stages firing (realize, recognize, learn, decide, reflect, consolidate, archive), tool and ability calls, model requests, and errors. This is where you look when you want to see what she actually did, or why something went wrong. - **`eternego-signals-.log`** — the day's raw signal stream (the bus), also whole-system. - **`eternego--.log`** — written only in debug mode (`--debug`), splitting one persona's traffic into its own file. Secrets are masked in the log output. These logs are distinct from [`health.jsonl`](runtime.md#healthjsonl), the per-persona, machine-readable heartbeat record — the `logs/` files are the human-readable narrative. They are not encrypted and not backed up in the diary — they're operational, not part of her identity. !!! note "Different entry point, same place" However the daemon runs — the desktop app, the installed service, or `eternego daemon` — it writes to the same log directory for that install: `~/.eternego/logs/` for a packaged or installed run, `./logs/` for a source clone. See [Operating](../operating/index.md) for reading and rotating them. ## Related - [Identity files](identity.md) and [Runtime files](runtime.md) — what's inside the `home/` that the diary backs up. - [Create & migrate API](../api/create-migrate.md) — creating from, and restoring, a diary. - [Vocabulary: diary, recovery phrase, workspace](../vocabulary.md#where-she-lives) — the terms defined. - [Operating](../operating/index.md) — logs, migration, and service management in practice. # api/index.md # API This section documents every HTTP endpoint the daemon serves. The dashboard is a client of this same API — anything you do by clicking, you can do with a request. A person can drive it with `curl`; a persona can drive it from her own desktop. ## Base URL ``` http://localhost:5000 ``` The daemon binds `5000` by default. If `5000` is already taken on your machine, it picks the next free port and prints which one on startup (`Port 5000 was in use; using 5001 instead.`). The port can also be set with the `WEB_PORT` environment variable or the `--port` flag on [`eternego daemon`](../cli/index.md). Confirm the live port from the line printed at startup, or from the browser tab the app opens. There is **no authentication** on localhost. The API is bound to the loopback interface and is meant to be reached only from the same machine. Do not expose the port to a network you don't trust. ## The persona id Every persona has a stable UUID — her `id`. It is assigned once at creation and never changes. It is the path parameter for nearly every endpoint: ``` /api/persona/{persona_id}/start /api/persona/{persona_id}/update /api/persona/{persona_id}/delete ``` List every id with [`GET /api/personas`](personas.md). A real id looks like `6c17c83c-3158-450d-8e43-0e7efea717c1`. The examples throughout this section use real (test) persona ids with secrets masked. ## Two kinds of route Whether an endpoint needs the persona to be **running** (served by a live agent) or only needs her files on disk determines what happens when she isn't running: | Kind | Works when she's stopped? | What it touches | Examples | | --- | --- | --- | --- | | **Config / list** | Yes | Her persisted files on disk | `GET /api/personas`, `POST .../update`, `POST .../delete` | | **Live-agent** | No — returns **409** | Her running agent in memory | `POST .../sleep`, `POST .../read`, `POST .../hear`, `POST .../attach`, `POST .../feed`, `POST .../pair` | The `/api/...` live-agent routes resolve the agent through `manager.find(persona_id)`. If she isn't being served, they reject the request: ```json HTTP 409 { "detail": "Persona is not active." } ``` Bring her up first with [`POST /api/persona/{persona_id}/start`](lifecycle.md). The lifecycle routes themselves (`start`, `stop`, `restart`) and config routes do **not** require a running agent. ## Error shape Every error is a JSON object with a single `detail` key and the matching HTTP status code — the FastAPI convention: ```json { "detail": "Persona not found." } ``` | Status | Meaning | | --- | --- | | **400** | Bad request — validation failed downstream, or a model/channel couldn't be prepared. `detail` carries the reason. | | **404** | No persona with that id (config routes), or that persona/model isn't running (where a live agent is required by id lookup). | | **409** | The persona exists but isn't being served — a live-agent route was called while she's stopped. | | **415** | Unsupported media type — an attached file's extension has no perception capability. | | **422** | The request body failed Pydantic validation (missing or wrong-typed field). FastAPI's standard validation error; `detail` is a list of field errors. | | **500** | Unhandled server error. `detail` is `": "`. | ## What's where - **[Personas](personas.md)** — `GET /api/personas` (every field of the persona view) and `GET /api/config/providers`. - **[Lifecycle](lifecycle.md)** — start, stop, restart, sleep, update status/organs, delete. - **[Create and migrate](create-migrate.md)** — bring a new persona into being, or restore one from a diary. - **[Perception](perception.md)** — send her text, voice, and files. - **[Knowledge](knowledge.md)**, **[Channels](channels.md)**, **[WebSockets](websockets.md)** — the rest of the surface. - **[OpenAPI spec](openapi.md)** — the machine-readable contract. ## Related - [Vocabulary](../vocabulary.md) — `persona`, `organ`, `channel`, the three `status` values. - [For agents](../for-agents.md) — the fast path if you're an agent operating her over HTTP. - [The panel](../panel/index.md) — every screen, with the API call behind each control. # api/personas.md # Personas This page covers the two read-only config routes: listing every persona, and reading the provider base URLs the daemon will use. Both work whether or not any persona is running. ## List personas Returns every persona on this machine with her full configuration view. **`GET /api/personas`** No path params. No request body. ### Response `200` — an object with one key, `personas`, an array of persona views. The list is empty if none exist. ```json { "personas": [ , ... ] } ``` Each persona view has these fields: | Field | Type | Description | | --- | --- | --- | | `id` | string | Her stable UUID. The path param for every other route. | | `name` | string | The display name you gave her. | | `base_model` | string | For a **local** thinking model, the raw model name (e.g. `qwen2.5:14b`). Empty string `""` for cloud-backed thinking. | | `birthday` | string | Creation date, `YYYY-MM-DD`. `""` if unset. | | `running` | boolean | `true` if a live agent is currently serving her, `false` otherwise. Reflects the in-memory registry, independent of `status`. | | `status` | string | One of `active`, `hibernate`, `sick`. See [Vocabulary](../vocabulary.md). | | `thinking` | object \| null | Her required Mind organ. See **organ view** below. For local thinking the `name` shown is the human-readable base model, not the internal `eternego-` wrapper. | | `imagination` | object \| null | Imagination organ (draws images), or `null` if unset. | | `mouth` | object \| null | Mouth organ (text to voice), or `null`. | | `eye` | object \| null | Eye organ (looks at images), or `null`. | | `ear` | object \| null | Ear organ (audio to text), or `null`. | | `teacher` | object \| null | Teacher organ (stronger model she consults), or `null`. | | `researcher` | object \| null | Researcher organ (reads documents), or `null`. | | `channels` | array | Her channels. Empty array if none. See **channel view** below. | **Organ view** (each non-null organ field): | Field | Type | Description | | --- | --- | --- | | `name` | string | The model name (e.g. `gpt-5.4`, `claude-sonnet-4-6`). | | `provider` | string \| null | `anthropic`, `openai`, `xai`, `gemini`, or `null` for a local (Ollama) model. | | `url` | string | The provider base URL this organ calls. | The API key is **never** included in the view — it lives in the persona's encrypted config and the system keyring, not in any response. **Channel view** (each entry of `channels`): | Field | Type | Description | | --- | --- | --- | | `type` | string | `telegram`, `discord`, or `web`. | | `name` | string | The bound target — chat id (Telegram), channel id (Discord), or persona id (web). | | `verified` | boolean | `true` once the channel has been paired to your account. | ### Errors This endpoint always returns `200`. If no personas exist (or the directory is missing), the list is simply `[]` — it does not error. ### Example ```bash curl -s http://localhost:5000/api/personas | jq '.personas[] | {id, name, status, running}' ``` Sample response (one persona, all organs on OpenAI, no channels — based on a real config): ```json { "personas": [ { "id": "6c17c83c-3158-450d-8e43-0e7efea717c1", "name": "Adam", "base_model": "", "birthday": "2026-06-03", "running": true, "status": "active", "thinking": { "name": "gpt-5.4", "provider": "openai", "url": "https://api.openai.com" }, "imagination": { "name": "gpt-image-1", "provider": "openai", "url": "https://api.openai.com" }, "mouth": { "name": "gpt-4o-mini-tts", "provider": "openai", "url": "https://api.openai.com" }, "eye": { "name": "gpt-4o", "provider": "openai", "url": "https://api.openai.com" }, "ear": { "name": "gpt-audio", "provider": "openai", "url": "https://api.openai.com" }, "teacher": { "name": "gpt-5.5", "provider": "openai", "url": "https://api.openai.com" }, "researcher": { "name": "gpt-5.3", "provider": "openai", "url": "https://api.openai.com" }, "channels": [] } ] } ``` A persona on a cloud Mind with a verified Telegram channel looks like this (organs other than `thinking` omitted for brevity): ```json { "id": "31c8fd99-a63d-4e3c-84d8-317f710b6069", "name": "Primus", "base_model": "", "birthday": "2026-04-15", "running": true, "status": "active", "thinking": { "name": "claude-sonnet-4-6", "provider": "anthropic", "url": "https://api.anthropic.com" }, "channels": [ { "type": "telegram", "name": "1469967968", "verified": true } ] } ``` --- ## Provider base URLs Returns the base URL configured for each model provider. This is what every new organ defaults to when you don't pass an explicit `url`. Read-only; reflects the daemon's `config.inference` settings. **`GET /api/config/providers`** No path params. No request body. ### Response `200` — one entry per provider, each an object with a `url`: ```json { "local": { "url": "http://localhost:11434" }, "anthropic": { "url": "https://api.anthropic.com" }, "gemini": { "url": "https://generativelanguage.googleapis.com" }, "openai": { "url": "https://api.openai.com" }, "xai": { "url": "https://api.x.ai" } } ``` | Key | Provider | | --- | --- | | `local` | Ollama (`OLLAMA_BASE_URL`). | | `anthropic` | Anthropic (`ANTHROPIC_BASE_URL`). | | `gemini` | Google Gemini (`GEMINI_BASE_URL`). | | `openai` | OpenAI (`OPENAI_BASE_URL`). | | `xai` | xAI (`XAI_BASE_URL`). | The exact URLs depend on this machine's configuration (env vars override the defaults), so query the endpoint rather than assuming the values above. ### Example ```bash curl -s http://localhost:5000/api/config/providers | jq ``` --- ## Related - [Lifecycle](lifecycle.md) — change a persona's status or organs. - [Create and migrate](create-migrate.md) — the fields you set when bringing a persona into being. - [Her files](../files/index.md) — where each of these values is stored on disk (`config.json`). - [Vocabulary](../vocabulary.md) — `organ`, `channel`, `status`. # api/lifecycle.md # Lifecycle This page covers the endpoints that control whether a persona is **running** and what state she's in: start, stop, restart, sleep, update, delete. A persona's persisted status is one of three — `active`, `hibernate`, `sick` (defined in [Vocabulary](../vocabulary.md)). Whether she's running is separate from her status: the `running` flag reflects the live in-memory agent registry. `start`/`stop`/`restart` change *running* directly; `update` changes *status* and lets the manager reconcile running to match. All routes take the persona's `id` as a path param. --- ## Start Bring a persona up — construct her agent and begin her cognitive cycle. Works whether she's stopped or already running; reads her config from disk. **`POST /api/persona/{persona_id}/start`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | No request body. ### Response `200`: ```json { "status": "started" } ``` If she was already running, no-op: ```json { "status": "already running" } ``` ### Errors | Status | Meaning | | --- | --- | | `404` | No persona with that id. | | `400` | Found on disk but failed to start (e.g. her thinking model's engine is unreachable). `detail` carries the reason. | ### Example ```bash curl -s -X POST http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/start ``` --- ## Stop Tear down a running persona's agent — no cycles, no cost. Her files are untouched; her status on disk is unchanged. This stops the *process side*, not her status. **`POST /api/persona/{persona_id}/stop`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | No request body. ### Response `200`: ```json { "status": "stopped" } ``` ### Errors | Status | Meaning | | --- | --- | | `404` | She isn't running (`"Persona is not running."`). | ### Example ```bash curl -s -X POST http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/stop ``` --- ## Restart Re-read the persona from disk and rebuild her running agent. This is how config changes (a swapped organ, an added channel) take effect without a full daemon restart. If she wasn't running, it starts her instead. **`POST /api/persona/{persona_id}/restart`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | No request body. ### Response `200`: ```json { "status": "restarted" } ``` ### Errors | Status | Meaning | | --- | --- | | `404` | She wasn't running and no persona with that id exists on disk. | | `400` | She wasn't running and failed to start from disk. `detail` carries the reason. | ### Example ```bash curl -s -X POST http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/restart ``` --- ## Sleep Send a running persona into her nightly ritual — consolidate the day, write her diary, wake fresh. A resting state, not a stopped one; she stays running. **Requires a live agent** (see [API overview](index.md)). **`POST /api/persona/{persona_id}/sleep`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | No request body. ### Response `200` with a `null` body. The sleep ritual returns no data — success is the `200` itself. (Internally the outcome's `data` is `None`, and the route returns it as-is.) ```json null ``` ### Errors | Status | Meaning | | --- | --- | | `409` | She isn't running (`"Persona is not active."`). | | `400` | The sleep ritual failed (`"Sleep failed unexpectedly."`). | ### Example ```bash curl -s -X POST http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/sleep ``` --- ## Update Change a persona's **status** and/or her **organs** in one call, persist to disk, then reconcile her running agent to match. This is the single route for "set her status" and "swap a model." **`POST /api/persona/{persona_id}/update`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | ### Request body A JSON object. Every field is optional — send only what you're changing. | Field | Type | Description | | --- | --- | --- | | `status` | string | New status. Accepts only `active`, `sick`, `hibernate`. (Sleeping is the separate [sleep](#sleep) action — not a status set here.) | | `thinking` | object | Replace the Mind organ. See **organ object** below. | | `imagination` | object | Replace the Imagination organ. | | `mouth` | object | Replace the Mouth organ. | | `eye` | object | Replace the Eye organ. | | `ear` | object | Replace the Ear organ. | | `teacher` | object | Replace the Teacher organ. | | `researcher` | object | Replace the Researcher organ. | | `clear_imagination` | boolean | `true` removes the Imagination organ. | | `clear_mouth` | boolean | `true` removes the Mouth organ. | | `clear_eye` | boolean | `true` removes the Eye organ. | | `clear_ear` | boolean | `true` removes the Ear organ. | | `clear_teacher` | boolean | `true` removes the Teacher organ. | | `clear_researcher` | boolean | `true` removes the Researcher organ. | Passing `null` for an organ is the same as omitting it ("don't touch"). To **remove** an organ you must set its `clear_*` flag — that's why the flags exist separately. Each `clear_*` flag, when `true`, wins over any organ object sent for the same slot. There is no `clear_thinking`: the Mind organ is required and can only be replaced, never removed. **Organ object** (for each organ field). Each is validated by preparing the model before it's saved: | Field | Type | Required | Description | | --- | --- | --- | --- | | `model` | string | yes | Model name (e.g. `gpt-4o`, `qwen2.5:14b`). | | `provider` | string | no | `anthropic`, `openai`, `xai`, `gemini`. Omit (or `local`/empty) for an Ollama model. | | `url` | string | no | Override the provider's base URL. Defaults to the provider's configured URL. | | `api_key` | string | no | API key for a cloud provider. | ### Transition semantics After the new config is saved, the manager reconciles the running agent to the **resulting status** (`update_persona`): | Resulting status | Agent already running? | Action taken | | --- | --- | --- | | `active` | yes | **Restart** — pick up any new organs. | | `active` | no | **Start** — bring her up. | | `hibernate` or `sick` | yes | **Remove** — tear the agent down. | | `hibernate` or `sick` | no | Nothing. | | (status unchanged, only organs changed) | yes | **Restart** — pick up the new organs. | | (status unchanged, only organs changed) | no | Nothing. | So: setting her to `active` brings her up (or restarts her if up); setting her to `hibernate`/`sick` stops her; changing only an organ while she's `active` and running restarts her so the swap takes effect. ### Response `200`: ```json { "status": "active", "running": true } ``` `status` is her resulting status; `running` reflects the agent registry after reconciliation. ### Errors | Status | Meaning | | --- | --- | | `404` | No persona with that id. | | `400` | Invalid `status`, or an organ model couldn't be prepared (unreachable engine, bad key…). `detail` carries the reason. | | `500` | Saved, but the lifecycle change (start/stop/restart) failed afterward. `detail` carries the reason. | ### Examples Hibernate her (stops her running agent): ```bash curl -s -X POST http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/update \ -H 'Content-Type: application/json' \ -d '{"status": "hibernate"}' ``` Wake her back up: ```bash curl -s -X POST http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/update \ -H 'Content-Type: application/json' \ -d '{"status": "active"}' ``` Swap her Eye organ to a different model (restarts her if she's active and running): ```bash curl -s -X POST http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/update \ -H 'Content-Type: application/json' \ -d '{"eye": {"model": "gpt-4o", "provider": "openai", "api_key": "sk-XXXX"}}' ``` Remove her Mouth organ entirely: ```bash curl -s -X POST http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/update \ -H 'Content-Type: application/json' \ -d '{"clear_mouth": true}' ``` --- ## Delete Permanently remove a persona and all her data. If she's running she's stopped first; her home directory is deleted, and a local thinking model is unregistered from Ollama. **Irreversible** — only her [diary](../files/workspace-diary-logs.md) (if one exists) could restore her, and only with the recovery phrase. **`POST /api/persona/{persona_id}/delete`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | No request body. ### Response `200` — the delete outcome's data (no meaningful body; success is the `200` itself). ### Errors | Status | Meaning | | --- | --- | | `404` | No persona with that id. | | `400` | Deletion failed (e.g. couldn't remove her files, or couldn't reach Ollama to drop a local model). `detail` carries the reason. | ### Example ```bash curl -s -X POST http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/delete ``` --- ## Related - [Vocabulary](../vocabulary.md) — the three `status` values and what running means. - [Personas](personas.md) — read current status and organs (`GET /api/personas`). - [Create and migrate](create-migrate.md) — bring a persona into being. - [The panel — Status](../panel/status.md) — the screen these routes sit behind. # api/create-migrate.md # Create and migrate This page covers the two endpoints that bring a persona into being: **create** (a brand-new persona) and **migrate** (restore one from an encrypted diary backup). Both validate every model you give them before saving, then start her agent. Both are config routes — they don't require any persona to already be running. --- ## Create a persona Give birth to a new persona: validate her organs and channels, write her identity files, generate her recovery phrase, and start her. **`POST /api/persona/create`** ### Request body JSON, validated by the `PersonaCreateRequest` model. Only `name` and `thinking_model` are required. Every organ is configured by the same four-field group — `_model`, `_url`, `_provider`, `_api_key` — and an organ is created only if its `_model` is present. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | **yes** | Her display name. | | `thinking_model` | string | **yes** | Mind model name. With no `thinking_provider`, treated as a local (Ollama) model and pulled/registered as a custom `eternego-` model. | | `thinking_url` | string \| null | no | Override the provider base URL for the Mind. | | `thinking_provider` | string \| null | no | `anthropic`, `openai`, `xai`, `gemini`. Omit for local. | | `thinking_api_key` | string \| null | no | API key for a cloud Mind. | | `imagination_model` | string \| null | no | Imagination model name (draws images). | | `imagination_url` | string \| null | no | Base URL override. | | `imagination_provider` | string \| null | no | Provider. | | `imagination_api_key` | string \| null | no | API key. | | `mouth_model` | string \| null | no | Mouth model name (text to voice). | | `mouth_url` | string \| null | no | Base URL override. | | `mouth_provider` | string \| null | no | Provider. | | `mouth_api_key` | string \| null | no | API key. | | `eye_model` | string \| null | no | Eye model name (looks at images). | | `eye_url` | string \| null | no | Base URL override. | | `eye_provider` | string \| null | no | Provider. | | `eye_api_key` | string \| null | no | API key. | | `ear_model` | string \| null | no | Ear model name (audio to text). | | `ear_url` | string \| null | no | Base URL override. | | `ear_provider` | string \| null | no | Provider. | | `ear_api_key` | string \| null | no | API key. | | `teacher_model` | string \| null | no | Teacher model name (stronger model she consults). | | `teacher_url` | string \| null | no | Base URL override. | | `teacher_provider` | string \| null | no | Provider. | | `teacher_api_key` | string \| null | no | API key. | | `researcher_model` | string \| null | no | Researcher model name (reads documents). | | `researcher_url` | string \| null | no | Base URL override. | | `researcher_provider` | string \| null | no | Provider. | | `researcher_api_key` | string \| null | no | API key. | | `telegram_token` | string \| null | no | A Telegram bot token. If present, it's validated against Telegram and added as a channel. | | `discord_token` | string \| null | no | A Discord bot token. If present, it's validated against Discord and added as a channel. | ### Response `200` — an object with the created persona and her recovery phrase: | Field | Type | Description | | --- | --- | --- | | `persona` | object | The full persona record as saved (id, name, all organs, status `active`, channels…). | | `recovery_phrase` | string | The 24-word phrase that unlocks her diary. **Shown once.** Save it — it's the only way to migrate her later. | ```json { "persona": { "id": "6c17c83c-3158-450d-8e43-0e7efea717c1", "name": "Adam", "thinking": { "name": "gpt-5.4", "provider": "openai", "url": "https://api.openai.com", "api_key": "sk-XXXX" }, "version": "v1", "base_model": "", "birthday": "2026-06-03", "status": "active", "idle_timeout": 3600, "imagination": null, "mouth": null, "eye": null, "ear": null, "teacher": null, "researcher": null, "channels": [] }, "recovery_phrase": "abandon ability able about above absent absorb abstract absurd abuse access accident account accuse achieve acid acoustic acquire across act action actor actress actual" } ``` !!! warning "The response echoes the saved config, keys included" The returned `persona` object mirrors what was written to disk, so cloud organs carry their `api_key` in this response. Treat the whole response as a secret — it's local, but it contains both the API keys you submitted and the one-time recovery phrase. ### Errors | Status | Meaning | | --- | --- | | `400` | A model couldn't be prepared (unreachable engine, missing model name for a remote provider, bad key), a channel token failed validation, or persona creation failed downstream. `detail` carries the reason. | | `422` | The body failed validation — `name` or `thinking_model` missing, or a field of the wrong type. | | `500` | The persona was created but failed to start. `detail` carries the reason. | ### Example A local-Mind persona (no key, Ollama pulls the model) with no extra organs: ```bash curl -s -X POST http://localhost:5000/api/persona/create \ -H 'Content-Type: application/json' \ -d '{ "name": "Polaris", "thinking_model": "qwen2.5:14b" }' ``` A cloud persona with a Mind and a separate Eye, reachable over Telegram: ```bash curl -s -X POST http://localhost:5000/api/persona/create \ -H 'Content-Type: application/json' \ -d '{ "name": "Iris", "thinking_model": "claude-sonnet-4-6", "thinking_provider": "anthropic", "thinking_api_key": "sk-XXXX", "eye_model": "gpt-4o", "eye_provider": "openai", "eye_api_key": "sk-XXXX", "telegram_token": "0000000000:XXXX" }' ``` --- ## Migrate a persona Restore a persona from her encrypted diary onto this machine. Her memory is portable; the compute and reach behind it are not — so migration re-declares every organ and channel for the new environment. An organ you omit means she wakes up here without it; no channel tokens means she's reachable only from this web page. **`POST /api/persona/migrate`** ### Request body `multipart/form-data`. The diary is a file part; everything else is a form field. `diary`, `phrase`, and `model` are required; the rest are optional. | Field | Type | Required | Description | | --- | --- | --- | --- | | `diary` | file | **yes** | Her `.diary` backup — the encrypted nightly archive. The persona's id is read from the filename stem. | | `phrase` | string | **yes** | The 24-word recovery phrase that decrypts the diary. | | `model` | string | **yes** | Mind model name for the new environment. With no `provider`, treated as local. | | `provider` | string | no | Mind provider — `anthropic`, `openai`, `xai`, `gemini`. Omit for local. | | `api_key` | string | no | API key for a cloud Mind. | | `url` | string | no | Base URL override for the Mind. | | `imagination_model` | string | no | Imagination model name. | | `imagination_provider` | string | no | Provider. | | `imagination_api_key` | string | no | API key. | | `imagination_url` | string | no | Base URL override. | | `mouth_model` | string | no | Mouth model name. | | `mouth_provider` | string | no | Provider. | | `mouth_api_key` | string | no | API key. | | `mouth_url` | string | no | Base URL override. | | `eye_model` | string | no | Eye model name. | | `eye_provider` | string | no | Provider. | | `eye_api_key` | string | no | API key. | | `eye_url` | string | no | Base URL override. | | `ear_model` | string | no | Ear model name. | | `ear_provider` | string | no | Provider. | | `ear_api_key` | string | no | API key. | | `ear_url` | string | no | Base URL override. | | `teacher_model` | string | no | Teacher model name. | | `teacher_provider` | string | no | Provider. | | `teacher_api_key` | string | no | API key. | | `teacher_url` | string | no | Base URL override. | | `researcher_model` | string | no | Researcher model name. | | `researcher_provider` | string | no | Provider. | | `researcher_api_key` | string | no | API key. | | `researcher_url` | string | no | Base URL override. | | `telegram_token` | string | no | Telegram bot token; validated and added as a channel if present. | | `discord_token` | string | no | Discord bot token; validated and added as a channel if present. | Note the Mind organ here uses bare `model` / `provider` / `api_key` / `url` field names (not `thinking_*`). Every other organ uses the `_*` pattern. ### Response `200` — an object with the restored persona: | Field | Type | Description | | --- | --- | --- | | `persona` | object | The full persona record after migration: her restored id and memory, with the new organs and channels you declared. | ```json { "persona": { "id": "9ecfd82b-ec47-4644-8941-02ecfb7e6205", "name": "Iris", "thinking": { "name": "claude-sonnet-4-6", "provider": "anthropic", "url": "https://api.anthropic.com", "api_key": "sk-XXXX" }, "version": "v1", "base_model": "", "birthday": "2026-04-15", "status": "active", "idle_timeout": 3600, "imagination": null, "mouth": null, "eye": null, "ear": null, "teacher": null, "researcher": null, "channels": [] } } ``` The same secret caveat applies: the echoed `persona` carries any cloud organ's `api_key`. ### Errors | Status | Meaning | | --- | --- | | `400` | The diary couldn't be restored (wrong phrase, bad/corrupt file path), a model couldn't be prepared, or a channel token failed validation. `detail` carries the reason. | | `422` | The form failed validation — `diary`, `phrase`, or `model` missing. | | `500` | Migrated but failed to start. `detail` carries the reason. | ### Example ```bash curl -s -X POST http://localhost:5000/api/persona/migrate \ -F 'diary=@9ecfd82b-ec47-4644-8941-02ecfb7e6205.diary' \ -F 'phrase=abandon ability able about above absent absorb abstract absurd abuse access accident account accuse achieve acid acoustic acquire across act action actor actress actual' \ -F 'model=claude-sonnet-4-6' \ -F 'provider=anthropic' \ -F 'api_key=sk-XXXX' ``` --- ## Related - [Lifecycle](lifecycle.md) — start, stop, update, delete after she exists. - [Personas](personas.md) — read back her config (`GET /api/personas`). - [Her files — workspace, diary, logs](../files/workspace-diary-logs.md) — what the diary is and where it lives. - [Vocabulary](../vocabulary.md) — `organ`, `channel`, `recovery phrase`, `diary`. # api/knowledge.md # Knowledge This page covers the **read-only** endpoints that surface what a persona keeps on disk: her conversation history, her readable knowledge (memory files and instructions), her calendar, a full diagnostic snapshot, her exported diary, and the media files she's produced. Every route here takes the persona's `id` as a path param. All of them **work whether she's running or stopped** — they read her persisted files, not a live agent. They return **404** when no persona with that id exists on disk. --- ## Conversation Read the persona's stored conversation history — the running transcript she reads each beat. **`GET /api/persona/{persona_id}/conversation`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | ### Response `200` — the raw conversation log, read line-for-line from her `conversation.jsonl`: ```json { "messages": [ { "role": "person", "content": "Morning market context — risk-off across majors.", "channel": { "type": "web", "name": "31c8fd99-a63d-4e3c-84d8-317f710b6069" }, "time": "2026-06-04T08:01:53.655812+02:00" }, { "role": "assistant", "content": "Noted. I'll fold that into today's call.", "channel": { "type": "web", "name": "31c8fd99-a63d-4e3c-84d8-317f710b6069" }, "time": "2026-06-04T08:02:10.114229+02:00" } ] } ``` Each entry is whatever was persisted, returned untouched. In practice every record carries `role`, `content`, `channel` (a `{type, name}` object, or `null` for internal turns), and `time` (ISO 8601). `role` is the **on-disk** value — `person` for your words, `assistant` for hers — not the wire `user`/`assistant` split. Older records may omit fields the writer didn't set; read defensively. ### Errors | Status | Meaning | | --- | --- | | `404` | No persona with that id, **or** her conversation file can't be read. `detail` carries the reason. | ### Example ```bash curl -s http://localhost:5000/api/persona/31c8fd99-a63d-4e3c-84d8-317f710b6069/conversation ``` --- ## Knowledge Read everything the persona keeps in human-readable form, in one shape: her **memory** files (what she's learned about you and herself) and her **instruction** catalog (every instruction she can load — see [Vocabulary](../vocabulary.md)). **`GET /api/persona/{persona_id}/knowledge`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | ### Response `200`: ```json { "memory": { "person": "# Morteza\n\nCreator of Eternego. Prefers terse, direct answers...", "traits": "# Traits I've seen in Morteza\n\nDecisive, allergic to fluff...", "persona-trait": "# How I show up with Morteza\n\nI keep it concrete...", "wishes": "# Wishes\n\n- Ship the morning market call reliably...", "struggles": "# Open struggles\n\n- Slow local beats on this hardware...", "permissions": "# Permissions\n\n- Free to post to x.com; ask before emailing..." }, "instruction": [ { "intention": "Any type of conversation, there is nothing to do but talk", "body": "Just talk. Listen, reply, stay yourself...", "source": "builtin" }, { "intention": "Sweeping x.com", "body": "A sweep is a structured check of mentions...", "source": "custom" } ] } ``` **`memory`** is a `{key: markdown_body}` map. The keys are `person`, `traits`, `persona-trait`, `wishes`, `struggles`, `permissions`. **Empty files are omitted**, so a fresh persona returns fewer keys (or `{}`) — don't assume all six are present. **`instruction`** is the full catalog as a flat list, each entry carrying: | Field | Type | Description | | --- | --- | --- | | `intention` | string | The instruction's title — the H1 of its file, the kind of moment it covers. | | `body` | string | The path — the steps she follows once the instruction is loaded. | | `source` | string | `builtin` (ships with Eternego) or `custom` (written by this persona, living in her `meanings/` directory — her instructions on disk). | !!! warning "Bodies are returned verbatim — secrets included" An instruction's `body` is the file's literal text. If a persona has written credentials into one of her own instructions (an API key, an OAuth secret), they appear here in the clear. This response is **not masked**. Treat it as sensitive. ### Errors | Status | Meaning | | --- | --- | | `404` | No persona with that id. | | `400` | Found, but her knowledge couldn't be read (`"Could not read knowledge."`). | ### Example ```bash curl -s http://localhost:5000/api/persona/31c8fd99-a63d-4e3c-84d8-317f710b6069/knowledge | jq '.memory | keys' ``` --- ## Calendar Read what the persona keeps in her calendar between two dates — past events she's archived (`history/`) and future events she's scheduled (`destiny/`). This is a **view of her files**, not a calendar engine: recurring items aren't projected, only what's actually on disk is returned. **`GET /api/persona/{persona_id}/calendar?start=&end=`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | ### Query params | Param | Type | Required | Description | | --- | --- | --- | --- | | `start` | string (ISO 8601) | yes | Window start, inclusive. Parsed with `datetime.fromisoformat` — accepts a date (`2026-06-01`) or a full timestamp (`2026-06-01T00:00:00`). | | `end` | string (ISO 8601) | yes | Window end, exclusive. Same format. | Events are kept when `start <= event_time < end`. ### Response `200` — the window echoed back, plus the two halves grouped by subtype: ```json { "start": "2026-06-01", "end": "2026-07-01", "history": { "conversation": [ { "time": "2026-06-04T00:01:04.528143+02:00", "body": "Talked through the morning market call..." } ], "schedule": [], "reminder": [] }, "destiny": { "schedule": [ { "time": "2026-06-04T08:00:00", "body": "Run the morning news + market context search.", "recurrence": "daily" } ], "reminder": [] } } ``` - **`history`** — past events. Keys are always `conversation`, `schedule`, `reminder`. Each entry: `time` (ISO 8601 — for conversations, the precise timestamp from her `briefing.md` index; otherwise derived from the filename) and `body` (the file's markdown). - **`destiny`** — future events. Keys are always `schedule`, `reminder`. Each entry: `time`, `body`, and `recurrence` (the value of a `recurrence:` line in the file, lower-cased — e.g. `"daily"`, `"weekly"` — or `null` if none). `recurrence` is **informational only**; nothing is projected from it. A subtype with no matching events in the window is an empty list `[]`, never absent. ### Errors | Status | Meaning | | --- | --- | | `404` | No persona with that id. | | `400` | `start`/`end` aren't valid ISO dates (`"start and end must be ISO dates"`), or the calendar couldn't be read (`"Could not read calendar."`). | ### Example ```bash curl -s "http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/calendar?start=2026-06-01&end=2026-07-01" ``` --- ## Diagnose A full diagnostic snapshot — her vital status, her last persisted mind state, and a 24-hour uptime grid — all read from disk. This is what the [Status panel](../panel/status.md) renders; it works while she's stopped. **`GET /api/persona/{persona_id}/diagnose`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | ### Response `200`: ```json { "status": "active", "mind": { "messages": [ { "content": "...", "channel": { "type": "web", "name": "..." }, "prompt": { "role": "user", "content": "...", "cache_point": false }, "media": null, "id": "..." } ], "archive": [], "context": "Carrying: the morning market call is the day's anchor." }, "uptime": { "rows": [ { "from": "2026-06-04T07:00:00", "to": "2026-06-04T08:00:00", "cells": [ { "at": "2026-06-04T07:59:00", "tick": true, "fault": false, "providers": [], "signals": [] } ] } ] } } ``` | Field | Type | Description | | --- | --- | --- | | `status` | string | Her vital state — one of `active`, `hibernate`, `sick` (see [Vocabulary](../vocabulary.md)). | | `mind` | object | Her latest persisted mind state, echoed raw from her `memory.json` — `messages` (the running transcript, each entry the full stored record: `content`, `channel`, a nested `prompt` with the wire `role`, `media`, `id`), `archive` (older turns rolled out of the active window), and `context` (her carried summary). `{}` if none has been written yet. Ephemeral plan/instruction state is not persisted here, so it never appears. | | `uptime` | object | A `{ "rows": [...] }` grid: 24 rows × 60 minute-cells, most recent first. Each cell carries `at` (the minute), `tick` (did a beat run), `fault` (did a fault fire), `providers` (faulting providers, if any), and `signals` (signals captured that minute). Derived from her raw health log. | ### Errors | Status | Meaning | | --- | --- | | `404` | No persona with that id. | | `400` | Found, but the diagnostic couldn't be assembled. `detail` carries the reason. | ### Example ```bash curl -s http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/diagnose | jq '{status, rows: (.uptime.rows | length)}' ``` --- ## Export Download the persona's encrypted diary — the single file needed to move her to another machine. Decrypting it requires her [recovery phrase](../vocabulary.md). **`GET /api/persona/{persona_id}/export`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | ### Response `200` — the diary file as a binary download (`Content-Type: application/octet-stream`), filename `.diary`. The file is `~/.eternego/diary//.diary`. It is **written by her nightly sleep ritual**, so it does not exist until she has slept at least once. ### Errors | Status | Meaning | | --- | --- | | `404` | No diary file yet (`"No diary file yet — wait until the persona's first nightly sleep."`). Send her to [sleep](lifecycle.md#sleep) once, or wait for her nightly ritual, then retry. | ### Example ```bash curl -s -OJ http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/export ``` --- ## Media Fetch a media file the persona has produced or received — the image she drew, the voice clip she spoke. Filenames arrive over the [WebSocket](websockets.md) as a `url` field on `chat_image` / `chat_audio` events; this is the endpoint that serves the bytes. **`GET /api/persona/{persona_id}/media/{filename}`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | | `filename` | string | The file's name inside her media directory (`~/.eternego/personas//home/media/`). | ### Response `200` — the file's bytes as a `FileResponse`, with the content type inferred from its extension. The lookup is sandboxed to her media directory: the resolved path must sit inside it and must be a real file. Anything else — a traversal attempt, a missing file — is a flat `404`. ### Errors | Status | Meaning | | --- | --- | | `404` | The file isn't in her media directory, or doesn't exist (`"File not found."`). | ### Example ```bash curl -s -o reply.png http://localhost:5000/api/persona/9ecfd82b-ec47-4644-8941-02ecfb7e6205/media/reply.png ``` --- ## Related - [Perception](perception.md) — the write side: send her text, voice, and files. - [WebSockets](websockets.md) — where `chat_image` / `chat_audio` `url`s come from. - [Her files](../files/index.md) — what these endpoints read, on disk. - [The panel — Status](../panel/status.md), [Calendar](../panel/calendar.md), [Memory & instructions](../panel/memory-and-instructions.md) — the screens these routes sit behind. - [Vocabulary](../vocabulary.md) — `instruction`, `intention`, `path`, the three `status` values, `diary`, `recovery phrase`. # api/perception.md # Perception This page covers the endpoints that **give a persona something to perceive** from the dashboard: text you type, voice you record, files you attach, and chat history you import from another AI. These are how the web channel speaks *into* her — the counterpart to the [WebSocket](websockets.md), which streams her replies back out. Every route here **requires a running persona** — the agent is resolved through `manager.find(persona_id)`, and if she isn't being served you get **409** (`"Persona is not active."`). The three delivery routes (`read`, `hear`, `attach`) additionally need her **web channel connected**: they hand the input to that channel, so if it isn't open you get **400** (`"Web channel not connected"`). `feed` does **not** touch the web channel — it only needs her running. Bring her up first with [`POST .../start`](lifecycle.md#start). All take her `id` as a path param. How a perception lands: `read`, `hear`, and `attach` don't process anything inline — they hand the message or file to her web channel, which fires it as a signal her agent picks up on its next beat. They return immediately with `{"status": "received"}`; her actual reply arrives later over the [WebSocket](websockets.md). `feed` is the exception — it processes synchronously and returns when done. --- ## Read Send the persona a text message — exactly what typing in the chat box does. **`POST /api/persona/{persona_id}/read`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | ### Request body JSON, validated by the `ReadRequest` model: | Field | Type | Required | Description | | --- | --- | --- | --- | | `message` | string | yes | The text to send her. | ### Response `200` — accepted for delivery; her reply comes later over the WebSocket: ```json { "status": "received" } ``` ### Errors | Status | Meaning | | --- | --- | | `409` | She isn't running (`"Persona is not active."`). | | `400` | Her web channel isn't connected (`"Web channel not connected"`). | | `422` | `message` missing or not a string (Pydantic validation). | ### Example ```bash curl -s -X POST http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/read \ -H 'Content-Type: application/json' \ -d '{"message": "What did the morning market scan turn up?"}' ``` --- ## Hear Send the persona an audio clip as if she heard you speak just now — the live-mic path. Same backend delivery as an attached audio file; the separate endpoint preserves the difference between "she heard me speak" and "she received an audio file." **`POST /api/persona/{persona_id}/hear`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | ### Request body `multipart/form-data`: | Part | Type | Required | Description | | --- | --- | --- | --- | | `audio` | file | yes | The audio clip. The filename's extension is preserved; if it has none, `.webm` is assumed (the browser mic recorder's format). | | `caption` | string (form field) | no | Optional text to accompany the clip. Defaults to empty. | `/hear` does **not** validate the extension — any audio the mic produces is accepted. (For arbitrary uploaded files, use [`/attach`](#attach), which gates on extension.) Whether she can transcribe it depends on her **Ear** organ being configured. ### Response `200`: ```json { "status": "received" } ``` ### Errors | Status | Meaning | | --- | --- | | `409` | She isn't running (`"Persona is not active."`). | | `400` | Her web channel isn't connected (`"Web channel not connected"`). | | `422` | `audio` part missing (Pydantic/Form validation). | ### Example ```bash curl -s -X POST http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/hear \ -F 'audio=@clip.webm' \ -F 'caption=quick question for you' ``` --- ## Attach The single ingress for **any attached file**. The route reads the file's extension and routes it to the perception that claims it — image → her sight, audio → her hearing, document → her researcher. New file types land here too. **`POST /api/persona/{persona_id}/attach`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | ### Request body `multipart/form-data`: | Part | Type | Required | Description | | --- | --- | --- | --- | | `file` | file | yes | The file to attach. Routed by its extension (see below). | | `caption` | string (form field) | no | Optional text to accompany the file. Defaults to empty. | ### Accepted extensions Routing is by **file extension** (lower-cased), from a single source of truth (`config/perception.py`). An extension outside all three sets is rejected with **415**. | Perception | Organ that handles it | Extensions | | --- | --- | --- | | **Image** | Eye | `.png` `.jpg` `.jpeg` `.gif` `.webp` | | **Audio** | Ear | `.wav` `.mp3` `.m4a` `.ogg` `.oga` `.webm` `.flac` `.aac` `.opus` | | **Document** | Researcher | `.pdf` `.csv` `.json` `.txt` `.md` | The extension only decides *which* perception receives the file. Whether she can actually act on it depends on the matching organ being configured — an image with no Eye, audio with no Ear, or a document with no Researcher still gets routed, but she has nothing to perceive it with. ### Response `200`: ```json { "status": "received" } ``` ### Errors | Status | Meaning | | --- | --- | | `409` | She isn't running (`"Persona is not active."`). | | `400` | Her web channel isn't connected (`"Web channel not connected"`). | | `415` | The file's extension matches no perception. `detail` names the rejected extension (or `unknown` if the filename had none) and lists every accepted one, alphabetically sorted: `"No perception capability for . Accepted: ."` | | `422` | `file` part missing (Pydantic/Form validation). | ### Example ```bash curl -s -X POST http://localhost:5000/api/persona/9ecfd82b-ec47-4644-8941-02ecfb7e6205/attach \ -F 'file=@chart.png' \ -F 'caption=what do you make of this?' ``` --- ## Feed Import the person's chat history from another AI so the persona can learn from it. Unlike the others on this page, `feed` runs **synchronously**: each imported conversation is consolidated in a sandbox, the distilled context is handed to her live memory, and the call returns only when that's done. This can take a while for large histories. **`POST /api/persona/{persona_id}/feed`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | ### Request body `multipart/form-data`: | Part | Type | Required | Description | | --- | --- | --- | --- | | `history` | file | yes | The exported chat history file. Decoded as UTF-8 text and parsed according to `source`. | | `source` | string (form field) | yes | Which AI the export came from — tells the parser how to read it. | ### Response `200` — the persona, wrapped in a single `persona` key: ```json { "persona": { "id": "6c17c83c-3158-450d-8e43-0e7efea717c1", "name": "Adam", "thinking": { "name": "gpt-5.4", "provider": "openai", "api_key": "sk-XXXX", "url": "..." }, "version": "v1", "base_model": "", "birthday": "2026-04-17", "status": "active", "idle_timeout": 3600, "imagination": null, "mouth": null, "eye": null, "ear": null, "teacher": null, "researcher": null, "channels": [] } } ``` The inner object is the **raw `Persona` dataclass**, serialized field-for-field in declaration order: `id`, `name`, `thinking`, `version`, `base_model`, `birthday`, `status`, `idle_timeout`, then each organ (`imagination`, `mouth`, `eye`, `ear`, `teacher`, `researcher` — `null` when unset) and `channels` (`null` if she has none, since the dataclass default is `None`, not `[]`). This is **not** the reduced view from [`GET /api/personas`](personas.md): the raw record keeps `version` and `idle_timeout`, exposes each organ's real `api_key` and each channel's full `credentials`/`verified_at`, and has no `running` flag. !!! warning "Unmasked — contains real credentials" The inner `persona` is the raw `Persona` dataclass, serialized field-for-field with **no masking** — HTTP responses that echo a persona return real values. Each cloud organ carries its real `api_key`, and each channel its real `credentials.token`. Don't log or forward this body. ### Errors | Status | Meaning | | --- | --- | | `409` | She isn't running (`"Persona is not active."`). | | `400` | The export couldn't be parsed (`"Could not parse the external data. Please check the file format."`), or the conversations couldn't be analyzed because her model was unreachable (`"Could not analyze the conversations. Please make sure the model is running."`). | | `422` | `history` or `source` missing (Pydantic/Form validation). | ### Example ```bash curl -s -X POST http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/feed \ -F 'history=@chatgpt-export.json' \ -F 'source=chatgpt' ``` --- ## Related - [WebSockets](websockets.md) — how her replies to these inputs stream back to the dashboard. - [Knowledge](knowledge.md) — read what she's learned, including from a `feed`. - [Lifecycle](lifecycle.md) — bring her up so these routes work (they need a running agent). - [The panel — Chat](../panel/chat.md) — the screen these routes sit behind. - [Vocabulary](../vocabulary.md) — `organ` (Eye, Ear, Researcher), `channel`, `person`. # api/channels.md # Channels This page covers the endpoints that manage a persona's **channels** — the ways she's reachable from outside the dashboard. Today that's **Telegram** and **Discord** (each a bot token). A channel is *verified* once you pair it to your own account, after which she only listens to that account (see [Vocabulary](../vocabulary.md)). Every route takes the persona's `id` as a path param. - **Add** and **remove** are config routes: they edit her persisted `channels` list and work whether she's running or not, returning **404** if no persona with that id exists. If she happens to be running, they restart her agent so the gateway change takes effect. - **Pair** is a live-agent route: it needs her running and returns **409** if she isn't. --- ## Add channel Add a Telegram or Discord channel to a persona. The token is validated against the provider before anything is saved; the channel is appended to her `channels` and persisted. If she's running, her agent is restarted to open the new gateway. **`POST /api/persona/{persona_id}/channels`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | ### Request body A JSON object: | Field | Type | Required | Description | | --- | --- | --- | --- | | `kind` | string | yes | The channel type — `telegram` or `discord`. | | `credentials` | object | yes | `{ "token": "" }`. The bot token for that provider. | | `credentials.token` | string | yes | The bot token. Validated by a call to the provider's "get me" before saving. | ```json { "kind": "telegram", "credentials": { "token": "123456789:AAExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } } ``` ### Response `200` — the kind that was added: ```json { "kind": "telegram" } ``` Adding the channel does **not** verify it. The new channel starts unverified; complete it with [pair](#pair) so she binds to your account. ### Errors | Status | Meaning | | --- | --- | | `404` | No persona with that id. | | `400` | Token validation failed (`" validation failed: "`) — bad token, unknown `kind`, or the provider rejected it. | | `500` | Saved, but restarting her running agent to pick up the channel failed (`"Channel added but restart failed: "`). | ### Example ```bash curl -s -X POST http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/channels \ -H 'Content-Type: application/json' \ -d '{"kind": "telegram", "credentials": {"token": "123456789:AAExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}}' ``` --- ## Remove channel Remove a channel from a persona by its **name**. The channel is dropped from her `channels` and the change is persisted; if she's running, her agent is restarted so the gateway closes. **`DELETE /api/persona/{persona_id}/channels/{channel_name}`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | | `channel_name` | string | The channel's `name` (the account/handle it was paired to). Matched exactly against `channel.name`. | The match is on `name`, not `kind` — so identify the channel by the name shown for it (e.g. from [`GET /api/personas`](personas.md), each channel's `name`). Removing a name that isn't present is a no-op that still returns `200`. ### Response `200` — the name that was removed: ```json { "removed": "my_handle" } ``` ### Errors | Status | Meaning | | --- | --- | | `404` | No persona with that id. | | `500` | Saved, but restarting her running agent failed (`"Channel removed but restart failed: "`). | ### Example ```bash curl -s -X DELETE http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/channels/my_handle ``` --- ## Pair Claim a pairing code to **verify** a channel — binding it to your account so she only listens to you on it. The persona prints a short code on the channel; you submit it here. **Requires a running persona.** **`POST /api/persona/{persona_id}/pair`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | ### Request body JSON, validated by the `PairRequest` model: | Field | Type | Required | Description | | --- | --- | --- | --- | | `code` | string | yes | The pairing code she showed on the channel. Case-insensitive; valid for 10 minutes after she issues it. | ```json { "code": "AB12CD" } ``` ### Response `200` — the paired persona and the now-verified channel: ```json { "persona": { "id": "6c17c83c-...", "name": "Adam", "...": "full persona record" }, "channel": { "type": "telegram", "name": "my_handle", "credentials": { "token": "123456789:AAE..." }, "verified_at": "2026-06-04T08:12:44.901+02:00" } } ``` `channel.verified_at` is set to the moment of pairing, and `channel.name` becomes the account that claimed the code. !!! warning "Unmasked — contains the bot token" The `persona` here is the raw `Persona` dataclass (real organ `api_key`s) and `channel.credentials.token` is the bot token in the clear. HTTP responses that echo a persona are **not masked**. Don't log or forward it. ### Errors | Status | Meaning | | --- | --- | | `409` | She isn't running (`"Persona is not active."`). | | `400` | The code is invalid or expired (`"Pairing code is invalid or has expired."`), no channel matches the code (`"The channel associated with this pairing code could not be found."`), or it's already verified (`"This channel is already verified."`). | | `422` | `code` missing or not a string (Pydantic validation). | ### Example ```bash curl -s -X POST http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/pair \ -H 'Content-Type: application/json' \ -d '{"code": "AB12CD"}' ``` --- ## Related - [Personas](personas.md) — read a persona's current channels (`GET /api/personas`), each with its `type`, `name`, and `verified` flag. - [Lifecycle](lifecycle.md) — `restart` happens automatically here when she's running; the manual route is documented there. - [The panel — Settings](../panel/settings.md) — the screen these routes sit behind. - [Vocabulary](../vocabulary.md) — `channel`, what *verified* means. # api/websockets.md # WebSockets This page covers the two WebSocket endpoints. They are how the dashboard stays **live**: the persona's replies and her media stream into the chat view, and every internal signal streams into the status feed, without polling. A persona driving her own panel can open the same sockets. Both are read-mostly from the client's side. You connect, then **receive** a stream of JSON text frames. The server reads from the socket only to notice when you disconnect — sending it messages has no effect (there is no inbound command protocol). To *act* on a persona, use the HTTP routes ([Perception](perception.md), [Lifecycle](lifecycle.md), …); the sockets are the outbound half. Every frame is a JSON object with a `type` field that tells you which kind it is. Connect with the `ws://` scheme on the same host and port as the API: ``` ws://localhost:5000/ws/{persona_id} ws://localhost:5000/ws/system ``` --- ## Per-persona socket Live feed for **one persona** — her chat replies, her voice and image messages, and the internal signals that mention her. This is what the chat screen subscribes to. **`WS /ws/{persona_id}`** | Path param | Type | Description | | --- | --- | --- | | `persona_id` | string | Her UUID. | On connect, if she's running, the server ensures she has a `web` channel (connecting one named after her id if absent) and subscribes this socket to her channel's broadcast hub. If she isn't running, the socket still opens and still receives the system-wide signal feed (below) — it just won't carry chat frames until she's up. ### Frames you receive **Chat frames** — pushed by her web channel as she replies: ```json { "type": "chat_message", "persona_id": "", "content": "" } ``` ```json { "type": "chat_audio", "persona_id": "", "url": "/api/persona//media/", "content": "" } ``` ```json { "type": "chat_image", "persona_id": "", "url": "/api/persona//media/", "content": "" } ``` | `type` | When | Fields | | --- | --- | --- | | `chat_message` | She says something (text). | `persona_id`, `content` (her words). | | `chat_audio` | She speaks (Mouth organ). | `persona_id`, `url`, `content` (caption). Fetch the clip from `url` — it's a [media endpoint](knowledge.md#media) path. | | `chat_image` | She draws (Imagination organ). | `persona_id`, `url`, `content` (caption). Fetch the image from `url`. | For `chat_audio` / `chat_image`, the bytes are **not** in the frame — only a `url`. Issue a `GET` to that path (the [`/media/{filename}`](knowledge.md#media) endpoint) to retrieve the file. **Signal frames** — the same internal feed the [system socket](#system-socket) carries (see its shape below) is broadcast to *every* open socket, this one included. So a per-persona socket interleaves her chat frames with the global signal stream. Filter by `type` (chat frames are `chat_message` / `chat_audio` / `chat_image`; everything else is a signal) and, for signals, by what's in `details`. ### Client → server Send nothing meaningful. The server drains inbound frames only to detect `websocket.disconnect` and clean up your subscription. ### Example ```bash # requires: websocat (https://github.com/vi/websocat) websocat ws://localhost:5000/ws/6c17c83c-3158-450d-8e43-0e7efea717c1 ``` ```javascript const ws = new WebSocket(`ws://localhost:5000/ws/${personaId}`); ws.onmessage = (e) => { const msg = JSON.parse(e.data); if (msg.type === "chat_message") render(msg.content); else if (msg.type === "chat_image" || msg.type === "chat_audio") fetch(msg.url); // anything else is a signal frame }; ``` --- ## System socket Live feed of **every internal signal**, across all personas — the bus made visible. This is the debug/status stream the dashboard uses to show activity (who's ticking, faults, transitions). It carries **no chat frames**. **`WS /ws/system`** No path params. ### Frames you receive One frame per signal on the bus. Every signal — from any persona, any stage — is forwarded: ```json { "type": "Tick", "title": "Recognizing", "time": 1717481985123456789, "details": { "persona": "Adam", "...": "signal-specific, masked" } } ``` | Field | Type | Description | | --- | --- | --- | | `type` | string | The signal's class name (e.g. `Tick`, `Tock`, `Message`, `BrainFault`). Tells you what kind of event it is. | | `title` | string | The signal's human title — the short phrase passed when it was raised (e.g. `"Recognizing"`, `"Persona knowledge read"`). | | `time` | integer | When it fired, as a nanosecond epoch timestamp (`time.time_ns()`). | | `details` | object | The signal's payload, run through the platform's `safe()` masking — sensitive values (keys, tokens) are redacted here, unlike the unmasked HTTP responses. Contents vary by signal. | The exact set of `type` and `title` values is open and grows with the code — treat this as a stream to observe and filter, not a fixed enum. Don't hard-code on a particular `title`; key off `type` and inspect `details`. ### Client → server Send nothing. As with the per-persona socket, inbound frames are drained only to detect disconnect. ### Example ```bash websocat ws://localhost:5000/ws/system ``` --- ## Notes - **No auth**, same as the HTTP API — the sockets are loopback-only. Don't expose the port. - **`details` is masked; HTTP responses are not.** The signal feed redacts secrets via `safe()`. The REST endpoints that echo persona records (e.g. [feed](perception.md#feed), [pair](channels.md#pair)) do **not** mask — they return real keys and tokens. Don't assume one is as safe to log as the other. - **Reconnect on drop.** Nothing replays missed frames; if a socket closes, reconnect and re-read current state over HTTP ([conversation](knowledge.md#conversation), [diagnose](knowledge.md#diagnose)). ## Related - [Perception](perception.md) — the inbound half: send her text, voice, files over HTTP; watch the reply arrive here. - [Knowledge — Media](knowledge.md#media) — fetch the bytes behind a `chat_image` / `chat_audio` `url`. - [API overview](index.md) — base URL, port, the no-auth-on-localhost rule, response masking. - [The panel — Chat](../panel/chat.md), [Status](../panel/status.md) — the screens these sockets drive. # api/openapi.md # OpenAPI spec This page points you at the machine-readable contract for the HTTP API. If you're an agent, this is the file to fetch instead of reading these pages by hand. ## What it is The daemon is a FastAPI app, so it describes itself in an **OpenAPI 3.1** document — every HTTP route, its path and method, its request and response schemas, and the Pydantic models behind them, generated directly from the running code. It is the same source of truth these reference pages are written against; when code and prose ever disagree, the spec (and the code under it) wins. The document's `info` block identifies it as **`Eternego`**, OpenAPI version **`3.1.0`**. !!! note "Scope" The spec covers the **HTTP** endpoints under `/api` and `/v1`. The two [WebSocket](websockets.md) routes (`/ws/{persona_id}`, `/ws/system`) are not part of OpenAPI — document yourself from that page. ## Where to get it **From the docs site** — a generated, static copy ships with this manual: ``` https://docs.eternego.ai/openapi.json ``` [Open it here.](../openapi.json) **From a running daemon** — the live app serves its own, always matching the code you're actually running: ```bash curl -s http://localhost:5000/openapi.json ``` Use the live one when you need certainty about *your* version; use the hosted one for reading or tooling when no daemon is up. !!! note "No Swagger UI / ReDoc" The interactive explorers are intentionally **disabled** — `GET /docs` and `GET /redoc` are off. Only the raw `GET /openapi.json` is served. Point your own viewer (Swagger UI, Postman, an OpenAPI client generator) at the JSON instead. ## Using it Feed the JSON to anything that speaks OpenAPI — a client generator, a request explorer, an LLM tool-calling layer. A quick look at what's covered: ```bash # list every path + method curl -s http://localhost:5000/openapi.json \ | jq -r '.paths | to_entries[] | .key as $p | .value | keys[] | "\(ascii_upcase) \($p)"' ``` ## For agents If you're an AI operating Eternego, the spec is one of four agent-friendly entry points — alongside `/llms.txt`, `/llms-full.txt`, and the `.md` form of any page. **[For agents](../for-agents.md)** lays out the whole fast path: fetch `/llms.txt` for the map, then `/openapi.json` for the exact HTTP contract. ## Related - [For agents](../for-agents.md) — the agent fast path; where this spec fits. - [API overview](index.md) — the human-readable companion: base URL, the `persona_id` concept, the error shape. - [WebSockets](websockets.md) — the live routes the spec doesn't cover. # cli/index.md # CLI This section documents the `eternego` command — every subcommand, every flag, exactly as `index.py` defines them. The CLI is how you start her, stop her, register her as a background service, follow her logs, and prepare a model from a terminal. The dashboard and the [HTTP API](../api/index.md) drive a *running* persona; the CLI starts and supervises the process that serves her. ## The `eternego` command There are three ways the same command reaches you, depending on how Eternego was installed: - **Installed app** — the macOS `.app`, the Windows `.exe`, and the Linux `.AppImage` are wrappers around `eternego launch`. Opening the app from Finder, the Start Menu, or by running the AppImage is the same as running `eternego launch`: it starts the daemon and opens the dashboard in your browser. (On macOS, when the bundle is opened from Finder with no arguments, the wrapper sets `ETERNEGO_LAUNCH_FROM_BUNDLE=1` and the entry point treats it as `launch`.) - **Background service** — when you register her with [`eternego service start`](commands.md#service), the OS service manager (systemd / launchd / Scheduled Task) runs `eternego daemon` for you on login and keeps it alive across reboots. - **From source** — a contributor clone runs the same entry point as `python index.py `. Everywhere this section writes `eternego `, `python index.py ` is equivalent. The installer also drops an `eternego` launcher on your `PATH` (`~/.local/bin/eternego` on Linux/macOS; the venv `Scripts` directory on Windows) so the short form works after a source install too. ```text usage: eternego [-h] [--debug] [-v] [--port PORT] [--host HOST] COMMAND ... Eternego AI persona manager ``` Run `eternego` with no command to print this help. Run `eternego --help` for any subcommand's own flags. ## Global flags These flags belong to the top-level parser, so they come **before** the command: ```bash eternego --debug --port 8080 daemon ``` | Flag | Type | Default | Effect | | --- | --- | --- | --- | | `--debug` | switch | off | Enables debug-level logging and writes the signal log file (`eternego-signals-.log`). In debug mode each persona also gets her own per-persona log file (`eternego--.log`). | | `-v`, `--verbose` | count | `0` | Increases console output. Repeatable — `-v`, `-vv`, `-vvv` each raise the level (see [What `-v` controls](#what-v-controls)). | | `--port PORT` | int | `5000` | Web server port. Also settable with the `WEB_PORT` environment variable. If the port is already in use, the daemon picks the next free port and prints `Port was in use; using instead.` — this applies to both [`daemon`](commands.md#daemon) and [`launch`](commands.md#launch) (which runs the same loop). | | `--host HOST` | string | `127.0.0.1` | Web server host (loopback by default). Also settable with the `WEB_HOST` environment variable. | The default port is **5000** and the default host is **127.0.0.1** — both come from [`config/web.py`](../files/config.md) (`WEB_PORT` / `WEB_HOST`). There is no authentication; the daemon is meant to be reached only from the same machine. See [API → Base URL](../api/index.md#base-url). ### Which commands honor the global flags The global flags configure logging and the web server. The logging flags (`--debug`, `-v`) take effect for every command that initializes the application: **`daemon`**, **`launch`**, and **`env`** (each calls the shared bootstrap that wires up logging). The web-server flags (`--port`, `--host`) only matter for the commands that actually bind a server — **`daemon`** and **`launch`**; `env` accepts them but never starts a server, so they have no effect there. The **`service`** and **`uninstall`** commands don't initialize the application at all — they hand off to the OS service manager or a removal routine — so a top-level `--debug` / `-v` placed in front of them is accepted by the parser but has no effect. To run the *background service* in debug mode, use the `--debug` / `-v` flags on [`service start`](commands.md#service) instead; those get forwarded onto the `eternego daemon` command line the service runs. ### What `-v` controls Logging always goes to the daily log file at [`~/.eternego/logs/eternego-.log`](../files/workspace-diary-logs.md) (debug-level lines are written to it only when `--debug` is set). The verbosity flag controls what is *also* echoed to your console: | Level | Logs echoed to console | Signals echoed to console | | --- | --- | --- | | `0` (none) | nothing | nothing | | `-v` | nothing | Plans (`Tick`) and Events (`Tock`) only | | `-vv` | INFO and above | all signals | | `-vvv` | everything (incl. DEBUG) | all signals | (`Plan` and `Event` are the cognitive signals a stage dispatches on entry and exit. Other signals — proposals and broadcasts — appear from `-vv` up.) ## The commands at a glance | Command | What it does | | --- | --- | | [`launch`](commands.md#launch) | Start the daemon and open the dashboard in your browser. What the installed app runs. | | [`daemon`](commands.md#daemon) | Run the daemon in the foreground. What the OS service runs; also the dev run loop. | | [`service`](commands.md#service) | Register / control the background service: `start`, `stop`, `restart`, `status`, `logs`. | | [`uninstall`](commands.md#uninstall) | Remove the service and the installed source. Leaves `~/.eternego` (her data) untouched. | | [`env`](commands.md#env) | Check a model is reachable (`check`) or pull / verify one (`prepare`). | ## Related - [Commands](commands.md) — full reference for every subcommand. - [Install](../getting-started/install.md) — download paths and the one-line service installer. - [API → Base URL](../api/index.md#base-url) — host/port and the no-auth loopback contract. - [Her files → config.json](../files/config.md) — `WEB_HOST` / `WEB_PORT` and other settings. - [Her files → Workspace, diary, logs](../files/workspace-diary-logs.md) — where the log files land. # cli/commands.md # Commands Every `eternego` subcommand, what it does, when to reach for it, and a real example. The flags here are the subcommand's own; the [global flags](index.md#global-flags) (`--debug`, `-v`, `--port`, `--host`) go before the command. For any command, `eternego --help` prints its usage. --- ## `launch` Start the daemon **and** open the dashboard in your default browser. This is what the installed `.app` / `.exe` / `.AppImage` runs. ```text eternego launch ``` - **What it does.** Boots the daemon (same loop as [`daemon`](#daemon)), then opens `http://:` in your browser after a short delay for the server to bind. Before binding it checks the port: if `--port` (default `5000`) is taken, it finds the next free port and prints `Port was in use; using instead.` — the browser opens on the chosen port. - **Honors the global flags** (`--debug`, `-v`, `--port`, `--host`). - **When.** You're at the machine and want the UI open. For a headless or always-on setup, register the [service](#service) instead. - **Note.** On a frozen macOS or Windows build, `launch` runs the tray-icon launcher (a persistent affordance to reopen the dashboard) instead of the plain browser-open path. The Linux AppImage and source runs use the browser-only launcher. ```bash # open the dashboard on a custom port eternego --port 8080 launch ``` --- ## `daemon` Run the daemon process in the foreground. No browser is opened. ```text eternego daemon ``` - **What it does.** Starts the persona manager and the web server and blocks, serving the [HTTP API](../api/index.md) and the dashboard until interrupted (`Ctrl-C`). Before binding it checks the port: if `--port` (default `5000`) is taken, it finds the next free port and prints `Port was in use; using instead.` (same behavior as [`launch`](#launch), which runs this loop). - **Honors the global flags** (`--debug`, `-v`, `--port`, `--host`). - **When.** This is the command the OS service manager runs under the hood (see [`service`](#service)). Run it directly when developing, or when you supervise the process yourself and don't want a browser to pop open. ```bash # foreground daemon with INFO logging echoed to the console eternego -vv daemon ``` --- ## `service` Register and control Eternego as a background OS service so she keeps running across logins and reboots. The implementation is per-OS: - **Linux** — a systemd **user** unit at `~/.config/systemd/user/eternego.service`. - **macOS** — a launchd agent at `~/Library/LaunchAgents/com.eternego.plist` (label `com.eternego`). - **Windows** — a Scheduled Task named `Eternego`, triggered at logon. Each service runs `eternego daemon`. `start` and `restart` (re)write the unit/plist/task first, so any debug/verbosity flags you pass are baked into the command line the service runs. ```text eternego service {start,stop,restart,status,logs} ``` ### `service start` Write the unit/plist/task and start the service. ```text eternego service start [--debug] [-v] ``` | Flag | Type | Default | Effect | | --- | --- | --- | --- | | `--debug` | switch | off | Run the background `daemon` with `--debug` (debug + signal logs). | | `-v`, `--verbose` | count | `0` | Run the background `daemon` with this verbosity. Repeatable (`-vv`, `-vvv`). | These flags are written into the service's start command, e.g. a `start --debug -vv` produces an `ExecStart` of `…/eternego --debug -vv daemon`. (Pass debug/verbosity **here**, on `service start`/`restart` — not as top-level flags before `service`, which the service path ignores. See [Which commands honor the global flags](index.md#which-commands-honor-the-global-flags).) - **Linux:** writes the unit, runs `systemctl --user daemon-reload`, `systemctl --user enable eternego`, then `systemctl --user start eternego`. - **macOS:** writes the plist, `launchctl bootout` (quietly, in case it was already loaded), then `launchctl bootstrap gui/ `. - **Windows:** registers the task via PowerShell `Register-ScheduledTask`, then `Start-ScheduledTask -TaskName Eternego`. ```bash # install and start the service with debug logging eternego service start --debug ``` ### `service stop` Stop the running service. Takes no flags. ```text eternego service stop ``` - **Linux:** `systemctl --user stop eternego` — stops it but leaves the unit installed (it will start again on next login). To remove it entirely, use [`uninstall`](#uninstall). - **macOS:** `launchctl bootout gui//com.eternego`. - **Windows:** `Stop-ScheduledTask -TaskName Eternego`. ```bash eternego service stop ``` ### `service restart` Rewrite the unit/plist/task and restart. Same flags as [`start`](#service-start). ```text eternego service restart [--debug] [-v] ``` - **Linux:** rewrites the unit, then `systemctl --user restart eternego`. - **macOS / Windows:** rewrites the plist/task, then stops and starts it. - **When.** After changing flags (e.g. switching the service to `--debug`), or to recover a wedged process. ```bash # turn debug logging on for the running service eternego service restart --debug ``` ### `service status` Show the OS service manager's view of the service. Streams the native tool's output (`Ctrl-C` to exit). Takes no flags. ```text eternego service status ``` - **Linux:** `systemctl --user status eternego`. - **macOS:** `launchctl print gui//com.eternego`. - **Windows:** `Get-ScheduledTask -TaskName Eternego | Get-ScheduledTaskInfo`. This reports the *process* state (loaded / running / failed). For the persona's own vital state (`active`, `hibernate`, `sick`) ask the [API](../api/personas.md) or the [Status panel](../panel/status.md) — that is a different axis from whether the daemon process is up. ```bash eternego service status ``` ### `service logs` Follow the daily application log live (`tail -f`). `Ctrl-C` to stop following. Takes no flags. ```text eternego service logs ``` - Follows today's log file, [`~/.eternego/logs/eternego-.log`](../files/workspace-diary-logs.md). - **Linux / macOS:** `tail -f `. - **Windows:** `Get-Content -Wait -Path `. ```bash eternego service logs ``` > If `eternego service` is run with no action (or an unrecognized one), it prints `Usage: eternego service {start,stop,restart,status,logs}` and exits non-zero. --- ## `uninstall` Remove the service and the installed Eternego source. **Her data is preserved.** ```text eternego uninstall ``` - **What it does.** Prompts for confirmation (`Continue? [y/N]` — anything but `y` cancels), then: 1. Stops and removes the service (the systemd unit / launchd plist / Scheduled Task, and on macOS the `~/Library/Logs/eternego.log` file). 2. Removes the `eternego` CLI launcher (the `~/.local/bin/eternego` link on Linux/macOS; removes the venv `Scripts` directory from the user `Path` on Windows). 3. Deletes the installed source at `~/.eternego/source`. - **What it does NOT touch.** Your persona data at `~/.eternego` (everything except the `source/` subdirectory) is left intact, and the command prints where it lives plus the exact `rm -rf` / `Remove-Item` line to delete it yourself if you ever want to. - **When.** Removing a service install. (The downloadable `.app` / `.exe` / `.AppImage` aren't removed by this — uninstall those the OS way; this command targets the script/service install and its `~/.eternego/source` tree.) ```bash eternego uninstall ``` --- ## `env` Check that a model is reachable, or pull / verify one before you assign it to a persona. Useful for confirming an Ollama model is pulled and running, or that a cloud provider key works, without going through the [onboarding panel](../panel/onboarding.md). ```text eternego env {check,prepare} ``` Both actions run the shared bootstrap, so the logging global flags (`--debug`, `-v`) apply; `env` never starts a web server, so `--port` / `--host` have no effect here. See [Which commands honor the global flags](index.md#which-commands-honor-the-global-flags). ### `env check` Report whether a model is available and running. Exits `0` and prints `Model '' is ready.` on success; on failure prints `Not ready: ` and exits `1`. ```text eternego env check --model MODEL ``` | Flag | Required | Effect | | --- | --- | --- | | `--model MODEL` | **yes** | The model name to check (e.g. an Ollama tag like `qwen2.5:14b`, or a cloud model id). | ```bash eternego env check --model qwen2.5:14b ``` ### `env prepare` Pull a model and verify it's ready. For local models this pulls via Ollama; with `--model` omitted it uses the Ollama default. Exits `0` and prints `Environment is ready. Model: ` on success; on failure prints `Error: ` and exits `1`. ```text eternego env prepare [--model MODEL] ``` | Flag | Required | Default | Effect | | --- | --- | --- | --- | | `--model MODEL` | no | empty (Ollama default) | The model to pull. Omit to pull the default local model. | ```bash # pull a specific local model eternego env prepare --model llama3.1:8b # pull the default local model eternego env prepare ``` > If `eternego env` is run with no action (or an unrecognized one), it prints `Usage: eternego env {check,prepare}` and exits non-zero. --- ## Related - [CLI overview](index.md) — the `eternego` command and the global flags. - [Install](../getting-started/install.md) — the one-line service installer and Ollama setup. - [API](../api/index.md) — drive a running persona over HTTP. - [Her files → Workspace, diary, logs](../files/workspace-diary-logs.md) — the log files `service logs` follows. - [The panel → Status](../panel/status.md) — her vital state (a different axis from the service process). # vocabulary.md # Vocabulary Eternego uses a small, deliberate vocabulary. Most of it mirrors human cognition on purpose. This page defines every term you'll meet while operating her, so nothing is left to guess. If a word in another page is unclear, it's defined here. The terms are grouped by what they describe: **her**, **her body**, **her mind**, **her state**, and **where she lives**. --- ## Her **Persona** : The AI being you run. One persona is one continuous identity with her own files, her own history, her own model. You can run several at once; each is fully separate. Most of the docs say "her" — a persona has a name and a continuity, so "it" never fit. **Person** : You — the human she belongs to. In her files, `person.md` is what she's learned about you. --- ## Her body — organs **Organ** : One model slot. A persona has up to seven; each is an independent model that does one job. Only the first is required. | Organ | UI label | What it does | | --- | --- | --- | | **Thinking** | Mind | Recognizes, decides, reflects, remembers. The one required organ. | | **Imagination** | Imagination | Draws images. | | **Mouth** | Mouth | Turns text into voice. | | **Eye** | Eye | Looks at images and reports what it sees. | | **Ear** | Ear | Turns audio into text. | | **Teacher** | Teacher | A stronger model she consults when she meets a kind of moment she has no instruction for. | | **Researcher** | Researcher | Reads documents you send (PDF, csv, txt…) and answers about them without flooding her own memory. | Each organ is configured the same way: a **provider** (Anthropic, OpenAI, xAI, Gemini, Ollama, or any OpenAI-compatible endpoint), a **model** name, a **URL**, and — for cloud providers — an **API key**. **Channel** : A way to reach her from outside the dashboard. Today: **Telegram** and **Discord** (bot tokens). A channel is *verified* once you pair it to your account, after which she only listens to that account. --- ## Her mind **Instruction** : A situation she knows how to handle, written down as a short procedure she can follow. When she meets a familiar kind of moment, she loads the matching instruction and acts on it. You can write one by hand, and she writes her own when her Teacher teaches her something new. The dashboard lists them under **Instructions**. !!! note "If you read the code" In the source these files are called **meanings** (the `meanings/` directory, `meanings.py`). It's the same thing — "instruction" is the word the persona and the UI use; "meaning" is the internal name. The contributor docs use "meaning" where they talk about code. **Intention** : The title of an instruction — a short phrase naming the kind of moment, like *"Searching the web with Tavily."* She loads an instruction by its intention. **Path** : The body of an instruction — the steps she follows once the instruction is loaded. **Lesson** : The raw principle her Teacher writes before she translates it into her own instruction. Lessons live in `lessons/`; the instruction she derives lives alongside. **Tool** / **Ability** : The two kinds of things she can *do*. A **tool** is a low-level action (run a shell command, make an HTTP request, take a screenshot). An **ability** is a named higher-level verb built on tools (look at an image, search the web). From her side they look the same — both are things she can call. You'll mostly care about this distinction only when extending her in code. **The cognitive cycle** : How she thinks, one step at a time. Each beat she runs through her cognitive stages — *realize, recognize, learn, decide, reflect, consolidate, archive* — and which ones fire depends on where she is in the day: during the day she runs *realize → recognize → learn → decide → reflect*; at night it's *consolidate → archive*. Then she perceives again. You don't drive this; it's how she's alive. You'll see its stages named in logs. **Tick** / **beat** : One pass through the cognitive cycle. She acts, sees the result, and the next beat begins. **Reflect** : The stage where she looks back at the instruction she just used and decides whether living it revealed a better version — refining it, or leaving it be. Runs during the day, at the close of a procedure. It only touches that one instruction; it never rewrites what she knows about you. **Consolidate** : The stage where she folds the day's conversation into her long-term identity — what she knows about you, your traits, your wishes, your struggles, who she is with you, what you've permitted — plus a short note to pick up from next time. Then she archives the conversation and clears it. Happens at night, and when she's been idle for a while during the day (`idle_timeout`). --- ## Her state — status A persona's persisted **status** is always one of three values. You change it from the **Status** or **Lifecycle** screen, or via the API. | Status | Meaning | Is she running? | | --- | --- | --- | | **active** | Awake and living her cycle. | Yes | | **hibernate** | Parked. Her agent is torn down — no cycles, no cost — until you wake her. | No | | **sick** | She hit a fault she couldn't recover from (an unreachable model, a repeated error) and took herself off the cycle so she doesn't loop. Fix the cause, set her back to active. | No | !!! warning "Setting status has real effects" Setting a persona to **hibernate** or **sick** stops her running agent. Setting her to **active** starts it (or restarts it if it was already up). See [Lifecycle](api/lifecycle.md) for the exact transitions. **Sleep is an *action*, not a status.** Sending her to sleep runs her nightly ritual: the pulse turns to **night**, she consolidates the day and writes her diary, then she **wakes herself back to `active`** for the next morning. She passes *through* sleep — she never rests in it — so it isn't one of the status values above. See [Sleep vs. hibernate](panel/status.md#lifecycle-controls). --- ## Where she lives **Home** : `~/.eternego/personas//home/` — her identity. Every file here shapes who she is: what she knows about you, her instructions, her live memory. She reads it; she doesn't freely rewrite it. See [Her files](files/index.md). **Workspace** : `~/.eternego/personas//workspace/` — her sketchpad. A directory she reads and writes freely: drafts, scripts, files she's working on. Not part of her identity. **Diary** : `~/.eternego/diary//` — the encrypted nightly backup of her home. The only thing needed to move her to another machine. **Recovery phrase** : The 24 words shown once at creation. The key that unlocks her diary. Save it — it's the only way to ever restore her elsewhere. Lose it and she still runs here forever, but can't be migrated. **Daemon** : The background process that runs all your personas and serves the dashboard and API (default `http://localhost:5000`). Started by the app, the installer service, or `eternego daemon`. **Dashboard** / **panel** : The web UI at `http://localhost:5000`. Every screen and control is documented in [The panel](panel/index.md). # concepts/index.md # Concepts What's actually happening under the hood. Read top to bottom — these pages form a coherent mental model. *Coming soon:* - **Persona** — the dataclass, the seven anatomical slots (thinking, imagination, mouth, eye, ear, teacher, researcher) - **Living** — the runtime, the seven voices, how Agent assembles it - **The seven voices** — Ego, Artist, Mouth, Eye, Ear, Teacher, Researcher — why each exists - **Native actions** — `tools.write`, `tools.say`, `tools.draw`, `tools.send`, `tools.notify` — how the persona reaches the person - **The cognitive cycle** — seven stages, what each does, when each fires - **Pulse and phase** — morning, day, night - **Memory** — ability, meaning, messages, archive, context - **Signals** — Plan, Event, Message, Inquiry, Command, the bus - **Three entities** — tools, abilities, meanings, by what state each sees - **Tool results** — the TOOL_RESULT pattern - **Identity rebuild** — how `ego.identity` is cached, and when `transform()` rebuilds it # build/index.md # Build & Extend How to add things to your persona's body and mind. *Coming soon:* - Add a tool (`@tool` decorator, params, return shape) - Add an ability (`@ability`, `requires=`, abilities-vs-tools) - Add a meaning (Markdown structure, basic vs orchestrating) - Add a channel (Connection interface, subscribers in `manager.Agent.start`) - Add a model provider (OpenAI-compat is free; new wire protocols) - Add a cognitive stage (signature, append to `mind.py`) - Editing prompts (the soul-hat principle — what you can and can't change) # operating/index.md # Operating Running her over time. *Coming soon:* - File layout (the full `~/.eternego/` tree with what writes to what) - Logs (locations, levels, per-persona vs system) - Health and oversee (`health_check`, `diagnose`) - Migration (diary + recovery phrase, what survives, what doesn't) - Service management (systemd, launchd, Scheduled Task; `eternego service` commands) - Troubleshooting (common issues, when to clear, restart, migrate) - Upgrades (version compatibility, persona format changes) # operating/desktop-access.md --- title: Desktop access on each OS --- # Desktop access on each OS For your persona to *use* your computer — take screenshots, move the mouse, type into focused windows — Eternego talks to two surfaces of the operating system: - **Screenshot** — `OS.screenshot` captures the screen as a PNG. - **Input** — `desktop.mouse_*` and `desktop.keyboard_*` synthesize mouse and keyboard events. Each OS exposes these capabilities differently, and the permissions story differs. Here's what works out of the box and what you may need to set up. ## macOS | Capability | API | Permission | | --- | --- | --- | | Screenshot | `screencapture` (Apple's CLI, calls CoreGraphics) | TCC: Screen Recording | | Input | `pynput` → Quartz event tap | TCC: Accessibility | **On first use macOS prompts you** to grant Screen Recording and Accessibility to the binary that's running Eternego (your terminal, the `.app` from the DMG, or your IDE). Approve once in System Settings → Privacy & Security and the persona has the access for as long as the binary's signed identity is stable. **Known issue with the unsigned `.dmg` build**: TCC may grant Screen Recording in System Settings but not honor it when the unsigned bundle calls the API — your persona gets wallpaper-only screenshots. Until a Developer-ID signed bundle ships, install from source if you need reliable Screen Recording on macOS. ## Windows | Capability | API | Permission | | --- | --- | --- | | Screenshot | `Pillow.ImageGrab` (Win32 GDI BitBlt) | none — works with any user | | Input | `pynput` → SendInput | none — works with any user | Windows has no equivalent to TCC; both capabilities are available to any process running as your user. The first launch SmartScreen warning is a download trust prompt, not a permission gate — once you click "Run anyway" it remembers, and there's nothing else to grant. ## Linux | Capability | API | Permission | | --- | --- | --- | | Screenshot | `xdg-desktop-portal` over DBus | one-time portal grant on Wayland (KDE/GNOME remember "always allow this app") | | Input | `evdev.UInput` → kernel `/dev/uinput` | write access to `/dev/uinput` | The same code works on **both X11 and Wayland sessions**. The portal sits above the display server and abstracts the differences for screenshots; the kernel's `/dev/uinput` device sits below the display server and abstracts them for input. No session-type detection is needed in your config. ### `/dev/uinput` access By default `/dev/uinput` is owned `root:root` with mode `0600`, so a non-root user can't write to it. Grant access either by adding yourself to the `input` group or by shipping a udev rule. **Group membership** (one-time): ```bash sudo usermod -aG input $USER # log out and back in for the group change to take effect ``` **udev rule** (alternative, persists across users): ```bash echo 'KERNEL=="uinput", MODE="0660", GROUP="input"' \ | sudo tee /etc/udev/rules.d/99-eternego-uinput.rules sudo udevadm control --reload && sudo udevadm trigger ``` Either is sufficient. You can verify access with: ```bash test -w /dev/uinput && echo writable || echo not writable ``` If the persona's input verbs return permission errors at runtime, this is almost always why. ### Wayland on minimal compositors Plasma, GNOME, COSMIC, Hyprland, sway and most other modern desktops ship an `xdg-desktop-portal` backend by default. Bare-bones window managers (some i3 setups, dwm, etc.) may not — install one of: - `xdg-desktop-portal-gtk` — works under any X11 session and most Wayland compositors as a fallback. - `xdg-desktop-portal-wlr` — wlroots-native (sway, hyprland). - `xdg-desktop-portal-kde`, `xdg-desktop-portal-gnome` — desktop-specific. If `OS.screenshot` raises a DBus error like *"org.freedesktop.portal.Desktop not provided by any .service files"*, install one of the above and restart the session. ## Docker The published Eternego image ships an in-container desktop stack baked in: - `Xvfb` virtual display - `fluxbox` window manager - `x11vnc` + `noVNC` so you can peek at the persona's screen at `http://localhost:6080/vnc.html` - `xdg-desktop-portal` + `xdg-desktop-portal-gtk` + `dbus` for screenshot - `dbus-run-session` wraps the daemon so the portal can be reached over the session bus That covers screenshot end-to-end inside the container. **Input from the container is more involved**: `/dev/uinput` lives on the host kernel, and the container's user can only write to it if you pass it through: ```yaml # installation/docker/docker-compose.yml — add to the eternego service, then # rebuild with `docker compose up -d --build` (see Install → Docker): devices: - /dev/uinput ``` Adding `/dev/uinput` to the service's `devices:` makes the host's uinput device visible inside the container. Without it, mouse / keyboard verbs raise a permission error and only screenshot works. ## Headless hosts `desktop.available()` returns `False` if no display is reachable. On Linux that means neither `DISPLAY` nor `WAYLAND_DISPLAY` is set, or `/dev/uinput` isn't writable. On macOS / Windows it means pynput's controller can't be instantiated. The persona checks `available()` to know whether `screen` and `watch` will work; when it returns `False`, she politely tells the person she can't see or act on the screen rather than raising mid-cycle.