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.
LimitWhat it catches
deadlineMsWall clock — work that has run too long
maxCostUsdSpend, checked the moment it changes rather than on a sweep
maxStepsA loop that is progressing but going nowhere
idleMsNo 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.