Why I built Eon

Over the past few months I’ve been trying to understand the Cloud not just as a set of services to learn, but as a way of designing and running systems. That’s why I built Eon, my personal AI assistant.

Eon runs entirely locally, using open-weight models served by Ollama. It was born on a Mac mini M4 and since October 30, 2026 it lives on Little Hub, my home lab. It has the personality of a “digital butler”, a bit like Jarvis, and a long-term memory that updates itself. I use it in two ways: from a web dashboard and from a Telegram bot. On its own, at night, it studies and prepares articles for this site.

No data leaves my home network, with two deliberate exceptions: Telegram messages and publishing to GitHub.

Eon dashboard: an empty chat asking "Come posso aiutarti?" (How can I help?), shortcuts for system status, jobs and study files, and Ollama status with CPU, RAM and disk usage at the bottom left
The Eon dashboard: chat in the center, Ollama status and Mac resources at the bottom left

I’m not trying to turn Eon into a product. It’s a personal learning project, built while I’m still studying computer science at a technical high school (ITIS). What I care about is understanding what happens when a language model becomes part of a real system, with scheduled jobs, persistent state, tools that touch files and apps, and actions with external consequences.

Eon by the numbers

  • Python 3 backend with FastAPI, Uvicorn, async httpx and Pydantic v2.
  • Two Qwen3 models: qwen3:4b-instruct for chat and qwen3:8b for study and publishing, plus nomic-embed-text for semantic search. On the Mac, chat used qwen3:8b: the reason for the change is further down.
  • About 15,000 lines of Python, about 8,600 lines of tests (733 tests, all passing) and about 2,300 lines of frontend.
  • A frontend in plain HTML, CSS and JavaScript, no framework. Libraries, Three.js included, are bundled with the project, so the dashboard also works offline.
  • Persistence in SQLite (eon.db) with versioned migrations, plus a few JSON files written atomically: I write to a .tmp file and then rename it, so a crash never leaves a half-written file.

Architecture: two processes

Eon is split into two independent processes that share state through eon.db:

  1. Web server (run.sh): dashboard, chat API, WebSockets and the study worker.
  2. Scheduler (run_scheduler.sh): Telegram bot, nightly study, morning briefing and health checks. On the Mac it was registered as a launchd service (KeepAlive and RunAtLoad), so it restarted on its own if it exited and at boot; on Little Hub Eon runs in a Docker container instead.
       Web dashboard                    Telegram
  (chat · Mind Flow · 3D sphere)           │
            │                              │
   API · /ws/mindflow · /ws/telemetry      │
            │                              │
  ┌─────────▼──────────┐        ┌──────────▼──────────┐
  │    Web server      │        │     Scheduler       │
  │  FastAPI + worker  │        │ bot · jobs · launchd│
  └─────────┬──────────┘        └──────────┬──────────┘
            │                              │
            ├──────────► eon.db ◄──────────┤
            │      (SQLite, shared state)  │
            │                              │
  ┌─────────▼──────────────────────────────▼──────────┐
  │   Router → Tool-using agent → Ollama gateway      │
  │      Memory · Semantic index · Secret filter      │
  └─────────┬───────────────────────────────┬─────────┘
            │                               │
     ┌──────▼──────┐                 ┌──────▼──────┐
     │   Ollama    │                 │  Publisher  │
     │ qwen3 · emb │                 │ GitHub API  │
     └─────────────┘                 └─────────────┘

Splitting the processes has a cost: the scheduler can’t just put something in the server’s memory and expect the dashboard to see it. Anything that needs to be shared goes through the database. It looks like a minor detail, but it’s exactly the kind of problem I wanted to learn to deal with.

The Ollama gateway

Every model call goes through a single module, ollama_gateway.py, which is now about 1,000 lines long.

  • Each use of the model has a role with its own configurable model: chat, router, study and publisher.
  • The gateway can use multiple Ollama hosts, periodically checks that they respond, and has a fallback host and model. When the fallback kicks in, the user sees a warning.
  • Study and publishing never use the fallback: I’d rather postpone an article than have it written by a smaller model.
  • Errors are classified (unreachable host, missing model, timeout) and every call records its speed in tokens per second.

The server isn’t tied to Ollama: switching to llama.cpp server only means changing the host.

Router and tool-using agent

Every message goes through the router (router.py) first, which decides how to handle it:

  • TOOL_CALL: a direct command in the form /tool:name;
  • DIRECT: normal chat, with a streamed response;
  • DELEGATE: a long-running task. For now the router recognizes it, but then handles it as normal chat.

A needs_tools function decides whether a message needs the agent or whether a direct streamed answer is enough, which is much faster.

The agent (agent.py) uses function calling in a bounded loop: at most 6 steps, at most 4 calls per step, and an overall timeout. The available tools are grouped like this:

  • System: Mac status, scheduled jobs, study notes.
  • Mac, via AppleScript: running apps, opening or quitting an app, volume, notifications, opening URLs, controlling Music and Spotify, text-to-speech with say, turning off the display. Finder, Terminal and Ollama are protected and can’t be quit. On Linux these tools aren’t loaded at all.
  • Files in the home folder: list, search, read, write, edit, move, send to trash, and send me a file. Every change creates a backup that’s kept for 7 days, and sensitive folders are excluded.
  • Blog articles: list, read, edit, move and delete articles, in both English and Italian.
  • Memory: remember, recall or forget a fact, and search the notes.

Safety before autonomy

Giving tools to an 8-billion-parameter model means accepting that, sooner or later, it will misunderstand a request. That’s why every tool has a risk level:

LevelExamplesWhat happens
readreading a file, Mac statusruns immediately
writeediting a file or an articleasks for confirmation, showing a preview or a diff
destructivemoving to trash, deleting an articleasks me to type a confirmation phrase, valid for 5 minutes at most

Then there’s prompt injection. When Eon reads a file, a web page or an article, that content might contain instructions written specifically to trick it. Everything coming from those sources is wrapped between the markers <<DATI_NON_ATTENDIBILI and <<FINE_DATI>> (Italian for “untrusted data” and “end of data”), and the model has a strict rule: never perform actions requested by that content.

This is the lesson I take away from the whole project: an LLM’s output, and everything an LLM reads, is untrusted input. It has to be validated and contained like any other data coming from outside.

Memory

Memory is the part that changed the way I use Eon the most.

  • Profile and facts: Eon keeps facts about me, grouped by type (habits, preferences, projects, people, goals). At most 40 of them, or 2,500 characters, go into the prompt, and the ones I told it myself take priority over the ones it inferred.
  • Learning while idle (learner.py): when I’m not typing, Eon rereads recent exchanges from the site and from Telegram and extracts stable facts from them. I can review and correct them from a page on the dashboard.
  • Conversation compression (summarizer.py): past 24 messages or 14,000 characters, older messages are summarized into at most 12 bullet points, and only the last 8 are kept in full.
  • Semantic index (semantic_index.py): Markdown notes are split into chunks by heading, and the vectors are stored as BLOBs in SQLite. When something changes, Eon only reindexes the modified files, detecting them by modification time and hash.
  • Multiple sessions: “New chat” archives the current session instead of deleting it.

This is the “Cosa so di te” (“What I know about you”) page: at the top, the profile Eon has built; below, the individual facts grouped by type, each one showing when and how it was learned.

Eon's "Cosa so di te" (What I know about you) page with a summary profile and learned facts, grouped into projects and people
Eon's memory is visible and correctable: each fact says whether it was learned on its own, and when

The site and Telegram share the same persona and the same memory: what I tell Eon on one side, I find on the other.

What Eon refuses to remember

Before saving a memory or a summary, secret_filter.py looks for sensitive data and redacts it:

  • private keys and API keys;
  • credentials;
  • credit card numbers, checked with the Luhn algorithm;
  • IBANs, with a validity check;
  • Italian tax codes (codice fiscale);
  • the values of secrets found in environment variables;
  • high-entropy strings, which are often tokens.

If I explicitly ask it to remember one of these, Eon refuses and explains why.

The Telegram bot and nightly jobs

The scheduler (eon_scheduler.py, about 2,100 lines) uses python-telegram-bot and APScheduler. The bot only answers my chat and accepts these commands: /start, /chatid, /status, /briefing, /study, /jobs, /publish, /reset, /memoria and /dimentica (the last two are Italian for “memory” and “forget”). Any other message becomes a normal conversation.

There are three scheduled jobs:

  • Nightly Deep Study: every night Eon studies a different area, in rotation (cloud and AWS, system design, containers, infrastructure as code, AI and ML infrastructure, and more). The resulting note has to pass a quality check: required sections, commented code, minimum length. If it fails, the study is postponed and retried.
  • Morning Briefing: the summary I get in the morning.
  • Health Check: a periodic check of the system’s state.

Publishing to mycloudrise

blog_publisher.py and article_manager.py close the loop between the nightly study and this site.

The publisher takes the latest study note and asks the model to rewrite it as an article, in the role of mycloudrise.com’s “principal cloud architect”. The article must be at least 1,200 characters long, have valid YAML front matter and at most 6 tags. It’s then published through the GitHub API into the repository’s src/content/journal or src/content/projects folders, either as a direct commit or by opening a Pull Request.

The category is picked by a heuristic, or I choose it myself. A few safeguards sit around it:

  • duplicate blocking, so the same topic isn’t published twice;
  • a preview to approve on Telegram: nothing reaches the repository without a tap from me;
  • locks that prevent two publications from running at the same time.

Automating the preparation of an action doesn’t mean automating its execution too. Publishing to a public repository has external consequences, so there’s always a human confirmation between Eon and the final commit.

Observability: seeing what happens inside

One of the things I wanted most was to see Eon while it works, without reading logs.

  • Mind Flow (/ws/mindflow): each component publishes its state (idle, active, done, error) along the Input → Memory → Ollama Core → Router → Worker pipeline, and the dashboard animates it in real time.
  • Telemetry (/ws/telemetry, once per second): CPU, RAM, disk and processes.
  • Topology (topology.py): a live graph of hardware, loaded models, services, jobs, notes and articles. It powers the 3D sphere at the center of the dashboard, built with Three.js and a bloom effect (UnrealBloomPass): the nodes aren’t decorative, they map to real entities in the system.
  • Event log in JSONL with file rotation: Telegram messages, Ollama calls, tools, jobs and publications.

Portability and the phone with a broken screen

Eon isn’t meant to live only on the Mac.

  • deploy.sh syncs the project to a second device over rsync and SSH.
  • phone/setup_termux.sh installs Eon on Android through Termux and picks the model based on the phone’s RAM: qwen3.5:4b or llama3.2:3b.

The phone I’m testing it on is a CMF Phone 1 with 8 GB of RAM and a completely broken screen: it shows nothing, but the device still works. Developer mode was already enabled, so I can control it from the Mac with scrcpy. The problem was unlocking it: without a display I couldn’t enter the PIN. I solved it by plugging in a keyboard through a powered USB-C hub. With a passive hub, the phone went into protection mode and couldn’t handle the connection.

When you work with real hardware, not everything gets solved by writing code.

What an 8B model taught me

An 8-billion-parameter model is surprisingly capable, but it has very concrete limits. A recent example: Eon kept confusing its own creation date with my birthday. In the prompt, the two dates, October 25 and December 25, were written right next to each other, and the model ended up mixing them.

The fix wasn’t adding yet another rule. I separated Eon’s identity from mine in the prompt, wrote the dates out in full in Italian, and replaced some tone rules with examples. With small models, showing works better than telling.

Moving to Little Hub

On October 30, 2026 Eon left the Mac mini. It now runs on Little Hub, my home lab: an old laptop with a Ryzen 7 5700U, 24 GB of RAM and Ubuntu, which also hosts HNCC and HOMEVERSE. The Mac mini is back to being just the machine I develop on.

Moving a system that “works on my computer” to another machine was a good exercise, because it surfaced everything I had taken for granted.

  • Ollama runs natively, as a systemd service, with the models in the server’s storage. Eon runs in a container and listens only on 127.0.0.1: from the Mac I reach it through an SSH tunnel or Tailscale, never by exposing it to the Internet.
  • Smaller models where they make sense. Without a dedicated GPU, an 8B model was too slow for chat. Chat now uses qwen3:4b-instruct, while study and publishing stay on qwen3:8b, the only one that called tools reliably in my benchmarks. A practical lesson: never use the plain qwen3:4b tag, because it’s the “thinking” variant and there’s no way to turn the reasoning off.
  • The KV cache matters. I tried compressing the model cache to q8_0 to save memory: on this CPU it was 2.8 times slower than f16. I went back to f16.
  • The Mac tools switch themselves off. They only start when the operating system is macOS, so on Linux Eon simply doesn’t offer them to the model.

Opening up to the network, but not too much

On the Mac, Eon was used almost only from localhost. On a server that other services need to reach, the rules had to be explicit:

  • EON_API_TOKEN: chat, memory, publishing and confirmations require a token (Authorization: Bearer or X-Eon-Token header) when the request comes from the network. From loopback nothing changes. If no token is set, Eon says so in the logs.
  • Deleting chats or memory and downloading files stay loopback-only, unless explicitly allowed.
  • EON_BIND_HOSTS: a single process listens on several addresses, 127.0.0.1 for me and the docker0 interface for containers. One lifespan means one worker and one state; if one listener stops, they all stop, so there’s never a half-running process.
  • The context field: a client can attach data to a single request. That’s how HOMEVERSE asks Eon to analyze the state of the house: the summary reaches the model only for that question and never ends up in the history, the memory or the system prompt. system messages sent by clients are dropped.

What’s still in development

I don’t want to present as finished what isn’t:

  • Background Terraform tasks: there’s a spec and a plan, but the code isn’t written yet. The workspace/iac/ folder is empty and Terraform isn’t even installed.
  • DELEGATE in the router: the router recognizes long-running tasks, but doesn’t hand them off to the worker yet.
  • Event queue (event_queue.py): a SQLite-backed queue with leases, retries with backoff, approvals, deduplication, risk levels and 14-day retention. It’s ready and tested, but no module uses it yet. It will be the foundation for DELEGATE and the Terraform tasks.
  • Voice mode with local transcription: today Eon can speak with say, but it can’t listen yet.
  • The sphere on the site: someday I’d like anyone who opens mycloudrise.com to see Eon’s topology in real time. First, though, it has to be a reliable window into the system, not an animation.

More generally, I’m trying not to confuse “it works on my computer” with “it’s ready for other people to use”. Those are very different goals, and Eon is openly the first one.

Closing

Eon started as a way to have a local AI assistant, but it has mostly become an exercise in systems design.

What I’m learning isn’t just how to connect an LLM to a backend. It’s understanding what happens when a probabilistic component becomes part of a system that has to keep state, run automated work, communicate across processes, observe its own resources and, in some cases, touch files, apps and public data. For now the answer is always the same: risk levels, confirmations, backups, filters and tests. Lots of tests.