Skip to content

Xeo Forge v1.25.1 — Local-First Agent Workbench

Choose a tag to compare

@github-actions github-actions released this 30 Aug 19:27
· 4 commits to master since this release
109a3cc

v1.25.1 — The Visible Switch

Summary

v1.25.0 shipped the in-session model switcher in the Work governance rail,
and it never worked: every real session rendered "No model selected for
this task" while the API it reads was returning correct data the whole
time. This release fixes that defect, pins the regression class in CI at
the display layer it failed at, and re-takes the desktop evidence frame
that had documented the empty state honestly.

The defect: a consumer reading a POST-only wrapper

WorkGovernanceRail fetched GET /api/providers and read body?.catalog
from the response. But the GET returns the ProviderCatalog DIRECTLY —
the wrapped { provider, catalog } shape belongs to POST /api/providers
only. The adoption gate body?.catalog was therefore always undefined,
setCatalog never ran, and the switcher rendered permanently empty. The
task row stored the right ids; the names just never resolved.

The part that let it ship is worth stating plainly: every verification
looked at the API's response — correct — and none looked at the value
that actually reaches the rail's display. The gap was in the wiring
between the two, and no test covered the wiring.

The fix

The rail's catalog plumbing is extracted into app/work/rail-catalog.ts:

  • adoptProviderCatalog(body) accepts the bare catalog (the actual GET
    contract), tolerates the wrapped POST shape, and refuses anything that
    is not actually a catalog — a producer shape change now degrades to the
    rail's honest empty state instead of a silent permanent blank.
  • resolveCurrentModel(catalog, modelId) derives the exact
    {modelName · providerName} label the rail renders, from the same
    catalog Settings uses, so the rail can never disagree with it.

The regression class is pinned, not just the bug

test/rail-catalog-adoption.test.ts pins three layers in CI:

  • C1 — the GET payload (bare catalog) is adopted; the wrapped shape stays
    tolerated; non-catalog payloads are refused.
  • C2 — the displayed label pair resolves from the adopted catalog, and an
    unknown model id yields the honest "no model selected" state.
  • C3 — source contracts: the rail consumes the API THROUGH the adoption
    helper (the original miss cannot silently return), GET /api/providers
    keeps returning the bare catalog, and the live-UI harness keeps its
    hard-fail assertions.

And because a unit test alone would repeat the original monitoring gap,
scripts/recapture-03-work.mjs boots the REAL Electron desktop shell
against a scripted OpenAI-compatible provider, drives a task to the
decision gate, and asserts the selected model name in the rail's
RENDERED text (aside.w-rail innerText) plus the switcher's actual
selected value. Hard-fail by design: if the value does not reach the
display, it errors out instead of capturing garbage.

Verified

  • test/rail-catalog-adoption.test.ts: 9/9 assertions pass; tsc --noEmit
    clean; production next build clean (full vitest suite + lint are CI's
    canonical gate).
  • Live display-layer proof: the harness asserted Kimi K3 (scripted) · Scripted Provider in the rendered rail text with the switcher's selected
    value matching the task row, zero page errors — and re-captured desktop
    evidence 03 (03-work-decision-gate-model-rail.png) showing the decision
    gate with the MODEL section actually filled.