OpenHands/agent-canvas

[Bug]: pickFallbackBackend ignores backend health & kind — Settings → LLMs shows "No backend is configured" even when a healthy local backend is registered

Open

#1.174 geöffnet am 5. Juni 2026

Auf GitHub ansehen
 (2 Kommentare) (0 Reaktionen) (0 zugewiesene Personen)TypeScript (26 Forks)github user discovery
bugcanvas-mvpgood first issue

Repository-Metriken

Stars
 (43 Stars)
PR-Merge-Metriken
 (PR-Metriken ausstehend)

Beschreibung

Summary

When there's no valid backend selection (fresh start, or the selected backend was removed), the active backend is chosen by pickFallbackBackend, which returns backends[0] — the first registered backend in insertion order, ignoring both its kind and its recorded health. If index-0 is a cloud backend or a dead local backend, getEffectiveLocalBackend() resolves to null/dead, and every local-protocol call (verified models, LLM profiles, conversation CRUD) throws No backend is configured. — even when a healthy local backend is registered further down the list. A synchronous health store already exists at the point of selection but isn't consulted.

Split out of #1093 (its Docker/local part was handled in #1081).

Environment

macOS · Docker (ghcr.io/openhands/agent-canvas) · 1.0.0-beta.7 · verified against main @ df8a573

Steps to Reproduce

  1. Register two backends so that index-0 is either a cloud backend or a now-stopped local backend, and a healthy local backend is not index-0.
  2. Have no valid selection (fresh load, or delete the currently-selected backend).
  3. Open Settings → LLMs.

Actual Behavior

No backend is configured. toast; empty LLM Provider dropdown; no LLM profiles. The healthy local backend is never tried.

Expected Behavior

Fallback selection resolves to a healthy local backend when one is registered; verified models and profiles load.

Root Cause

pickFallbackBackend (src/api/backend-registry/active-store.ts:36-38):

function pickFallbackBackend(backends: Backend[]): Backend {
  return backends[0] ?? NO_BACKEND;
}

Used by computeSnapshot when no selection resolves (active-store.ts:58-62). The chosen backend flows to getEffectiveLocalBackend() (:94-98), which returns null if it isn't a live local backend; local-protocol calls then throw NoBackendAvailableError("No backend is configured.") (agent-server-client-options.ts:22-27, :55-57). Selection consults neither kind nor the health store.

Proposed Fix

A synchronous health store already exists at this exact point (health-store.ts:29 getBackendHealthEntry; fed by the 10s probe in use-backends-health.ts; disabled persists in localStorage). Rank candidates by recorded health (and prefer a local backend, since that's what local-protocol calls need) instead of taking index-0 (illustrative, untested):

import { getBackendHealthEntry } from "./health-store";

function failScore(b: Backend): number {
  const h = getBackendHealthEntry(b.id);
  if (!h) return 0;            // no recorded failures → best
  if (h.disabled) return 1000; // probes gave up → worst
  return h.consecutiveFailures;
}

function pickFallbackBackend(backends: Backend[]): Backend {
  if (backends.length === 0) return NO_BACKEND;
  const ranked = [...backends].sort((a, b) => failScore(a) - failScore(b)); // stable: index-0 stays the tiebreak
  return ranked[0] ?? NO_BACKEND;
}

Caveats / open questions

  1. Cold-start ambiguitynull health can't distinguish "healthy" from "never probed"; before the first probe a stale-but-unflagged backend can still win. localStorage persistence means it self-corrects after the first session.
  2. Should the fallback prefer a local backend over a cloud one? Local-protocol calls need a local backend, but the current design may intentionally allow a cloud fallback — needs a product call.
  3. Explicit cloud selection still yields getEffectiveLocalBackend() === null by design (docstring in active-store.ts:86-93). That's the literal #1093 repro but appears intentional — if verified models/profiles should work on a cloud backend, that's a separate issue.

Related

#1093 (parent) · #1081 (Docker/local fix)

Contributor Guide