Concepts
The Monitor Protocol has five parts, and each one is small. This page explains what each part is for, how the parts work together, and what version 0.1 does not do yet.
The five parts
| Part | What it is | Where you meet it |
|---|---|---|
| Monitor | A definition: what is watched, how often, and with what authority | POST /mp/v0/monitors |
| Observation | One thing the monitor saw, with where it came from and how much it can carry | the monitor's log |
| State | A running summary computed from the observations, including what it does not know | GET /mp/v0/monitors/<id>/state |
| Subscription | A reader with its own place in the log, called its cursor | POST /mp/v0/subscriptions |
| Lease | An exclusive, time-limited claim on one piece of work | POST /mp/v0/subscriptions/<id>/leases/claim |
Configuration is not a sixth part. Updating, pausing, resuming or retiring a monitor appends an observation of type ai.mentu.monitor.configured that records what changed, before and after. Months later, "why did this change?" has an answer in the same log as everything else.
Monitors
A monitor is created once and then lives on its own. Its main fields:
| Field | What it means |
|---|---|
id |
A name of 2 to 64 letters, digits, dots, dashes or underscores |
owner |
Who it belongs to, such as human:you or agent:ci. The owner decides how high its observations can be rated. |
source |
What is watched: a kind (shell, ws, http, file, feed, cir, formula or log) and a ref |
horizon |
The time scale it works on: event, minute, hour, day, week or month |
capabilities |
What subscribers may be granted: observe, react or act |
visibility |
private, shared or public |
types |
The kinds of observation it emits, in reverse domain style such as com.example.ci.run |
ttl_seconds |
How long silence is normal. After that, the state reports stale_source. |
retire_after_mute_seconds |
How long a subscription may go without pulling before it is retired. The default is seven days. |
| Visibility | Who can see and subscribe |
|---|---|
private |
Only someone holding the owner token |
shared |
Only someone holding the subscribe token the server mints for it |
public |
Anyone. Subscribers get the monitor's default_grant, which is observe unless the owner says otherwise. |
Capabilities are granted, not requested. Asking for act on a public monitor does not give it to you: more than the default grant needs the owner token or the subscribe token.
Observations
An observation is a CloudEvent with six extra attributes and a structured provenance.
| Attribute | What it says |
|---|---|
sequence |
Its place in the log, zero-padded to 20 digits and strictly increasing within a monitor |
tier |
How much the claim can carry: src, measured, derived, unverified or falsified |
origin |
What kind of source made it: human, agent, webhook, probe or system |
verified |
How it was checked: human_verified, machine_verified, certified, reported or unverified |
horizon |
The monitor's time scale |
actor |
Who saw it, such as probe:ci or agent:claude |
data.provenance uses the relation names of W3C PROV. It records the actor, the person it acted for in on_behalf_of, whether the monitor's owner was attested, a source_ref, the rule that produced it, what it wasDerivedFrom, and what it supersedes.
Nothing in the log is edited. A correction is a new observation that names the one it replaces in supersedes, and the original stays as it was. A refusal is kept too: when a write breaks a rule, the server appends an ai.mentu.monitor.rejected observation with the reason and the raw input, because a dropped event looks exactly like one that never happened.
State
The state is computed from the log each time you ask for it. It is a summary, never a second source of truth.
| Field | What it means |
|---|---|
as_of, as_of_seq |
When it was computed, and up to which observation |
covers_until |
The last time the source was actually heard from |
live |
Whether the monitor is running, and if not, why: paused or retired |
counters |
Observations, deliveries, completed leases, active subscriptions and refusals |
ages |
Seconds since the last observation, the last pull and the last contact with the source |
confidence |
A value, its inputs split into present and missing, and its gaps |
While any input is missing, confidence.value stays null. The state never fills a gap with a number. The gaps say what is uncertain:
| Gap | What it means |
|---|---|
independence_unknown |
Nothing has checked whether the sources are independent of each other |
single_actor |
Everything so far comes from one actor |
stale_source |
The source has been quiet for longer than its ttl_seconds |
no_subscribers |
Nobody is subscribed |
no_event_provenance |
Some observations carry no provenance |
unattested_origin |
Nobody independent vouched for the monitor's owner |
Subscriptions and delivery
A subscription's cursor is the next sequence number it will be given. It starts at 0, the beginning of the log, or at the head if you subscribe with "from": "head".
- Pull returns what comes after the cursor, oldest first, and never moves the cursor.
waitholds the request open for up to 25 seconds until something arrives, andlimitcaps the batch. A filter sent with a pull can narrow what the subscription receives, never widen it. - Acknowledge moves the cursor forward, after the work is done. It is cumulative: acknowledging
nextcommits everything before it. A lower value is refused withCURSOR_BACKWARDS, and the same value twice returns the same result. - Redelivery is what happens after a crash. Anything pulled but not acknowledged comes back on the next pull, marked
"redelivered": true. A reader may see something twice. It never misses it. - Seek moves a cursor on purpose, with a reason, and the move is recorded.
- Retention is a server's choice to delete old entries. If a cursor falls below the retention floor, a pull answers
410 CURSOR_EXPIREDwith the floor and a pointer to the state, so the reader can rebuild from the state and carry on. - Retirement is what happens to a subscription that stops pulling. After
retire_after_mute_seconds, it is retired and anai.mentu.monitor.subscription_retiredobservation is recorded. Subscribing again keeps the cursor, so a reader that comes back loses nothing.
Server-sent events, MCP resource updates and the Claude Code Monitor tool are wake-ups. They tell a reader to look, and any of them can be missed. The cursor is the guarantee.
Filters
Filters have the shape of Nostr filters: every key must match, and a list matches any of its values.
{ "types": ["com.example.ci.run"], "tiers": ["measured", "src"], "#status": ["failed"] }The keys are types, sources, subjects, actors, tiers, origins, horizons, since, until, limit and text, plus #<tag> for tags an observation carries in data.tags. An unknown key is refused with INVALID_FILTER, and the reply lists the keys that exist. A filter that silently matched nothing would look like a quiet monitor.
Leases
A lease lets several workers share one stream of work without doing the same job twice. It needs the act capability.
- Claim a subject. One claimant wins. Everyone else gets
LEASE_HELD, naming the holder. - Renew while you work. A lease lasts 15 minutes unless you ask for a different
lease_duration_seconds. - Complete it with the outcome and the evidence, or release it with a reason so someone else can take it.
If a worker stops, its lease expires on its own and another worker can claim the subject. A worker that tries to complete a lease it has lost gets LEASE_LOST. Every claim, renewal and completion is recorded as an observation.
Running a server on your machine
A monitor server usually runs next to a web browser, so it assumes every web page you open can reach it. Since version 0.1.3 the reference server:
- Refuses web pages. A request that carries an
Originthe operator has not allowed is refused withORIGIN_REFUSED. Allow a dashboard with--allow-origin. Command line tools send noOriginand are unaffected. - Answers only to its own names. On a loopback connection, a
Hostother thanlocalhost,127.0.0.1or::1is refused, which stops DNS rebinding. - Caps request bodies at 1 MiB and refuses larger ones with
TOO_LARGE. - Refuses writes to a paused or retired monitor with
UNAVAILABLE, and records the refusal. - Keeps its state safe. Each write is flushed to disk before it replaces the last one, the previous copy is kept as
.prev, and a damaged file is set aside while the newest readable copy is loaded. With no readable copy, the server refuses to start and changes nothing. - Allows one writer per state file. A second server on the same file refuses to start and names the first one's process.
What version 0.1 does not do yet
Some fields are part of the specification but not enforced by the reference server yet. They are listed here so nobody builds on them by mistake.
- Budgets are stored, not enforced. A monitor can declare a
budget, and the errorOVER_BUDGETis reserved, but nothing counts spending yet. - Rules are checked, not run. A monitor's
rulesare validated when they are written. Nothing evaluates them yet. - Push delivery is declared, not sent. A subscription can ask for
protocol: "http"with asink, but the server does not push to it yet. Pull, the stream and MCP notifications work today. - Horizon authority is recorded, not enforced. The principles say a faster horizon never writes what a slower one owns. Version 0.1 stores the horizon on every observation but does not check it.