0.17.0¶
Released 2026-09-21.
The release that says what its answers are worth. Two failures in the field, both of the same shape: an answer handed back a value that looked more solid than it was, and a consumer acted on it. An entity id, opaque and durable-looking, resolved to nothing after a failover. A nanosecond timestamp, exact-looking, was read as ordering two events its producer's cadence could not distinguish. Neither value was wrong. Both were handed over without the one thing needed to use them safely — and in both cases Toise already knew that thing and simply never published it.
Contract delta is additive: new fields, and an existing argument that accepts more forms. No data migration, nothing in the event log changes, and every call you make today keeps working unchanged.
Every entity now carries a handle that survives¶
An entity id is minted locally by whichever replica answers you, and minted again when an entity comes back after a long silence. Carry one across a failover or an outage and it resolves to nothing — and because it is an opaque string, it cannot warn you. Until now the cost was paid in prose: the MCP instructions told every consumer never to carry an id between investigations. A contract that has to be re-explained to each consumer, forever, has moved its cost onto them.
The deterministic name already existed. It is a hash of the entity type and its identifying attributes, so every node computes the same value without coordinating — 0.16.0 already keyed operator annotations on it, precisely because ids do not survive. It was simply never exposed.
Every entity now carries identity_fingerprint (identityFingerprint on
GraphQL), and every read surface that takes an entity id takes a fingerprint
instead. Prefer it for anything you keep: a variable across two calls, a note in
a runbook, a row in your own store.
The id stays, in every answer and in the event log. It is now documented as what it is — local to the answering node, scoped to one incarnation — rather than as "the reference".
Ask about the host you already know, in one call¶
Answering a question about a known machine took two calls: find_entities to
trade host.id for an entity id, then the real question. That round trip is
exactly what teaches a consumer to cache the id — the one value it must not
keep.
The handle argument now also accepts the identity itself, written inline:
host:host.id=6a6d1121-4a85-4e64-a222-746f7bc9c04c
service.listener:service.endpoint=h1:80/tcp,network.transport=tcp
No tool and no query grew a second parameter. The three forms cannot be
confused: a logical id carries no colon, a fingerprint a colon and no =, an
identity both. Matching stays exact — a subset of the identifying attributes is
a different identity, never a near-enough match.
Timestamps say how finely you may read them¶
event_time is when a producer observed a fact, never when the fact became
true. You cannot know that producer's cadence, so you read a nanosecond
timestamp as exact and compare gaps that carry no information.
This is not hypothetical. In an incident review on our own fleet, a 47-second gap between two observations — under a 30-second cadence — was read as "the service returned before the address moved, so the two are unrelated." Direct measurements from the operator later showed the opposite order. The reviewer had even stated the caveat first, then reasoned against it: applied where the gap was minutes, forgotten where it was seconds.
get_entity and entity_history now carry a resolution block, and GraphQL
exposes Entity.resolution:
"resolution": {
"observation_interval": "30s",
"meaning": "every event_time here is when a producer OBSERVED the fact, not when it became true: the change happened somewhere in the 30s before it. Two changes less than 30s apart cannot be ordered from these timestamps, and no causal conclusion may be drawn from a gap that small — not even against an external clock. …"
}
It reports the coarsest interval among the producers referencing that entity, because a faster second producer does not make the slower one's observations finer. It is absent rather than approximate when no live producer declares an interval: saying nothing beats inventing a bound. And it does not claim to know a past cadence — the interval is liveness state, never an event field, so it cannot be recovered for an old observation.
It ships as a sentence and not only a number, for the same reason
delete_source got its plain-language gloss: a bare duration beside nanosecond
timestamps invites the very misreading it exists to prevent.
Under the hood¶
The identity hash now costs one allocation instead of seven. Publishing the fingerprint on every rendered entity made the benchmark gate fire, and the fix pays off beyond the new call site: this hash also runs once per ingested observation, where it had never been measured.
Its encoding is now frozen by a test. The hash is stored — it keys the live and tombstone indexes and is what annotations live under — yet every existing test checked only determinism and distinctness. A changed encoding would have passed CI and orphaned every annotation on upgrade.