Standing agents
Maintainer, 2026-09-29: "we should have standing bug triage. find out where it is in docs. wanna also have url to see in platform in an app ==> all standing agents" — and: "issue triage should probably run on control and should be triggered by changes in github."
Most agents run because a person opened a chat with them. A standing agent is one the platform wakes by itself: a GitHub webhook delivery, a signed event on the pooled inbox, a sweep on a schedule. Nobody watches it start, so the only way to know it is alive is to be able to see it — what wakes it, where it runs, when it last did something, and what it has open.
The app is Standing Agents — on any instance,
https://{instance}/Essentials/StandingAgents. It ships with Essentials, so it is on every portal;
each portal shows the standing agents whose packages it has installed and whose records the viewer
can read.
Declaring one — data, never a list in code
A package declares a standing agent with ONE node beside its agents:
{Package}/StandingAgents.json a Markdown node backing the folder
{Package}/StandingAgents/{id}.json nodeType: Essentials/StandingAgent
| Field | Meaning |
|---|---|
agentPath |
the agent node, e.g. Hosting/Agent/triage; empty for a deterministic job that runs no model |
purpose |
one line |
triggers |
what wakes it, one entry per trigger — webhook event kinds, inbox event kinds, a sweep and its cadence |
runsOn |
a ROLE — the control instance, the build instance, every portal — never a host name |
wired |
false when the agent is designated but nothing starts its threads yet; the page says so |
ledgerPath + activityFields |
the node that records its activity and the instant fields on it that count; the latest one is the row's last activity |
queuePath, threadNamespace |
where its work items and its threads live; a pattern such as {recipient}/_Triage is shown but not linked |
openItemsQuery |
an ANCHORED query whose matches are its open items, counted live; empty = not derivable, and the page says so |
docPath, note |
the page that documents it, and what an operator must know besides |
The app lists every such record by one query — nodeType:Essentials/StandingAgent partitions:all
(StandingAgentCatalog.Query). There is no list of agents anywhere in the code: a package that adds
a record adds a row, on every instance where the package is installed, and an agent that is not
declared is not listed, whatever it is called. Both halves are pinned by the Tests area of
Essentials/StandingAgent, including a live journey that writes a scratch declaration, finds it by
the query, renders its row and deletes it again.
Why a companion record and not a property on the agent. Agent is the AI engine's compiled
type (AgentConfiguration in src/MeshWeaver.AI), parsed from the agent's front matter by the
engine. Putting operational fields there would change a compiled module, its parser and every
portal's engine for facts only an operator page reads — and most of them (a ledger path, a webhook,
the instance it runs on) describe the deployment that wakes the agent, not the agent. The record
needs no core or engine change, lets a package declare a standing job that runs no agent at all, and
lives in the package that owns the agent, so it disappears with that package.
Records are shipped content: change one in the repository, never live on a mesh — a GitSync would revert the edit.
What the columns read
| Column | Read from | Live |
|---|---|---|
| Agent, Purpose, What wakes it, Runs on, Wired, Queue / threads | the declaration | the record's node, through the query |
| Last activity | the latest of activityFields on the node at ledgerPath, in the viewer's time zone, with the field it came from; no ledger when none is declared, reading the ledger…, ledger unreadable: why, or the ledger records no activity yet |
the ledger's node stream — a ledger write re-renders the row |
| Open items | the number of nodes openItemsQuery matches; not derivable when none is declared, query failed: why when it fails — never a silent zero |
the query's change stream |
Each row links to its record's detail page ({Package}/StandingAgents/{id}): the declaration as
a field table, one row per trigger, the note, and links to the agent, queue, threads, ledger and
documentation. Every surface is composed from platform controls — headings, body text, DataGrids
of property columns, navigation links.
Issue and bug triage — on the control instance, triggered by GitHub
Issue triage runs on the control instance and is triggered by changes in GitHub: one
ORGANISATION webhook (events issues, pull_request, pull_request_review, check_suite) delivers
every repository's changes, HMAC-signed, to the pooled inbox Hosting/PlatformBuilds/_Inbox; the
always-on watcher turns an opened or reopened issue into a Hosting/TriageItem and a thread with
Hosting/Agent/triage, and a pull request into a review thread with Hosting/Agent/pr-reviewer.
Hourly (TriageIssueSweep) and ten-minutely (PullRequestSweep) sweeps catch what a delivery
missed. The ledger is Hosting/Triage/Status. The mechanism, the scope and how to verify it end to
end: Hosting/Triage. Both arm only on the portal that IS the triage inbox,
which is why their records say the control instance.
Bug triage is the Essentials concept on top of it — one queue, one agent
(Essentials/Agent/bug-triage), one permanent thread per defect (BugTriage). Its
record is declared with wired: false: the intake still starts every thread with
Hosting/Agent/triage, whose work folds into bug-triage once the intake is switched. The app says
so rather than showing a loop that does not run yet.
Planned: the control role moves from the company portal to a dedicated, minimal control instance (the design record is kept on the control instance). Nothing here changes when it does — the records name the ROLE, not a host, and the webhook's payload URL is the one thing that moves.
Declared today
| Package | Record | Agent | Wired |
|---|---|---|---|
| Hosting | Hosting/StandingAgents/triage |
Hosting/Agent/triage |
yes |
| Hosting | Hosting/StandingAgents/pr-reviewer |
Hosting/Agent/pr-reviewer |
yes |
| Hosting | Hosting/StandingAgents/pr-babysitter |
Hosting/Agent/pr-babysitter |
yes (build instance, opt-in) |
| Hosting | Hosting/StandingAgents/platform-update |
Hosting/Agent/platform-update |
yes, but the release wave is off by default |
| Essentials | Essentials/StandingAgents/bug-triage |
Essentials/Agent/bug-triage |
not yet |
| Essentials | Essentials/StandingAgents/log-triage |
Agent/LogTriage |
yes (where the log watcher is configured) |
| Essentials | Essentials/StandingAgents/notification-triage |
Agent/NotificationTriage |
yes |
| Feedback | Feedback/StandingAgents/wrong-answer-digest |
— a deterministic weekly job | yes |
Deliberately not declared — each was checked against its code, and a person starts it:
Feedback/Agent/feedback-agent (the /feedback skill), Hosting/Agent/live-defect-triage (a person
reports a live defect) and Hosting/Agent/loki-error-triage (its instructions say ideally on a
schedule, but nothing schedules it). The day one of them gets a trigger, its package adds a record.