a Hexis 401 says the token was refused, not that Hexis is down (V-587)

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>
This commit is contained in:
2026-08-06 02:48:00 +04:00
parent d5d4166710
commit 5d2fd91c06
3 changed files with 229 additions and 1 deletions
+16
View File
@@ -551,6 +551,14 @@ func unauthorizedEcosystemError(err error) bool {
return errors.As(err, &ee) && ee.Unauthorized()
}
// isEcosystemError reports a failure that belongs to the service rather than to
// what was asked of it: a call that never landed, or one the far side refused.
// It separates "Hexis is down" from "the restart failed".
func isEcosystemError(err error) bool {
var ee *ecosystemError
return errors.As(err, &ee)
}
// traceErrorFields describes an ecosystemError for a trace without leaking the
// payload: the HTTP status and the failure class, nothing else.
func traceErrorFields(err error) map[string]any {
@@ -777,6 +785,14 @@ func (h *reactiveHandler) execHexis(ctx context.Context, capID, capName, entityI
mergeFields(traceErrorFields(err), map[string]any{
"entity_id": entityID, "capability": capName, "causation_id": causationID,
}))
// Hexis never answering, or answering "no", is a gap in Hexis and is
// named as one — a refused token said "не получилось выполнить команду"
// here and sent him to debug a capability that was never reached
// (Vikunja #587). An execution that genuinely ran and failed is not an
// ecosystemError and keeps the command-level line.
if isEcosystemError(err) {
return ecosystemGap(serviceHexis, err)
}
return phraser.A(phraser.ActFailEntity, map[string]string{"name": displayName})
}
// One record per hop: the second write this used to make said the same