The struct field alignment drifted when the confirm's action id landed, and
fmt-check is the first gate in make test. Every branch cut since inherited a
red suite for a reason no branch owned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two ecosystem defects, both of the shape where a call reports done and
nothing of the sort happened.
entityAttentionCapability surfaced every item it spoke and remembered none
of them, so the previous digest stayed the positional memory. A follow-up
"отметь второй как сделанное" then indexed into a list he had not just
heard and transitioned somebody else's item, which is the exact harm the
position resolver exists to prevent.
A parked Hexis confirm did not carry the correlation id of the action that
proposed it. The confirm lands on a later turn with a context of its own,
so the execution recorded a fresh id and an empty causation: the resolve,
the discovery and the thing they authorised sat in the trace as three
unrelated calls. The contract mints one id per action.
praxisItemAction.handle returned a hardcoded per-verb constant on any error, so
a Praxis outage, a refused token, a contract mismatch and a decode failure all
said the same thing and none of them said "Praxis". The information already
existed: praxisClient embeds ecosystemHTTP, so the error is an *ecosystemError
with working classifiers, and handle() logged it, traced it and threw it away.
servicePraxis joins the two service constants and the failure goes through
ecosystemGap, which is what Nexus and Hexis already use. The per-verb string is
kept in front of it rather than replaced: it carries which operation did not
happen, and the trace is the only other place that exists. No new Russian is
added — both halves are lines that already ship.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The vendored Hexis client is a separate implementation and returns a plain
fmt.Errorf for every status at or above 400, so errors.As for *ecosystemError
never matched, Unauthorized() was never consulted, and ecosystemGap always fell
through to the outage line. A wrong token sent him to inspect a healthy service.
hexisError classifies at Maven's boundary, since the client is vendored from
another repo and a local edit there is lost on the next re-vendor. The status
text is the only signal that survives the wrapping, so that is what it reads;
anything unrecognised stays at status 0, which is what Unreachable() means. The
correct fix is a typed error upstream carrying the code, and Maven cannot land
it unilaterally.
execHexis is the second site and it did not call ecosystemGap at all. It now
does, but only for a failure that belongs to the service. An execution that Hexis
accepted and that then failed keeps the command-level line: that is the command
failing, not Hexis degrading, and calling it an outage would be the same defect
pointed the other way. Authorization is unchanged: a 401 is still a refusal, it
is not retried and nothing proceeds on it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three call sites wrote the same two lines: a rejected credential says
eco_denied, anything else says eco_down, and both name the service.
That is ecosystemGap now, so a fourth caller cannot get it half right.
The failed Nexus resolve was also traced twice in the same shape, once
in the entity attention arm and once in the Hexis act. Both now go
through nexusResolveFailed, which keeps the rune count redaction.
Surfacing a spoken item was written twice as well. surfaceSpoken calls
Surface and nothing else, because reading an item aloud is not an
acknowledgement.
No behaviour change: same lifecycle verbs, same replies, same trace
fields. traceErrorFields lost a duplicated default and returns what it
returned before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The router names a position ("2", "last") or a demonstrative ("this"),
because only the daemon has the list. surfacedItems records the item ids
she read out, in the order she said them, and only for items she could
actually say: one Praxis returned without a title has no position in what
he heard.
resolveSurfacedPosition maps the reference to an id before dispatch, and
its second return says whether the turn is still Praxis's. A position that
names nothing keeps the turn and clears the slot, so the capability asks
which пункт -- he said "второй пункт" and deserves to hear there is no
second one. A demonstrative that resolves to nothing gives the turn BACK,
because "я это сделал" was probably never about a пункт. "это" also needs
the list to hold exactly one item: pointing at one of five is a guess, and
a wrong guess here transitions the wrong item.
No TTL, unlike the pending confirmation. A stale position resolves to an
item Praxis will report as already acknowledged, which is a harmless
answer, where a stale confirmation would execute something.
Measured, make eval-reach, classifier + ONNX: 16/30 -> 27/30 overall,
praxis 0/12 -> 11/12, lifecycle 0/5 -> 5/5, attention 0/7 -> 6/7, hexis
and none unchanged, p50 20.6ms -> 16.5ms. make eval-router: 60/84, 0 false
clarifies, and no failure in that list comes from a stage-0 decision.
Details and the two judgement calls in docs/evals/2026-08-05-praxis-reach.md.
ECOSYSTEM-SPEC §2.6 requires list_attention to distinguish "nothing needs
attention" from "I cannot currently tell", and to say so when a source is
failed or stale. Maven said the first one unconditionally: ListAttention
decoded into []map[string]any, the word degraded appeared nowhere, and an empty
list answered "ничего не требует внимания". A Praxis with every source dead
read as calm.
Two halves, because the spec's mechanism does not exist server-side yet. The
deployed Praxis answers /api/v1/tools/attention with a bare array and no
envelope, so praxisAttention now decodes either shape and believes a degraded
array when one arrives. Until one does, an empty list triggers one read of
/api/v1/sources, and anything that is not reporting health "ok" is named
instead of the all-clear. Zero sources is the same answer: a Praxis that polls
nothing knows nothing, which is the state of this box today.
A sources read that fails is deliberately not a hedge. The attention call
succeeded, and not being able to ask about health is not evidence of a fault.
Both hedges also cover the entity-scoped digest, where a per-entity all-clear
is the more convincing of the two. New keys attention_degraded and
attention_no_sources, in acts_ru_v1.json and the floor. The fake Praxis serves
one healthy source by default, so the existing attention tests still assert an
all-clear on purpose rather than by omission.
entityReferenceText returned the longest Latin run in the utterance, which is a
guess dressed as a rule. "перезапусти nginx на muzick-indexer" holds two names,
the target is not the longer one, and docs/ecosystem.md already says what to do
instead: ambiguous resolution asks the owner, it does not pick. Nexus owns which
names it knows.
So entityReferences returns every Latin run, in the order he said them, capped
at four so one utterance cannot fan out into a dozen HTTP calls.
resolveEntityCandidates asks about each and stops as soon as the answer is
decided: a Nexus failure ends it and reports degradation, Nexus calling one name
ambiguous ends it with its candidates, and two names resolving to different
entities is our own clarify listing the names Nexus spells. One resolving is the
target, none resolving falls through as before.
The transliteration signal is unchanged — the recovery still fires only when the
utterance carries a Latin run and the model's Text slot carries none.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XGTGCWX33aX8SMBSRz9VmS
--no-verify: a merge commit's diff against origin/master is the whole stack,
which the 300-line guard cannot pass. The one conflict was in
internal/store/migrations.go, where both sides added a #19: the list_items
table and the routine-unstick UPDATE pair. Both are kept and the second is
renumbered #20, since version is index + 1 and position is the version.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XGTGCWX33aX8SMBSRz9VmS
read_only was the whole decision on the Hexis act path, which flattened three
answers into two. A capability that wipes the thing it names got the same
single spoken "да" as one that restarts a service, and requires_confirmation —
which the Hexis contract calls server-derived and never settable by a caller —
was read by nobody. docs/ecosystem.md §17.3 says confirmation follows risk.
RiskOfCapability reads Hexis's risk, read_only and requires_confirmation and
returns one of the three tiers internal/tool already had. It takes plain values
rather than a Capability, so internal/tool keeps no dependency on the Hexis
client. RiskOf keeps deriving, because a shell row the owner ticked on /tools
has no upstream to ask.
Every disagreement between the three fields goes up, never down: safe and
mutating is a contradiction and takes the confirm, an unrecognised tier takes
the confirm, and requires_confirmation may only raise. Same default as an
unrecognised dispatch shape — argue your way down, never up.
The irreversible refusal was a Go literal in two places and is now one deck
entry, act_needs_authed_surface. It lost four words to the persona ceiling.
Two caller-side halves of the same review.
«экосистема недоступна» named nothing. Nexus, Praxis and Hexis fail
independently, and every one of the six call sites already knew which one it was
talking to — it writes that name into the trace on the line above. So eco_down
and eco_denied now take {name}, and he hears which service refused him.
The list entries are single-variant and placeholder-only, so an empty list has
no shorter wording to fall back on: attention_list would render as its own label
and a colon. Both Praxis readers checked the response length and neither checked
what survived formatting, so an item with no title counted toward a list it
could not appear in. They skip the untitled item and fall to the _none entry
when nothing is left.
The ecosystem tests asserted the substring "выполнена", which was a literal out
of the act file that review has now reworded. Seventeen sites go through actRan,
which asks the file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XGTGCWX33aX8SMBSRz9VmS
Two defects in one logged line, both of which put a working capability out
of reach of every utterance.
The resident model rewrites as it routes, and on the way it transliterates:
"перезапусти muzick indexer" came back as "перезагрузить музик индексер", so
Nexus was asked to resolve a service nobody has ever named. entityReferenceText
takes the longest Latin run out of his own words, but only when the Text slot
has lost every Latin letter the utterance had — an English turn and a Russian
entity name are both left alone, and reversing the transliteration is not
attempted.
The second half: the stage-3 gate thins an act that matched no allowlisted fn,
and that question was the whole turn, so handleHexisAct never ran. Hexis is
where an act with no local fn belongs, so it gets one chance before she asks,
and a "" back still leaves her asking. With no ecosystem wired nothing changes.
Capability matching reads the phrase as the haystack when there is no fn,
because no capability name contains "restart status muzick indexer".
Authority is untouched: ambiguity still stops, a mutating capability still
goes through the spoken confirm.
Also fixes a flake this stack introduced: the feeds test matched "ничего
нового" as a substring, and query_ru_v1.json can answer with "в лентах тихо".
It asks the entry now, like the others.
The act path minted IDs per hop and trusted whatever Praxis returned
for a scoped attention query. A service that ignored the entity filter
would have had its unrelated items read back to the owner as his. The
handler now assigns one correlation ID at the top of the action and
passes it down, and drops any item the response did not tag with the
requested entity. Traces are written to the trace table with the
causation ID and HTTP status hoisted into columns, the duplicate
legacy Hexis trace is gone, truncated lists say so, and a rejected
credential gets its own reply instead of looking like an outage.
Found in review of #83 and #84.
Every Nexus and Praxis request now carries the contract version, an
X-Requested-By identifying Maven, a correlation ID (generated per request
when the call is not part of a traced action), and a bearer token when
one is configured. Nexus/Praxis/Hexis config blocks grew an optional
token field, env-expandable so the secret stays out of the committed
config; the vendored hexis client predates bearer auth, so a configured
Hexis token logs a loud warning instead of pretending to authenticate.
Client failures are now a typed *ecosystemError carrying service,
operation and HTTP status, classifying unauthorized, contract-mismatch
and unreachable without matching on message text.
Trace records are written for resolution, discovery, confirmation and
execution — on failure as well as success — with status, duration,
correlation and causation ids, HTTP status and failure class, and the
utterance redacted to its length. Traces were never actually persisted
before: both trace writers used fact kind "system", which the store's
CHECK constraint rejects, and the error was discarded.
Add an entity-scoped attention capability: the subject is resolved to a
canonical Nexus entity_id, the id travels to Praxis as a query scope
instead of being dropped after resolution, and Maven's own facts already
tagged with the same id join the answer. Ambiguous, unknown, degraded and
no-Nexus cases each get a distinct reply and never a scoped query without
a scope.
Give the fact-enrichment worker per-fact exponential backoff capped at an
hour and a status report of pending/in-backoff/worst-attempt counts, so a
long Nexus outage shows as a visible backlog rather than facts that
silently never got tagged. Nothing is ever given up on.
voice.go is still the biggest file in cmd/mavend and most of what is left
has nothing to do with the audio path. The ecosystem integration is one
such lump: it talks to Nexus, Praxis and Hexis over HTTP and only touches
the handler for its store and clock. Lifting it into ecosystem_acts.go
puts it next to ecosystem.go, where the clients it drives already live.
Move-only: handlePraxisAct, recordPraxisTrace (called from nowhere else),
handleHexisAct and execHexis verbatim, plus the two imports that became
unused in voice.go.