Pin this tag from the gem: #v2.0.3.
No Mod redeploy needed — AI_RELAY_VERSION stays at 2.5.0.
Every issue in this release was filed by DM Whisper as a downstream consumer, and they share one theme: an operation that did not happen, reported as though it did.
Failures are now visible as failures (#190, #191, #192)
ToolResult gains isError, plus a fail() builder beside text()/json(). A handler that throws always got the flag from the SDK; one that returned a failure had no way to say so, so clients were left pattern-matching English prose. Prose is unchanged — this is additive.
resolve_aoe no longer swallows a failed sheet read into {bonus: 0, source: "none"}, which was byte-identical to "this creature has no save for that ability". It fails before the damage batch, so nothing is half-applied, and the report marks any save that was genuinely rolled on a flat d20.
Five swallowed reads now propagate, including two found while triaging that the issues hadn't named: getDoors/getPaths reported an unread page as "no walls" (which would tell a wall-placement QC pass the walls were never placed), and batch_import_maps treated an unread page list as "nothing exists" and re-created pages skipExisting was passed to protect.
A live bug this surfaced
Surfacing source immediately exposed a real defect underneath. The relay collapses an attribute with an empty max to a flat value rather than {current, max} — the common case for NPC save attributes — and resolveSaveBonus read only .current. Every monster whose sheet carries a save that way had been saving at +0 and taking the damage. get_character_attribute had always handled both shapes; this path never got the same treatment.
Reads report misses, and expose token class (#194, #196)
get_token returns an error on a miss instead of a null body with isError: false. list_tokens and get_token now carry tokenClass, so a client can finally tell a sidekick from a true PC — previously the server routed HP writes on a distinction it would not disclose.
A per-flavor familiar class was considered and rejected: familiars, animal companions and summons are all player-controlled NPCs, and none of them route differently. sidekick is documented to cover all of them.
Name resolution tolerates punctuation (#195)
"Bandit Captain, the Scarred" — the natural spoken form, and what speech-to-text produces — now resolves to Bandit Captain the Scarred. Case was already folded; punctuation is the same class of difference. Four forked copies of the same matcher now share one normalizer.
Widening only ever widens: ambiguity still refuses rather than guessing, and that now extends to identically-named tokens, which previously resolved silently to whichever came first.
Docs (#181)
NOTICE drops the D&D Beyond bridge claim and reframes the Ammann tactics-index section as historical, preserving every legal fact.
Not in this release: the epithet-pool rework (#199, PR #201) bumps the relay to 2.6.0 and is held back deliberately — it would warn every campaign about a stale deploy until each is redeployed by hand.
483 tests, clean build and lint.