Xeo Forge v1.25.1 — Local-First Agent Workbench
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; productionnext buildclean (full vitest suite + lint are CI's
canonical gate).- Live display-layer proof: the harness asserted
Kimi K3 (scripted) · Scripted Providerin 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.