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 opened on Jun 5, 2026

View on GitHub
 (2 comments) (0 reactions) (0 assignees)TypeScript (26 forks)github user discovery
bugcanvas-mvpgood first issue

Repository metrics

Stars
 (43 stars)
PR merge metrics
 (PR metrics pending)

Description

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