Home → Automation
Background work & automation
Work that outlives the turn that started it — sub-agents, background jobs, dev servers, schedules, watchers — in one place, with limits the platform enforces and outcomes that do not go missing.
Five registries became one
Sub-agents lived in one map, background agents in another, cron firings in a store, backgrounded shell commands in a pid table, Mini App servers in a fifth. Each had its own shape and its own idea of what "running" meant.
That is survivable until something has to answer "what is running right
now?" — and then it is impossible, because there is nowhere to ask. So
there is one record, one id space, and one append-only log at
~/.aico/work.jsonl.
bg:84bf0996 [running] agent refactor the auth module
ran 4m · 22 steps · $0.31 · now: Edit
proc:41820 [running] process npm run dev
ran 12m · pid 41820
cron:9c2f… [failed] schedule nightly dependency scan
3 vulnerabilities, 1 high
A restart no longer loses work silently
The log is replayed at startup and anything it says was running is settled. A process whose pid is still alive keeps running — a detached dev server legitimately outlives the session that started it. Everything else is marked lost, with a reason.
Before this, a crash mid-delegation left no trace at all: "it finished" and "it never came back" looked identical from the next session.
Outcomes wait to be read
Finished work stays on the list until it is acknowledged. Reading does not clear it — because losing a failure to whichever turn happened to glance at it is how a background job becomes a mystery an hour later. A job that failed at 3am is still there in the morning.
Limits the platform enforces
Rather than remembering to check on something, put a limit on it once:
Give that background agent a $2 ceiling and stop it if it goes
ten minutes without doing anything.
| Limit | What it catches |
|---|---|
deadlineMs | Wall clock — work that has run too long |
maxCostUsd | Spend, checked the moment it changes rather than on a sweep |
maxSteps | A loop that is progressing but going nowhere |
idleMs | No activity at all — not the same as slow |
Idle is deliberately separate from a deadline. An agent that has worked hard for an hour and one that has made no call in ten minutes are different failures, and a single timeout kills the wrong one. It applies only to work that reports progress — a detached server has no heartbeat to give, and a warning that is always on is a warning nobody reads.
The action is report, stop or kill.
There is no pause: an LLM turn cannot be suspended, because the
provider stream is a single open request — and a control that silently
cancels would be worse than not offering one.
Waiting without burning turns
Asking an agent to "wait for the build" means it runs a command, sleeps, and runs it again — a full turn and a full prompt per check. And the common failure is not the cost: it is the agent that checks twice, decides it is probably fine, and moves on.
❯ start the build and tell me when it's done
Watching dist/bundle.js — you will be woken when it appears.
(turn ends; nothing is being polled)
→ dist/bundle.js appeared
Watchers cover files, processes, HTTP endpoints, commands, log patterns and other work. One turn to register, one to be woken by. The wake arrives at the next step boundary, so nothing the turn has already learned is discarded.
Scheduled jobs report what they did
A firing is real work, not a receipt: it stays open as long as its run does, adopts the agent it started, and closes with that agent's verdict. So done, failed, stopped by you and stopped by a limit are four visible states rather than one, and the schedule carries what the run cost.
Scheduled runs get full tool access by default. Nobody can
approve anything at 3am, so the alternatives are "act" or "silently do
nothing" — and a job that refuses itself every night is worse than one that
acts, because it looks like it is working. Writing the prompt and choosing
the schedule is the authorization. Set permissions: "readonly"
on a job that only needs to report.
Letting another AI use aico
aico mcp-serve speaks MCP on stdin and stdout, so Claude Code —
or another aico, or any MCP client — can hand it work:
{
"mcpServers": {
"aico": { "command": "aico",
"args": ["mcp-serve", "--cwd", "/path/to/repo"] }
}
}
Nothing listens. There is no socket and no port: the transport is the pipe the client opened by starting the process, which is why it needs no password to be safe — reaching it already requires being able to run programs as you.
What the caller gets is delegation, not remote control: submit a
task, ask about it, collect the result, stop it. It deliberately does not
expose Read, Bash or Edit — those
would make aico a worse version of the caller's own tools, and would move
every safety rule aico has to the wrong side of the boundary. Every submitted
job carries a spend ceiling and a deadline.
Read-only unless you say otherwise. Submitted work can
read, search and analyse; it cannot run commands or change files until the
server is started with --allow-writes.
It is not inherited from your own autoApprove, because consent
does not transfer. Ticking auto-approve for your own session, with a
terminal in front of you, is not the same decision as letting an unattended
process on the other end of a pipe edit your repository. The posture is
printed to stderr every time the server starts, so it is never a surprise
in either direction.
Nothing headless waits for a person
A prompt with nobody there is not a question — it is a hang. Background agents, scheduled jobs and submitted work now get a decision from policy, never a dialog, and the refusal says what to do instead. The question-asking tool is removed from those runs entirely rather than left present with a disclaimer, because a tool that is present gets called eventually.