Aromba — Assistant
1. What it is
The assistant is the AI inside the Telegram bot. It lives on the server, knows it (what runs, what changed, what usually happens), answers questions, explains alerts, and proposes fixes. It never executes a change itself: it proposes, the Warden runs what the user taps. It answers on credits of Aromba’s own — a capped key the relay mints for each paired server (PLAN D108) — or on whatever key the user connects, which replaces ours.
2. Characteristics
| Target | |
|---|---|
| Weight | inside the daemon’s budget (section 3); adds no process, no runtime, no local model |
| Speed | simple answers under 8 s; text streams into the Telegram message as it arrives |
| Cost to the user | on a cheap open model: well under $1/month for typical use; heavy use on a strong model stays under ~$10 (est.) |
| Cost to us | zero — model calls go from the user’s server to the user’s provider |
| Knows | server card, notebook, health baselines, receipts, project structure, its own history |
| Can | read (filtered), diagnose, propose, write chores, propose enabling skills |
| Cannot | execute, read secrets or fenced paths, act on text it reads, change its own limits |
| Learns | on its own, from arithmetic, not from the model (section 8) |
3. Weight budget and what it forces
The whole daemon — Worker, Warden, chores, state — has a fixed budget. The assistant lives inside it.
| Budget | Comparison | |
|---|---|---|
| Disk, core | 30 MB | two Go binaries, chores, SQLite; user data (quarantine, history) counted and shown separately |
| RAM idle | 50 MB for both processes | about what sshd uses |
| CPU idle | under 1% of one core | collector reads /proc once a minute |
| RAM during a turn | spike up to 150 MB, back to idle after | context assembly and streaming |
| Skills | each declares its own disk/RAM; shown before install; counted against a separate, user-adjustable skills budget | e.g. voice (whisper) is +143 MB disk, +300 MB RAM while transcribing |
What the budget forces:
- No model inside the daemon, ever. The model is always outside: a provider over HTTPS, or Ollama the user installed themselves.
- No vector store, no embeddings. The notebook is a text file; retrieval is grep.
- No Node or Python runtime. Go and bash only.
- Logs are never loaded whole. They are streamed through filters; the model sees summaries and pages.
- Health history is a ring buffer in SQLite; a year is a few megabytes.
- Anything heavy that works only on already-exported data lives on the relay, opt-in (
06-architecture.md, split principle). - The dashboard’s Trust page shows the live footprint against the budget: “Aromba: 38 MB of 50 MB.”
4. Saving tokens — every lever
Order matters: the earlier the lever, the more it saves. Percentages are estimates (est.) of what each removes from a naive design.
| # | Lever | Saves | How |
|---|---|---|---|
| 1 | Router before the model | ~30% of turns | a script answers commands and frequent questions (“disk?”, “last receipt”, “is X running”, “what ran tonight”) from state. Zero tokens. |
| 2 | Summaries before raw | ~60–80% of tool tokens | every tool returns a summary first (counts, unique errors, time histogram, 20 samples). Raw lines only on request, in pages. |
| 3 | Stable blocks first, cached | ~60% of base input | system prompt, server card, tool schemas, notebook go first and don’t change for days; providers with prompt caching bill them at a fraction. The card carries what Aromba already worked out — the last scan’s findings, what the watchers said in the last day, and the chores it may offer, by name — so a question starts from what is known instead of spending steps rediscovering it, and a small model never invents a chore that does not exist (PLAN 3.11’s bench found both). |
| 4 | Short context | — | ✅ the last few exchanges, not the whole chat. What falls out becomes one line at the front — “Earlier in this chat I asked about: …” — so “and the other one?” still means something after seven turns. The line is written by the Worker, in a few words a question, never by a model: a summary that costs a call costs more than the turns it replaces. |
| 5 | Answer cache | ~10% of turns | same question, same state, 24 h → answered with what was said before, and the chat says so: an answer repeated without saying it is a caretaker pretending to have looked. “Same state” is strict — a scan, a chore run or an alert since makes it a different server, so on a busy one the cache almost never hits. |
| 6 | Per-turn ceiling that scales | bounds the worst case | ✅ three quarters of the model’s own context window, never under 40k and never over 150k: 40k on a 32k model, 98k on the everyday 131k one, 150k on a 200k one. depth: light / normal / deep in /settings halves it or doubles it, and the two bounds hold either way. It ends the looking, never the answer — the turn is told to answer with what it has, and may still offer the fix. It is not a budget and is never off; the budget is the person’s own and off until they set one. ✅ A scan that finds ten or more things running says so once, quietly, with a button that sets deep: on a server that size the ceiling is what the person feels first, and a knob nobody knows about is not a setting. |
| 7 | Ask before going deep | — | ✅ answered by levers 6 and §6 together rather than by a question in the middle of a turn: the ceiling ends the looking and the turn answers with what it has, and [Look deeper] under that answer is the person’s own way to spend more. A turn that stops to ask is a turn the person has to watch (PLAN D142). |
| 8 | Loop and repeat guard | — | same tool + same args twice → stop; max 6 tool steps. |
| 9 | Learning without the model | — | section 8: all learning is arithmetic; one model sentence a week. |
| 10 | Show the price | — | cost per answer in /status, optionally under each answer. People self-regulate when they see cents. |
Result (est.): a diagnosis that would cost 30–40k tokens naively costs 8–12k here. On a cheap open model that is a fraction of a cent.
5. Scenarios → tools → adaptive limits
Tools follow from what people actually ask. The five scenarios that cover most turns:
| Scenario | Example | Tools used | Typical tokens (est.) |
|---|---|---|---|
| Status question | “how’s the server?” | router, or status | 0–3k |
| Alert explained | “Take a look” on a restart loop | look (the unit’s or container’s log), health | 6–10k |
| Fix proposed | “start redis” | propose, then explain | +2k |
| Setup | “prepare for Next.js + Postgres” | inventory, write_chore, skill_propose | 12–20k |
| Custom job | “back up Postgres to Backblaze nightly” | write_chore | 10–15k |
Tools
Every call goes through the Warden’s policy; the model sees the schema, the engine decides.
| Tool | Color | First page | Executed by | Does |
|---|---|---|---|---|
status() | green | 30 lines | Worker | same as /status |
health(metric, range) | green | 60 points | Worker | aggregated series, never raw samples |
inventory() | green | summary + pages | Worker | software, ports, services, containers, cron |
receipts(days) | green | 100 lines | Worker | recent receipts |
notebook() | green | 100 lines | Worker | learned facts |
read_file(path, page) | green | 400-line pages | Warden (path policy, redaction) | allowed paths only |
look(command, args) | green | 40 lines, and how many were cut | Warden (allowlist) | one read-only command and its output: df, du, ls, ps, docker ps, docker logs, journalctl, systemctl status, ss, free, uptime, cat on allowed paths. It is logs and run_readonly in one tool, which is what shipped: all three ended in the same sentence — run a command the Warden allows and read what came back — and one schema, one refusal and one policy row are better than three (PLAN 3.3) |
plan(title, goal, steps) | green | one plan a turn, 8 steps | Worker → the chat | Writes a plan down when one move is not enough and shows it. It runs nothing: a chore step runs with the approval that chore always needs, a check step is one read-only command the Warden decides on, an ask step is a button. The owner moves it a step at a time, and /task is where it lives (PLAN D119) |
chores(), offer_chore(name) | green | the list; one offer a turn | Worker → the chat | chores lists what this server can be asked to do; offer_chore puts one to the owner with a button and runs nothing. The tap runs it the way /run does — dry-run pair, approval, receipt — whatever its colour (PLAN D109) |
propose(cmd, why, touches, undoable) | red | — | Worker stores, Warden runs on tap | approval message with the exact command and a plain-language line |
write_chore(file) | red | — | Worker → mine/ | written to the chore brief (PLAN D234); the header is checked, and the chore is shown whole with [Save it] [Not this]. Saved red and off; /run tries it dry first |
skill_propose(name, why) | red | — | user taps | proposes installing a skill from the catalog, showing its weight and permissions: /build’s second place to look, after this server’s chores (PLAN D223) |
undo(action_id) | red | — | Warden | restores the snapshot taken before a red action (7 days) |
explain(cmd) | green | 5 lines | Worker (model) | “what this will do” for a non-admin; under every red button |
web(url) | green | 400-line pages | Worker | documentation domains only; provided by the docs skill, off until enabled |
calendar(days) | green | days 1–14; anything else refused | Worker (the calendar, PLAN D107) | the merged timeline: Aromba’s plan, the user’s items with their titles, and connected calendars’ busy and free time and marked events by word and time — their titles only with the “Calendar titles” setting on (the calendar skill) |
calendar_add(item), calendar_done(id) | green | one item a turn | Worker | adds, moves or finishes an item in Aromba’s own calendar at once, with [Undo] under the message: it changes nothing on the server. Never a calendar of the user’s own (PLAN D107) |
The build turn (15-instruments.md, PLAN D210) is not the chat’s. A product on this machine hands over a job; the turn has its own short prompt, the product’s brief, the job and the library, and three tools of its own — use_instrument(slug, params), read_instrument(slug), write_instrument(slug, name, description, input_schema, params, page) — plus none_fits on the cheap step. None of them reads this server: status, look, health and the logs are not in it. The cheap model reads the job against the library’s eight nearest instruments and answers from one, or says none fits; only then does the job’s writer — the model its direction has at the tier the owner chose (PLAN D224) — write, once, and what it writes is checked in code and put in front of the owner. An exact repeat of an earlier ask is answered with no model at all. The person’s daily budget covers these turns as it covers questions.
The models a product’s pages are written on (PLAN D216, D224), by the direction a job names, as providers.toml ships them for OpenRouter, and the model /build works on, which is the programming direction’s (PLAN D231). They are open models only, each with its weights published, and the catalog refuses one that is not (PLAN D235): on the same work the closed ones cost five to a hundred times as much. Each page direction was chosen by writing the same pages on several models and reading every one at phone width; programming by /build’s find bench, the twenty programming cases of REPORTS/cases.md on a server with the core chores and the catalog, and by reading the chores each model wrote for the six that need one. /model <direction> moves a server between the three; the default is the middle one.
| Direction | Best | Default | Cheapest |
|---|---|---|---|
| health | z-ai/glm-5.3 | deepseek/deepseek-v4.1-flash | z-ai/glm-5.3-flash |
| hobby | z-ai/glm-5.3 | deepseek/deepseek-v4.1-flash | z-ai/glm-5.3-flash |
| finance | z-ai/glm-5.3 | deepseek/deepseek-v4.1-flash | z-ai/glm-5.3-flash |
| design | z-ai/glm-5.3 | deepseek/deepseek-v4.1-flash | z-ai/glm-5.3-flash |
| programming | moonshotai/kimi-k2.7-code | deepseek/deepseek-v4.1-flash | openai/gpt-oss-120b |
| incident | z-ai/glm-5.3 | z-ai/glm-5.3 | deepseek/deepseek-v4.1-flash |
| users | z-ai/glm-5.3 | z-ai/glm-5.3 | deepseek/deepseek-v4.1-flash |
incident answers a product’s report of an error one of its users met (PLAN D279), and the screen, when its words cannot settle it, is described by z-ai/glm-5.3-flash, which reads pictures. users is the pages a product’s own users order, sent without a tap by the owner’s rule (PLAN D277): a job under that rule that names no direction is written there, on the model a bench of three real user asks found strongest at about $0.16 a page (PLAN D278). A direction whose best and default are one model writes a page on its cheapest when that model could not write one that was kept.
A page of any other direction is written on deepseek/deepseek-v4.1-flash. Whether an approved page answers a job, is extended for it, or none fits is decided on deepseek/deepseek-v4.1-flash (PLAN D229, D235). Every page is written to a design brief and then reviewed and written again as a designer would (PLAN D228).
Adaptive limits
Limits are pages, not walls:
- First page is a summary sized for the question; the model asks for
page=2when it needs raw lines. - Page size scales with the model’s context (400 lines at 32k, 2,000 at 200k).
- The per-turn ceiling scales with context and with
depth. - A big server (many containers, large logs, detected at scan) gets
depth: deepsuggested, not forced. - Going past the ceiling always asks first, with the price in cents.
- The step cap counts looking.
offer_choreandplanread nothing, run nothing and cost nothing on the server, so they stay available on the last step — twice at most — and a turn that spent its looking can still put the fix in front of the owner. One offer or one plan a turn, and no more. - The last call gets the room the whole answer needs, not a tool call’s: a cheap model that thinks before it writes answers with nothing when it runs out mid-thought (PLAN 3.11).
- A move up carries the tools that read nothing —
offer_choreandplan— for one call, then asks for the answer with none: a turn that found the cause after moving up must not lose the button for having moved (PLAN D128). - A stronger model that answers in prose anyway is asked once more, on the cheap model, over the answer it is about to send: whether one of the card’s chores fixes what it describes. One call, over evidence already gathered, on the model that costs the least — and the button stops depending on which model happened to answer (PLAN D129).
- A move up asks for the least thinking the provider offers: the stronger model has the evidence already and owes six lines, and one left to think as long as it likes spends the whole budget on it and answers nothing.
- A turn has a clock as well as a step count, and half of it is for the looking. Past the halfway mark it stops asking for tools and asks for the answer, so a slow provider costs the turn its last few looks and not its answer. A move up to the stronger model is skipped when what is left of the turn is less than that model needs: the cheap answer is already in the chat, and replacing it with a deadline would be the worst of both.
6. Suggested replies and voice
- Suggested replies: ✅ chips under an assistant answer, built from the answer itself and not by a second model call — a model asked for follow-up questions costs a turn’s tokens for something the answer already implies. An answer about space invites “What can be cleaned?”; one about something that failed invites “Show me the log”; one that invites neither carries none. On by default, off in
/settings. The tap asks that question as if it had been typed. - Look deeper: ✅ the button that says out loud that a stronger model is there (
PLAND113). It is under every answer the stronger model did not already give, it asks the same question again, and it is not a suggested reply — turning the chips off leaves it. A question asked this way skips the answer cache: somebody who asks for a deeper look is not asking for the answer they just read. - It does its own looking. Before saying that something failed, the assistant reads what it said: a unit has a journal, a container has a log, a port has a process, and all three are on the Warden’s read-only list. “Check its logs to find the cause” is the one thing the owner could have done without asking, and it is not an answer (PLAN D129). Every line of an answer is a whole sentence about this server, not a bare phrase like “renew the certificate”.
- Voice: provided by the
voiceskill (whisper on the server, +143 MB disk,13-skills.md§5,PLAND144). Off until enabled; the bot offers it once, on the first voice message somebody sends, which is the moment it means anything. Nothing said into the server leaves it: the relay carries the bytes from Telegram and keeps none, the Worker decodes the Opus itself, and the words are made on the machine in a cage with no network — there is no account anywhere and no key. The transcript is shown as a quote before anything is done with it, so a word heard wrong is a word the person can see was heard wrong; then it is a question with no more standing than a typed one. Voice never triggers a red action by itself.
7. Models
The user connects an OpenRouter key, or runs Ollama on the server itself — and nothing else: Aromba runs on open models only (PLAN D235, D236). The Anthropic and OpenAI providers were taken out. On this bench’s work a closed model costs four to thirty times what the best open one does, and a person setting up should not reach for one and pay hundreds a month for what open models do for a few dollars. Wherever a key is set up — the chat’s /key, aromba key, and the message when a key stops working, which is how Aromba’s own credits running out reaches the person — they are told so in one sentence (provider.OpenModels). They pick the model; they pay the provider. Aromba’s requirements: tool calling and a 32k context; the assistant tests the key with one tiny call when set.
The audience is people who can open OpenRouter and pick an open-weights model that matches top closed models on this kind of work for a few cents per million tokens. For them: /model lists what their key can reach, with price per million next to each. For everyone else: the dashboard’s Models page shows three recommendations, balanced for price and quality, refreshed with each release, with the exact price and a one-tap “use this”.
There are no tiers and no AI cost that depends on the plan. A server that has no key of its own is given one at pairing, minted by the relay against our own account (PLAN D108). The ceiling is per person, not per machine (D125): each server has a key of its own so one can be revoked alone, and when an account’s servers together reach the month’s allowance all of its keys stop until the 1st, when they start again: the credits are ours, the calls still go from that server straight to the provider, and a key the user sets themselves replaces it. An account may be given an allowance of its own, on the relay (D222). /key says which of the two is answering and what is left of the month.
7.1 Two models, and moving between them by itself
Every provider names two: the everyday one that answers almost everything, and the one a turn moves up to (deep in providers.toml; assistant.deep overrides it, off turns it off). On OpenRouter they are openai/gpt-oss-120b for the everyday question and z-ai/glm-5.3 for the move up, chosen on the five servers in trouble the replays hold the chat to (PLAN D236). Nothing asks a model whether a model is needed — the decision is made from what the cheap turn already showed, so it costs no tokens and no time:
| The cheap turn | Why it means the cheap model is out of its depth |
|---|---|
| tool arguments that are not JSON | it cannot call the tool, it will not write the answer |
| the same tool with the same arguments twice | it is going in a circle |
| nothing said at all | there is nothing to send |
| every step used and still told to answer | it did not converge |
| more than 3 tools, or 12k tokens of evidence | more reading than a small model holds |
an answer that hedges after using a tool (not sure, unclear, might be) | it is guessing |
| the person asked, by word or by button | they asked |
A question that is about code — a path, a file extension, a traceback, “write a chore”, “why does X fail” — starts on the stronger model; trying the cheap one first would only cost a round trip.
A turn of a direction — /build — does neither (PLAN D231). It runs on the model the owner chose for that work, start to finish, and nothing moves it: the move up is the everyday model’s, and the direction’s own model is not a cheap one waiting for a stronger one. Each of its steps may write 8,000 tokens, because a chore goes out whole inside one tool call, and at the chat’s 600 the strongest model’s chores were cut off mid-line and could not be read. Its answer has no Look deeper; /model programming is where it changes.
The move is one call over the evidence already gathered, with the instruction not to start over: the tools do not run twice. The cheap answer is sent to the chat the moment the turn decides to move up, ending with its own line — Handing this to a stronger model: the cheap model was not sure. One moment. — and the stronger answer arrives as a second message that does not repeat the first. If the stronger model then fails, the chat is told that and the first answer stands. A question that started deep says so under its answer. Either way the receipt names which model answered and why.
8. What it learns on its own
The second week must be smarter than the first without the user teaching anything. Six sources, all free of tokens except the last, all written to the notebook as plain lines the user can read and delete.
8.1 Rhythms — from the health collector
- Deploy pattern: “deploys usually Tuesday ~15:00” → checks disk and pending migrations before Tuesday on its own.
- Known spikes: “CPU peaks daily at 03:10 — the backup chore” → stops mentioning it; alerts if the peak is missing.
- Memory shape: “memory rises for 3 days after deploy, then drops” → knows the norm, doesn’t panic.
- Real quiet hours: “traffic is zero 01:00–06:00” → proposes moving the quiet window there.
8.2 Baselines — what normal is for this server
- Every metric gets a “usually”: alerts fire on deviation (“disk growing 4× faster than usual”), not only on fixed thresholds.
- Restart counts: “api-worker restarts twice a day — normal; 17 — not.”
- An alert the user ignored three times stops arriving as an alert and moves into the receipt as one line. The user can bring it back in
/settings.
8.3 The user — from their buttons, never from their words
- Always taps “Show logs” before “Fix” → logs come with the proposal from then on.
- Never opens details → reports get shorter.
- Replies by voice → answers get shorter (they’re reading on a phone).
- Active 22:00–01:00 → non-urgent alerts wait for that window.
8.4 The project — from what runs, not from what was written
- ✅ Reads what is running: the images the containers were made from, by their plain name, the services that are not the system’s own, and the databases the scan already recognises → “this server runs nginx, site, node, postgres and redis, with Postgres behind it”.
- This is nearer the truth than a compose file — a compose file says what somebody meant — and it asks for no permission Aromba did not already have. Reading a project’s own files would mean widening what
catmay read to paths under/srv, and.env.exampleis a path the redactor already refuses on sight (PLAND137). - Feeds the card and the diagnoses: a question about a server that runs Postgres is answered differently from one about a server that does not.
- Never learns from file contents. Not “what’s in the database”, only “there is a database”.
- Still to come: the edges — “api depends on redis” — which are in a compose file and nowhere else, and so wait for a reader that may open one.
8.5 History — its own past
- “Last time this was fixed by restarting redis” → proposes that first.
- “This chore failed 3 times on this server” → offers to disable or fix before the fourth.
- “Postmortem from March 3” is attached when the same failure signature returns.
8.6 Gaps — quiet suggestions, one per week at most
✅ The only learning that uses the model, and the only place in Aromba where a model is asked to suggest rather than to answer. Finding the gap is arithmetic like everything else above; the model turns the facts into one sentence of at most twenty words, and the chore that would close it is offered under it with its own button.
One a week at most, and each gap once — not again for ninety days, because a person who did not set up a backup in three months heard the suggestion and decided. A server with no AI key says nothing at all, and nothing is written down when it does, so the week is not spent.
Built:
- “You run Postgres and have no backup” — a database running and no trace of any backup: nothing in cron, no timer, no restic, rclone or borg, and no backup chore on.
- “9.0 GB of regenerable rubbish and nothing cleaning it” — over five gigabytes, with every cleaning chore off.
Still to come, each with the watcher that finds it: the domain that expires in 20 days (a WHOIS watcher), and the dead projects holding 12 GB (a growth watcher that knows a project from a folder).
Rules that keep it from being creepy
- Every learned line is in
/var/lib/aromba/notebook.md, plain text, visible in Telegram (/notebook) and on the dashboard. One tap deletes a line; a deleted line is not re-learned for 90 days. - Learning changes what and when it says, never what it may do. Permissions are unchanged by anything learned.
- “Not right” on any learned claim removes it immediately and logs why it was believed.
- Nothing learned leaves the server. The notebook is never synced to the relay.
- Learned facts have a confidence: “seen 3 times” vs “seen 20 times”; the assistant says “seems to” under 5 observations.
9. Failure modes
| What breaks | What the user sees | What keeps working |
|---|---|---|
| Provider down or slow | “AI is unreachable right now; I’ll retry in a minute.” Then “Still down; chores are unaffected.” | everything except the assistant |
| Key invalid or out of credit | “Your AI key stopped working (provider says: …). aromba key to replace.” | everything else |
| Per-turn ceiling hit | “Ran out of room on this one. Here’s what I found so far.” | — |
| Model proposes a denied command | “The assistant proposed a blocked command; not run.” + ladder counter | — |
| Tool loop | stops at 6 steps or on repetition; reports | — |