Skip to content

0.15.0

Released 2026-08-20.

The release that answers. Two operator agents worked a full day of infrastructure incidents without opening Toise once. A third used it heavily and concluded that a host had been renamed, a database deleted and a service removed by hand — three human operations that never happened, asserted at high confidence, inferred from entities disappearing.

The graph held the right answers the whole time. What failed was everything around the data: what an answer looks like, what it admits, and what an agent knows before asking. That is what this release fixes.

No wire-contract break, no data migration. Everything is additive.

The day that produced this release

Two production incidents cost roughly two hours of investigation. The root cause was a WireGuard netdev being created across the fleet, which reconfigured the public interface on each host and dropped its DHCP lease. It was found by reading journalctl on machine after machine.

Toise had it. One graph_diff bounded to the incident window returns the whole rollout — eleven hosts gaining the same interface and its address, in sequence, with timestamps. That is the view a per-machine log structurally cannot produce: not this host changed, but eleven hosts changed the same way in six minutes.

Nobody reached it, and the naive path actively misled: asking recent_changes for a five-hour window returned the most recent slice, which contained nothing of interest. The honest reading of that day is not that consumers used the tool badly. It is that a graph which holds the answer and returns "nothing" to the natural question is, from where the user stands, indistinguishable from a graph that does not know.

Guidance where an agent actually looks

The MCP instructions field — the one guidance channel delivered to every client at initialize, without being asked — was empty. Everything we had written lived in a resource that must be deliberately fetched and in prompts a human invokes, so an autonomous agent saw none of it.

It now carries only what changes an answer:

  • Start from describe_schema. Guessing type and attribute names is the most common way to conclude "Toise does not know that" about something it holds.
  • Bound an incident window with graph_diff (or recent_changes with from/to) — fleet-wide, in one call.
  • What a disappearance does not mean.
  • Ids are per-replica and re-minted after the resurrection window, so they must never be carried between investigations.
  • An address is two hops away, not an attribute of its host.
  • Absence is not evidence of absence — the graph is exactly as complete as its producers.

Disappearances say what they mean

delete_source has always answered who authored this disappearance. It is a three-value enum sitting next to a field name that reads like a cause, with its semantics living in a contract document — and it was read as a cause, repeatedly.

Every change on the feed now carries a disappearance sentence alongside the code, and each one states the negative explicitly:

delete_source What the payload now says
producer the producer reported this gone — an observation about the world, not an operator action
liveness_expiry no news: the producer went silent, and the thing may well still be running
cascade collateral: something this edge touched died — a consequence, not a cause

None of the three means a human deleted anything. Never report an operator action, a rename or a manual removal from a disappearance alone.

A window you can trust

recent_changes gains from / to, so the tool an agent reaches for first can answer about the past — the ten minutes before an alert — instead of only about now. Previously the sole way to reach further back was a wider window, and the limit then kept the newest changes: an event two hours old was silently absent from a five-hour answer.

Every answer now names the window it actually read (window_from, window_to). When the limit truncates, covered says so in words:

PARTIAL: 200 of 800 matching changes returned, covering 15:12:03Z to 16:28:38Z —
NOT the whole window you asked for (11:28:38Z to 16:28:38Z). 600 older changes in
the window are not shown. To see them, ask again with from/to bounding the
sub-window you care about (that is how you investigate the minutes before an
incident), or narrow with kind/change_type.

This is the "no silent caps" rule applied to time rather than to pages: a partial answer that reads as a complete one is worse than an error.

Heartbeats leave the GraphQL history

entityHistory and recentChanges now take includeHeartbeats (default false), matching what MCP has always done. A live window is dominated by entity.unchanged heartbeats; returning them by default meant a client asking what changed received mostly noise.

Behavior change: a client relying on heartbeats appearing must now pass includeHeartbeats: true. Counts drop sharply — that is the point.

Producers: the four-key endpoint helper

pkg/emit v0.10.0 adds DependsOnLocalEndpoint, which builds the identity of a host-local network.endpoint so producers stop hand-rolling the place the contract is most often violated. Forget the fourth key on a loopback address and every machine's local endpoint collapses into one entity; add it on an RFC1918 peer and you fork an identity nothing else will ever join.

The helper classifies the address itself — exactly 127.0.0.0/8, ::1, 169.254.0.0/16 and fe80::/10 gain the observing host's id, everything else falls back to the three-key edge — and applies the frozen canonicalization on the way through, so ::ffff:127.0.0.1 and 127.0.0.1 derive one identity rather than two.

Also in this release

  • The debug UI names its tenant on every page, with a zero-JS switcher where tenant selection is client-controlled. An empty page that does not say what it is empty for was once diagnosed as "the producer stopped".
  • The HA docs state what replicas agree on — and what they legitimately do not, which had been diagnosed twice as a defect.
  • The ingestion guide documents the 15-minute resurrection window, which had shipped since 0.7.0 undocumented, along with its two consequences: never persist a logical id, and ids are per-replica.
  • The 1.0 freeze audit walked the whole MCP surface; two conventions are now stated once in the stability policy rather than repeated per field.