The always-on agent
The fleet forbids resident processes and then permits exactly one. This page covers what earned that exception, what the process has to declare about itself, and the three properties that stop a permanent loop from dying without anyone noticing.
Updated
experimental
Learn what earns a process the right to be resident
Reads this page, explains the ban on resident processes, its one exception and the three properties that keep a loop from dying quietly, then quizzes you. Writes only its checklist.
PROMPT 1 of 2 — learn what the always-on agent page teaches: why a fleet that forbids resident processes permits exactly one, and the three properties that keep a permanent loop from dying quietly. You read and explain. You write one checklist file and nothing else.
I run my own agents and background processes. Skip basics.
FIRST — WRITE YOUR CHECKLIST TO A FILE
Write ~/ALWAYS-ON-LEARN-CHECKLIST.md and work from it:
# Always-on agent — learn
- [ ] 1. Fetch the page
- [ ] 2. The ban, and the one exception
- [ ] 3. The loop, and why apply runs last
- [ ] 4. The three load-bearing properties
- [ ] 5. Isolation, and the weak points
- [ ] 6. Quiz me, then tell me to paste prompt 2
## Rules
- Read only. The checklist is the one file I write.
- Every claim quotes the page sentence it came from.
1 — FETCH
curl -s https://docs.utopiamodels.ai/docs/structure/agent-always-on.md
If that fails, say what came back and stop.
2 — THE BAN AND THE EXCEPTION
Explain why a resident process is forbidden for anything the board, a run or a git event already announces — the failure mode is silence, which looks like health. Then the exception's condition: an external system that emits no subscribable signal, where latency below the poll interval is the point. And the requirement that does most of the work: the process must NAME that event in its own docs. Give the two events this one names, and why the second one — a poll that stopped for good once it captured its identifier — is the right shape.
3 — THE LOOP
Draw detect → enrich → mailbox → notify → apply. Explain why apply runs LAST, using the page's numbers (median 19 s, p90 98 s, max 246 s across 45 records) and the fact that duties run sequentially.
4 — THE THREE PROPERTIES
- a duty is REGISTERED in the loop body, not discovered from a directory — why a dropped-in handler is dead code that looks installed
- `Restart=always` without a start-limit interval and burst restarts a crash loop forever, never enters `failed`, and never fires the failure path
- a schedule is a timer; only the watch is resident — the three tests the daily rollup failed
5 — ISOLATION AND WEAK POINTS
Explain why the isolation is "the absence of a name in two string literals" and why that survives everyone forgetting. Then the weak points: unit files in no repository, an undescribed virtualenv, and a probe run as the wrong user that cannot tell "absent" from "not permitted".
6 — QUIZ AND FINISH
Ask me three questions with the `AskUserQuestion` tool, one at a time, four options each, exactly one correct — one must be a proposed new duty I have to classify as resident or timer. Quote the settling sentence after each.
If `AskUserQuestion` is unavailable in this session, ask the same questions as plain numbered text in one message instead.
Then say exactly this:
"Always-on learned. Paste prompt 2 and I will put everything resident on your machine through the test."
Then stop.Put everything resident on my machine through the test
Inventories everything that runs continuously or on a schedule on this machine, puts each through the naming test and finds the ones that can die silently. Writes only after you confirm.
PROMPT 2 of 2 — inventory everything that runs continuously or on a schedule on this machine, put each resident process through the naming test, find the ones that can die silently, and fix what I approve. The survey is read-only; every change waits for my explicit yes.
HOW LONG THIS TAKES: a multi-step audit (6 steps, about 5 questions, writes only what you approve) — expect roughly 10–25 minutes across several turns. Running headless with a timeout? Budget for that, or start with a shorter prompt first. Cut off partway? Paste this prompt again — it resumes from its checklist rather than starting over.
curl -s https://docs.utopiamodels.ai/docs/structure/agent-always-on.md
Read it first if prompt 1 did not run in this session.
ASK EVERY QUESTION WITH the `AskUserQuestion` tool, with a RECOMMENDED option first and one line of why.
FIRST — WRITE YOUR CHECKLIST TO A FILE
RESUMING? Before you write anything, check whether ~/ALWAYS-ON-INTEGRATE-CHECKLIST.md already exists. If it does, this prompt was interrupted: read it, tell me which steps are already ticked, and continue from the first unchecked step. Do not overwrite it, do not redo a ticked step, and do not re-ask a question whose answer it already records — the scope answer under ## Rules included, if it's there. If ## Rules has no Scope field, this checklist predates the scope question: say so plainly in your first output line and ask it now, before anything else. Only if the file does not exist, write it fresh as below.
Write ~/ALWAYS-ON-INTEGRATE-CHECKLIST.md and work from it:
# Always-on — integrate
- [ ] 1. Enumerate, from every layer
- [ ] 2. Separate resident from scheduled
- [ ] 3. The naming test on each resident process
- [ ] 4. Find the ones that can die silently
- [ ] 5. Report in three piles
- [ ] 6. Fix what I approve, and prove it
## Rules
- Steps 1–5 are read only. I start, stop, enable, disable and edit nothing.
- Every finding carries the command and its raw output.
- Something I cannot read is UNREADABLE, never absent — say which account could read it.
SCOPE — ASK ME BEFORE YOU SURVEY ANYTHING OF MINE
Before step 1, ask me this with the `AskUserQuestion` tool, with both options exactly as written:
"Should I treat any path as off-limits, or do you want me to have full access to survey and propose changes anywhere in this environment?"
A — Exclude .env*, credentials and CI/deploy config (.github/workflows/, deploy keys) from anything I read or propose touching. [RECOMMENDED for most readers]
B — No exclusions. Full access to survey and propose anywhere, including secrets and CI files.
This is my preference, not a policy: some people want their agent to reach everything, some do not, and neither answer is the correct one. Never assume it and never pick for me. My answer governs every read and every proposal for the rest of this prompt. Write my answer under ## Rules in the checklist as "Scope: A" or "Scope: B", so it survives compaction. It governs the SURVEY and PROPOSE steps only — nothing gets written until step 6, and there only after my yes.
Before you ask, decide whether an answer can reach me at all. It cannot in a one-shot non-interactive run — `claude -p`, `codex exec`, `gemini -p`, a scripted or CI invocation — and you can tell without guessing: the `AskUserQuestion` tool is missing from your tool list there, while an interactive session has it. It also cannot if the question comes back immediately with no real answer. In either case do not ask, do not wait and do not re-ask: a question asked as plain text in a one-shot run ends the run with nothing done. This is the one case where you pick for me: treat A as my answer, record and hold it exactly as above, and make the first line of your output exactly: "No interactive scope answer available — defaulted to A (excluded .env*, credentials and CI/deploy config). To choose B, re-run this prompt in an interactive session and answer the scope question."
Only if the `AskUserQuestion` tool is in your tool list but fails when you call it, ask the same question as plain numbered text in one message instead.
1 — ENUMERATE
There is never only one scheduler. Cover all of these, and say which do not apply here:
systemctl list-units --type=service --state=running --no-pager
systemctl list-timers --all --no-pager
systemctl --user list-units --type=service --state=running --no-pager
systemctl --user list-timers --all --no-pager
crontab -l
ls -la /etc/cron.d/ 2>/dev/null
launchctl list 2>/dev/null
docker ps --format '{{.Names}}\t{{.Status}}\t{{.Command}}' 2>/dev/null
Then ask me with the `AskUserQuestion` tool what runs that a machine listing cannot see — a hosted bot, a CI schedule, a browser tab left on a dashboard, a polling script on another box. RECOMMENDED option: "nothing else — use what you found".
2 — RESIDENT OR SCHEDULED
Resident stays up between pieces of work; scheduled starts, does a thing, exits. Anything resident that does its work on an interval is a timer wearing a daemon's clothes — flag it.
3 — THE NAMING TEST
For each resident process:
1. What EVENT does it exist to catch? "Monitoring the API" is not an event. "A 5xx rate crossing a threshold" is.
2. Could I be TOLD about that event — a webhook, subscription, callback, trigger, file watch? Say whether it already exists and I simply have not wired it.
3. Is SPEED the point? If five minutes later would be equally useful, it is a schedule.
4 — SILENT DEATH
For each resident process, from its actual config, quote:
- its restart policy — `Restart=always` with no `StartLimitIntervalSec`/`StartLimitBurst` crash-loops forever and never enters failed
- its failure path — an `OnFailure=` unit or any alert; if none, its death is silent
- where it announces liveness somewhere a person actually looks
And every timer whose service unit does not exist — it never fails, so `systemctl --failed` reads clean.
5 — REPORT
Three piles: KEEP RESIDENT (passed the test) · DEMOTE TO A SCHEDULE · REPLACE WITH A SUBSCRIPTION. Then the silent-death list and any timer pointing at a missing unit.
6 — FIX, WITH CONSENT
Ask with the `AskUserQuestion` tool which to fix first. RECOMMENDED: the silent-death finding most likely dead right now — it is the one where I have no way of learning I was wrong.
Show the exact change — for a systemd unit usually a drop-in adding `StartLimitIntervalSec=` / `StartLimitBurst=` and `OnFailure=` — and write it only after my yes. Then prove it: show `systemctl show <unit> -p StartLimitBurst -p OnFailure` reading the new values. Do not crash a production process to test the failure path without a separate yes.
When the writes are done, tell me in one line, filled in with the real paths:
"To undo, delete `~/ALWAYS-ON-INTEGRATE-CHECKLIST.md` and <each drop-in you wrote, e.g. `~/.config/systemd/user/<unit>.d/<name>.conf`>, then run `systemctl --user daemon-reload` (without `--user` for a system unit) — nothing else on your system was touched."
If I approved an edit to a file you did not create, name that file in the same line with the command that reverts it (`git checkout -- <path>` if uncommitted) instead of claiming nothing else was touched.
Finish with: the three piles, what changed, and the one process you would bet is dead right now.This page comes from the fleet's own component document for agent-always-on. It is marked experimental
for three reasons. There is exactly one instance of it. It exists under an amendment to a rule that forbids
every process of its kind. And its service units live in no repository, so nothing compares what is
running against a committed artifact.
Everything else in this fleet is event-driven. Work arrives on a board, a run announces itself on an issue, and a merge changes a status. The fleet forbids a resident process for anything that the board, a dispatched run or a git event already announces. Heartbeat polls and scheduled health checks fall in that group. When they fail they go silent, and silence looks exactly like health.
There is one exception, and this page is about why it was allowed.
What a permanent loop has to prove
The amendment permits a resident process only to watch an external system that emits no signal anyone can subscribe to, and only where reacting faster than the polling interval is the actual goal. It adds one requirement that does most of the work.
The process must name the external event it exists to catch, in its own documentation.
That requirement is the part other teams can reuse. Suppose a daemon watches for an event nobody can subscribe to. If it cannot name that event, it is polling something it could have been told about. Writing the name down makes that obvious to the author, before anyone else has to review it.
This process names two events:
- A role appearing on an applicant-tracking board. No webhook, feed or subscription exists for "a role opened at any company in this region". The only advantage is being early, and an application sent after two minutes is a different application from one sent after twelve hours.
- A chat-membership update at bootstrap. This is the moment a human adds the bot to a group. This watch is finished. The poll stopped for good once it captured the identifier, and the published feed reports it as captured, not as running. A watch should stop once it has caught its event. A watch that keeps polling for something it already has is built wrong.
The loop
service, Restart=always one cycle
├─ detect watch the external boards
├─ enrich fill in what was seen
├─ mailbox inbound replies
├─ notify outbound, successes only
└─ apply act on what detect wrote ← LAST, and deliberately
each duty runs every executable in its directory, sequentially,
logging the name and the exit code of each
handlers WRAP the canonical tools in the main repository — never a fork
every cycle merges a liveness block into a state file
→ collected into a committed feed
→ rendered on the command center, where a human already looksThree properties that are load-bearing and easy to undo
A duty is registered in the loop, not discovered from its directory
The loop body lists, as literal calls, which directories run. If someone drops a handler into a directory that is not on that list, the handler looks installed but is dead code. It is present and executable, and nothing ever calls it.
The tempting alternative is to discover handlers by scanning directories. That buys a small convenience and costs a failure that nobody sees. With an explicit list, adding a handler takes one edit that a reviewer can see.
The last duty runs last for a measured reason. It is the only handler that can take minutes instead of seconds. Across 45 records its median is 19 seconds, its 90th percentile is 98 and its maximum is 246. The duties run one after another, so if it ran before the watch, it would use up the latency budget that the process exists to protect.
In a loop that runs duties in sequence, order matters, so write the reason for the order next to the order.
The restart limit is what makes death visible
Restart=always on its own restarts a crash loop forever. The unit never enters a failed state, the
failure handler never fires, and the always-on agent dies while its service manager still reports it as
active.
A start-limit interval and burst turn an endless crash loop into a unit that eventually gives up, enters
failed, and triggers the notification path. Without them, the design loses its most important property,
that failure announces itself. It loses it for the one component that has no other liveness signal, and
nothing tells you.
A schedule is a timer. Only the watch is resident
The daily rollup that summarizes what the loop did is a timer, not a sixth duty. There are three reasons, and you should test any future addition against each one:
- A rollup of rows already written gains nothing from low latency, so it fails the test that allowed the resident process in the first place.
- The duties run in sequence, so a rollup inside the loop would use up the watch's time budget.
- A report on what the system did today must keep working when the thing it reports on stops.
Here, anything that runs on a schedule is a timer, and only the watch is resident. That is the test for any proposed sixth duty, and it is much easier to apply than "is this important enough."
Its isolation is enforced by code that never reads the registry
The scheduler's list of dispatchable agents is a hardcoded string in two scripts. This agent is in neither, so no board issue can route to it. A misconfiguration, a typo in a lane name, or someone assigning it in good faith cannot change that.
Adding it to either list would make an always-on agent dispatchable, and that is the one property agents of this kind must not have. So the isolation is not a policy that anyone has to remember. It is a name missing from two string literals, and that guarantee keeps holding even after everyone forgets about it.
The second half is simple. This agent never runs in the foreground. All human interaction with it goes through the planning agent. If the resident process had its own chat interface, people would have a second place to give it instructions, and a second place to give a system instructions is a second tracker by another name.
Where this one is currently weakest
- The unit files are not in any repository. A script installs them from a different repository, and the main repository holds neither the files nor the script, so nothing compares the running schedule against a committed artifact. Read the live unit files on the box. Do not trust a written copy, including this page.
- The interpreter is a virtual environment on the box, and nothing in git describes it. One duty needs a real browser under a virtual display, and on this distribution the system interpreter has no package directory of its own. Three commands documented in the agent's own brain repository create the environment. When it is missing, the handler's pre-flight check names it, instead of failing with a bare non-zero exit. Check that it exists. Do not assume it.
- A probe run as the wrong user reports a missing unit and a unit it cannot see in the same way. These are per-user service units, so a probe from another account cannot see them, and the failure text does not tell "not there" apart from "not yours to see." Run the probe as that account with the runtime directory set. Otherwise read the committed feed, which carries its own timestamp and is the only reading available from an account that cannot reach the box at all.
That last point applies well beyond this fleet. A health check that cannot tell "absent" from "not permitted" will eventually report a working system as dead.
Architecture
How a one-server fleet is laid out, covering its hosts, its agents, the dispatch runtime, the services on the box and how it stores secrets.
The skills layer
A skill is reusable agent behaviour stored as a directory with one markdown file. It reaches exactly one machine. The fleet has three delivery layers, and each one reaches a different set of machines.