# 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
" }` |
| **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.