Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Alate — the live agent

An alate is the winged form of an aphid. It is the form that leaves the plant and lives away from it.

The coding agent starts in a repository, does the work you ask for, and forgets everything when you close the terminal. An alate is different in five ways:

  • It has a home directory that it owns. The home is also its workspace.
  • It has a memory. What it learns in one session, it knows in the next.
  • It has a heartbeat. It wakes on a clock and looks at what it has.
  • It has a crontab. It can schedule a prompt to run at a time, in a conversation of its own.
  • It has a gateway. You attach a terminal to it, and you detach again. The agent continues either way.

The agent itself is the same agent. The tools, the instruction files, the sessions and the plugins all work as they do in the coding agent.

$ aphid alate run --name work        # one terminal
$ aphid alate attach --name work     # another, whenever you want it
$ aphid alate gui --name work        # or a window, on the desktop

CLI gives the commands that start an alate and the terminal that attaches to one, and Window the one that puts it on your desktop. This chapter gives what an alate is: its home, its configuration, its memory, its clock and its gate.

Sessions

An alate has more than one conversation at a time. Each is a session: one context, one transcript, one file in ~/.aphid/sessions. Sessions run at the same time, so a job that starts at nine does not wait for you to stop typing.

Three things make a session, and each ends differently:

KindMade whenEnds when
residentThe alate starts.Never. It stops with the alate.
attachedA client attaches.That client detaches.
cronA job comes due.Its run ends.

The resident session is where the heartbeat wakes. It keeps its context all day, which is what makes an alate resident and not new every quarter of an hour. Give it the work that must continue after you close the terminal.

An attached session is yours, and it ends with your terminal. A run still in progress is stopped. This is deliberate: it keeps a day of attaching and detaching from filling the alate with conversations nobody returns to.

A terminal is not the only client that can attach. A client can say what it is when it attaches, and the session list then shows that in place of attached. A chat on the Telegram bot is listed as telegram: <chat id>, and a channel in a colony as colony: #general, so a list of conversations tells you where each one is being had.

A cron session starts empty each time. It cannot see what you are saying, and you cannot see it in your own window — but the memory is shared, so a job can write a fact that you recall an hour later, and it can say one thing out loud in the conversation that scheduled it. Refer to Cron.

A chat on the Telegram bot is attached from the moment the alate starts, and not only once somebody writes to it. That is what gives a job at three in the morning somewhere to report to; the cost is a conversation in /sessions for each allowed chat, whether or not anybody is using it.

What sessions share is everything that is the alate and not a conversation: the memory, the crontab, the plugins, the model and the permission gate.

A session that ended still has its transcript. Ending a session loses the context, never the record. /session <id> opens any of them, including the ones that finished last week.

A client that connects opens a session, so a window that reconnects after the daemon was restarted is in a new conversation and says so.

The home directory

Each instance has one directory:

~/.aphid/alate/<name>/
  alate.json      the configuration
  AGENTS.md       the instructions this alate always carries
  HEARTBEAT.md    what to say when it wakes itself
  memory/         the facts, as markdown
  cron.json       the jobs it has scheduled
  state.json      when the heartbeat last woke
  gateway.sock    the socket that clients attach to
  alate.log       each frame the gateway sent
  .aphid/
    skills/       skills for this alate
    plugins/      Rhai plugins for this alate
    sessions/     the transcripts

The directory is made when you first run the instance.

The home is also the workspace of the agent. Two results follow:

  • read, write and edit can touch only this directory. To let the agent work somewhere different, set workspace in alate.json.
  • AGENTS.md, .aphid/skills, .agents/skills and .aphid/plugins are found in the usual way, because they are in the usual place.

The bash tool is not limited to the home. This is true of the coding agent also.

Sandbox

Alate runs each bash command and each plugin exec command in a sandbox. The sandbox can write only to the workspace. It can read the system files that the command needs to run, but it cannot see your other home directories. It also has its own process list, temporary files and home directory.

The sandbox uses Bubblewrap on Linux. Bubblewrap must be installed and the system must allow user namespaces. Alate refuses to start when it cannot make the sandbox. This is deliberate: a warning would make a resident agent run with more access than its configuration says.

The policy is outside the agent workspace, so the agent cannot give itself more access:

~/.aphid/alate/.sandbox/<name>.json

An absent file gives the strict default. This example allows a toolchain to be read and one directory to be changed:

{
  "version": 1,
  "enabled": true,
  "network": "host",
  "read_only": ["/opt/toolchain"],
  "read_write": ["/var/tmp/alate-output"],
  "host_environment": ["DEPLOY_TOKEN"]
}

Paths must be absolute and exist when Alate starts. network is host by default. Set it to none to remove the network from commands and plugin HTTP calls. It does not remove the model or gateway network of Alate itself.

On a system without Bubblewrap, or on macOS, set enabled to false in this file to run without a sandbox. This is an explicit opt-out.

alate.json can set variables for sandboxed commands:

{
  "environment": {
    "MODE": "production",
    "TOKEN": "${DEPLOY_TOKEN}"
  }
}

A complete ${NAME} value copies the host variable named NAME. The name must be in host_environment in the sandbox policy. $${NAME} writes the literal text ${NAME}. Values do not expand inside larger strings and do not expand again. A missing allowed host variable stops Alate at start.

A name can hold letters, digits, dot, dash and underscore. It cannot start with a dot, and it cannot hold a path separator. These rules keep --name inside the root directory.

alate.json

Each field has a default. An absent file, and an empty file, give the defaults.

{
  "version": 1,
  "model": null,
  "thinking": "medium",
  "workspace": null,
  "permissions": "ask",
  "heartbeat": { "every": "15m", "prompt": null },
  "memory": { "recall": 5 },
  "gateway": { "socket": null, "attachment_limit": 20971520, "telegram": null, "colony": null },
  "environment": {}
}
FieldEffect
modelThe model, by the name aphid model list shows. The first configured model when absent. An alate with no configured model fails and says to run aphid models add.
thinkingoff, minimal, low, medium, high, xhigh or max.
workspaceWhere the agent works. The home when absent.
permissionsask, allow or deny. See Permissions.
heartbeat.everyThe time between wakes: 30s, 15m, 2h, 1d. Use off for none.
heartbeat.promptWhat to say on a wake. See The heartbeat.
memory.recallThe quantity of facts offered for each prompt. Use 0 for none.
gateway.socketThe socket file. gateway.sock in the home when absent.
gateway.attachment_limitThe largest file, in bytes, an attachment-capable gateway can receive. It is 20 MiB when absent. Set 0 to turn attachments off.
gateway.telegramA Telegram bot on the gateway. No bot when absent. See Telegram.
gateway.colonyA colony on the gateway. No colony when absent. See Colony.
environmentLiteral variables for sandboxed commands. ${NAME} copies an allowed host variable. See Sandbox.

A file with a higher version than this build understands is refused by name. This prevents a new file from being read as an old one.

The memory

The memory is a set of facts. A fact is one short sentence. Each fact belongs to a path, such as /projects/aphid or /people/thiago.

The facts are markdown files in the home. The path /projects/aphid is the file memory/projects/aphid.md:

# /projects/aphid

- 2026-08-11 — The plugin API stays as small as it can be.
- 2026-08-11 — Docs are written in ASD-STE100.

You can read these files with cat, search them with grep, and change them with an editor. The agent can also read and change them with its own file tools, because they are in its workspace. A memory that only the agent can open is a memory that nobody can check.

The two tools

ToolEffect
rememberWrite one fact under one path. A path is made the first time it is used.
recallSearch the memory. With no query, it gives the newest facts.

Recall that you do not ask for

Before each prompt, the alate searches its memory with the words of the prompt. It puts the best memory.recall facts in front of the model as a system note. The facts are never put in the message of the person who spoke. The model can always see which words came from the memory and which came from you.

Recall gives more weight to a word that is rare in the memory than to a word that is common in it. Facts that answer equally well come back newest first.

The paths, but not the facts, are in the system prompt. The agent sees which subjects exist, and calls recall for what is in them.

Size

There is no index. The memory reads all of its files for each search. For the hundreds of facts that one agent writes, this takes a fraction of a millisecond. A memory of tens of thousands of facts needs a database, and this is not one.

The heartbeat

The heartbeat is a pulse at a fixed interval. heartbeat.every sets it: 15m, 2h, 30s, or off for none. The first wake comes one interval after the alate starts.

It wakes in the resident session, so the alate comes back to a conversation that remembers this morning. A wake does not happen while that session is already running, and missed wakes do not collect.

What the alate hears is, in order:

  1. heartbeat.prompt from alate.json;
  2. HEARTBEAT.md in the home;
  3. a standard line, which tells it to look at its memory and either act or stop.

Every attached terminal sees the wake, whichever conversation it is looking at.

Use the heartbeat for “look around and see”. Use cron for anything that must happen at a particular time.

Cron

The alate schedules its own work with the cron tool. Each job has a name, a schedule and a prompt.

ArgumentEffect
nameWhich job. A name that exists is replaced.
scheduleFive fields, in local time. Use off to remove the job.
promptWhat to do.

A job runs in a session of its own, which starts empty. The prompt must therefore hold everything the job needs: the session that runs it does not remember the conversation that scheduled it.

The jobs are in cron.json in the home. You can edit that file yourself.

{
  "version": 1,
  "entries": [
    {
      "name": "morning-review",
      "schedule": "0 9 * * *",
      "prompt": "Read yesterday's notes and tell me what is still open.",
      "origin": {
        "session": "20260810T201400-0007",
        "label": "telegram: 42"
      },
      "since": "2026-08-10T20:14:00-03:00",
      "last": "2026-08-11T09:00:00-03:00"
    }
  ]
}

Answering back

Nobody is watching a job’s own session, so what it says there reaches its transcript and no person. To reach one it has a send_message tool, which says one thing in the conversation the job was scheduled in — a Telegram chat, a colony channel, a terminal, the resident conversation. That conversation is origin on the job, written when the job was written, and the tool takes only the words: a job cannot choose somewhere else to write.

The tool is not gated by permissions. The destination is not the agent’s to pick, and a question asked at three in the morning is a question nobody answers.

A conversation is found again by its id while it is still open, and by its name after that — telegram: 42 comes back under that name when the chat reconnects, and so does resident when the alate is restarted. A terminal that said nothing when it attached is listed as attached, and several of them carry that one word: a job scheduled from such a terminal is answered while the terminal is there, and once it closes there is nothing to tell those apart, so the job is told there is nowhere to say it. Attach with a name — aphid alate attach and the Telegram bot both do — to be reachable tomorrow.

A message that has nowhere to go is reported to the job as a failed tool call, not swallowed. The job can then write what it found to the memory instead.

The schedule

Five fields, as in Vixie cron: minute, hour, day of month, month, day of week. Seconds are not accepted; a pattern with six fields is refused, and the message says so.

0 9 * * *          every day at 09:00
*/15 * * * *       every 15 minutes
0 9 * * MON-FRI    at 09:00 on the days of work
0 3 1 * *          at 03:00 on the first day of each month

The times are local. 0 9 * * * is nine in the morning where the machine is, not nine UTC.

A new job waits for the first time its schedule names after you write it. since records that moment. A job written at 20:00 for 0 9 * * * runs at 09:00 the next morning, and not at once. Writing over a job that exists starts its clock again in the same way.

A job that goes past while the alate is stopped runs one time when the alate comes back. A daily job and a week of stopped time make one run, not seven.

The names of the jobs, their schedules and their prompts are in the system prompt, so the alate knows what it already told itself to do.

Permissions

permissions in alate.json controls the bash, write and edit tools.

ValueEffect
askAsk each attached client. The first answer decides.
allowPermit each call.
denyRefuse each call.

With ask and no terminal attached, there is nobody to ask, and the call is refused. An unattended agent that permitted instead could agree with itself all night.

A question waits five minutes for an answer. After that it is refused.

Plugins and skills

Rhai plugins in <home>/.aphid/plugins load when the alate starts. They are not gated by a trust question: there is no terminal to ask at, and the home is a directory that you made for this agent.

A plugin that calls prompt puts words to the agent in the same queue that a terminal uses. A plugin listening for code/tick runs four times each second. See Plugins.

Skills in <home>/.aphid/skills and <home>/.agents/skills work as they do in the coding agent. See Skills.

Logs

There are two, and they are not the same thing.

alate.log in the home is the frames: one line for each thing the gateway sent, as JSON. Read it with jq. Refer to The log.

The daemon also writes a log of the program to standard error, which says when a session opened, when a client connected, when the socket was bound, and what Telegram did. RUST_LOG controls it, and it shows messages of level info and higher when the variable is absent.

$ RUST_LOG=debug aphid alate run --name work
$ RUST_LOG=aphid_alate::telegram=debug aphid alate run --name work
$ aphid alate run --name work 2> ~/.aphid/alate/work/daemon.log

The terminal that runs the alate is the terminal that gets this. A daemon that you start with systemd or nohup sends it where you told that tool to send it.

Files and environment variables

PathContent
~/.aphid/alate/<name>/One instance. $APHID_HOME moves the parent of this.
~/.aphid/models.jsonThe model catalogue, shared with the other front ends.
~/.aphid/gui.sockWhere a running window is told to show itself. One for the machine, because there is one window.
~/.aphid/gui.jsonWhat that window remembers: its mode, its familiar, and the alate it was last pointed at.
VariableEffect
APHID_HOMEMove ~/.aphid. The alates move with it.
DEEPSEEK_API_KEYThe key for the standard models. A model in the catalogue can name a different variable.
TELEGRAM_BOT_TOKENThe token of the Telegram bot. gateway.telegram.token_env can name a different variable.
APHID_COLONY_KEYThe key this agent speaks with in a colony. gateway.colony.key_env can name a different variable.
RUST_LOGWhich messages the daemon writes to standard error. info when absent.