Files
LocalAI/docs/content
Ettore Di Giacinto 6b712e76db fix(cluster): keep the refusal vocabulary in one table
The worker re-classified a failure a local service had already classified.
classifyServiceFailure preserved exactly one of the four refusal codes,
which was faithful to its own comment for as long as there was one worth
keeping; once ErrStreamNotServed existed, a service returning the code
whose whole job is to say "I learned nothing" had it promoted to
ErrStreamTargetUnavailable, which every reap guard acts on.
ErrStreamTagUnknown was promoted too, and cost nothing only because both
sides of that one reap. No in-tree service produces either, which is the
same "unreachable, therefore safe" argument that let the request-frame
merge survive a whole phase, and LocalService is exported.

The cause was a fifth site enumerating the vocabulary by hand, so the fix
is one table. streamRefusals pairs each sentinel with its wire code and
with whether a frontend may act on it as evidence about a backend, and the
writer, the reader, IsWorkerAnswer and the new IsStreamRefusal all read it.
A fifth code is now taught to every one of them at once.

The codes are also pinned against literals written out in a spec, the way
this branch already pinned the NATS vocabulary. The round-trip table
cannot see a rename, because a rename moves the writer and the reader
together; an unrecognised code is deliberately not the worker's answer, so
renaming "unavailable" would turn every crashed backend on a tunnelled
worker into a row nothing can ever reap, silently and with the suite green.

Three comments the previous fix falsified, corrected:

- tunnelHeaderTimeout still said the window bounds only framing the
  frontend writes immediately after opening the stream. That is true on the
  direct path and false on the relay path, and it was the argument for
  treating an expiry as the frontend's fault.
- classifyServiceFailure's deny-list is three causes, not two: on a dial
  error net.Error.Timeout also covers ETIMEDOUT and EAGAIN. Both are kept
  deliberately, because reaping a wedged or resource-starved backend is the
  eviction this phase exists to prevent, and ECONNREFUSED still reaps.
  isReadTimeout is renamed reportsTimeout, which is what it asks.
- The operator table named three refusals and said a refusal is acted on.
  It now lists four, with when each is sent and whether the row is reaped.

Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-02 06:10:26 +00:00
..