0.13.0¶
Released 2026-08-13.
One answer per question. Three places where the same question had two answers, or none.
No wire-contract break, no data migration. Every change is additive.
same_as on GraphQL¶
Toise stores two entities when two producers describe the same real machine
from different vantage points — a hypervisor's compute.vm and an in-guest
host. That is deliberate: merging them would destroy the ability to say which
producer saw what (ADR 0018).
When a producer can justify the link it asserts a same_as edge carrying a
confidence and a basis. Toise derives the grouping at read time, and never
writes it back.
That overlay was reachable only over MCP. A GraphQL consumer had to walk
same_as itself and pick its own threshold — which is how two read surfaces
come to disagree about whether two entities are one machine.
query WhoElseIsThis($id: ID!) {
canonical(id: $id) {
aliases { id type label }
links { from to confidence basis }
}
}
Null when nothing qualifies. Otherwise every entity reachable over same_as
edges at or above identity_confidence_threshold (default 0.9),
transitively, with the edges that justify it.
The walk now lives in one place and both surfaces call it, so they cannot
drift. It is a top-level query rather than a field on Entity so that
asOf selects the graph the belief is read from: a group is derived from
edges, and edges change over time like anything else.
Below-threshold evidence stays in the graph and collapses nothing — a wrong merge answers confidently about the wrong machine, which is worse than a visible gap.
host.id has one spelling¶
/etc/machine-id holds 32 hex digits with no separator. A library that formats
the same bytes yields a hyphenated UUID:
Same machine, same file, two strings — and under exact identity, two entities with nothing in the graph saying they are one. A producer met this between its own agent and a new Kubernetes probe: one duplicate host per node, per cluster, silent. Checking that both sides read the same file proved nothing; only comparing the emitted strings caught it.
The contract now pins the rendering — lowercase hyphenated UUID — and the
conformance kit fails on the raw 32-hex form and on uppercase hex, wherever
host.id appears: a host identity, a compute.vm's hypervisor id, a
host-local network.endpoint's fourth key, a relationship target.
A failure rather than an advisory, because the defect is silent and surfaces months later against an entity everything already points at.
If you already emit another rendering
Coordinate the change; do not switch on upgrade. Your graph is keyed on the form you emit today, and aligning re-keys every host and cascades every edge that touches one. The check's message says so.
The db fallback identity¶
The contract listed PostgreSQL's system_identifier, MySQL's server_uuid,
then an operator-configured logical name, and stopped. But MariaDB and Redis
expose no stable identifier (Redis's run_id changes at every restart), and
most operators configure nothing — so the fallback is the common case, not the
exception.
Left unspecified, a producer reasonably invented address:port. And
127.0.0.1:3306 is byte-identical on every machine, so every local database in
an estate collapsed into one entity whose attributes flipped between
servers: 118 attribute updates in six hours, with telemetry join keys pointing
at one machine while the attributes described another.
The fallback is now specified:
<db.system.name>:<port>@<host.id> — reusing the @<host.id> shape the
contract already defines for service.instance, so how do you name a local
thing with no stable id has one answer instead of one per type. The port
discriminates within a host; it is not the basis of the identity. Routable
addresses are left alone: they already distinguish the target.
The contract now also documents how to migrate an identity — an unconditional
explicit entity.delete of the old one, and a one-cycle same_as bridging the
two so the timelines stay joinable without anything being merged — together with
the trap that follows. The cascade removes the bridge along with the deleted
entity, so after the cutover a current-state query for same_as returns zero
and looks like a failure to emit. Verify with a dated read at the cutover instant.
Local databases gain a runs_on
A host-local identity was previously refused an anchor, so a local database was in no host's impact radius at all. With a unique identity it gains one — a hole in the graph closing, not a modelling nicety.
Also¶
-
The liveness sweep counts entities and relations apart. It logged
liveness sweep expired stale entities count=14for a pass that expired 7 entities and cascaded 7 relations: the label said entities, the number was both. During a flap triage that ratio is the signal.Log format change if you parse these lines: the message is now
liveness sweep expired stale recordswithentities=andrelations=instead of a singlecount=. -
ADR 0034 proposes
network.segment— a reachability domain, so that why can't A reach B becomes a graph question. Proposed, not implemented: only the Docker Swarm subtype has an assigned identifier, and the Kubernetes and VLAN cases stay explicitly open rather than being guessed.
Upgrading¶
Nothing to do. Existing types, relations and stored events are untouched. If you parse the liveness sweep log line, note the format change above.