Skip to content

Six finished fixes: provider error copy and a /models route that answers, two workflow knowledge-selection defects, esbuild discovery leaving its worktree, phrase search on the marketplace shelves, and three doc corrections - #277

Merged
Broccolito merged 10 commits into
mainfrom
fix/provider-copy-workflow-kb-and-docs
Sep 12, 2026

Conversation

@Broccolito

Copy link
Copy Markdown
Collaborator

Six independent fixes that had been finished on a branch with no PR, brought up to
date with 133 commits of main and verified on the merged tree. 1,574 insertions
across Rust, the desktop UI, the website and docs.

Every count below was measured in this run, on this merge, with
BIOROUTER_DISABLE_KEYRING=true and multiple filters after a bare --.

The six fixes

1. fix(providers) — error copy that does not contradict itself, and a /models route that answers (a2d94fad)

Four defects in what a failed or unconfigured provider tells the user.

  • stop_notice invited a retry on all three of its routes, including the one
    where the error is fatal. Measured on a vendor model rejection, where the text
    above read "version 2.1.251 or newer is required" and the frame below invited
    a retry of a request that will fail identically forever. The advice now follows
    is_recoverable, the same predicate the retry decision uses, so the two cannot
    drift; the retried count stays on the retryable branch only.
  • Claude Code gets the unknown_model_hint Codex already had — the case is
    stronger here, because claude --model X -p accepts an unrecognised id and only
    warns, so a typo neither fails loudly nor gets a pointer.
  • An unconfigured Versa Bedrock stopped rendering error: Error … in Settings..
    plus three keychain lines that answer the wrong question (and, on the sibling
    arm, contradict it: the store-refused message says do not re-enter it while
    biorouter configure says the opposite). end_sentence is now pub so there is
    one rule, and CREDENTIAL_NEVER_SET is shared by both producers and the
    consumer by construction rather than by hope.
  • GET /config/providers/{name}/models returned [] for nine of the twenty-three
    builtins. Behaviour changed, not the name: the same fallback is already correct
    in the declarative-provider branch of the very same handler and in the desktop's
    fetchModelsForProviders, and the shape is unchanged, so no OpenAPI or TS client
    regeneration.

Measured: -p biorouter --lib -- providers:: agents::mistakes privacy::config_keys
837 passed, 0 failed; -p biorouter-server --lib -- routes:: 510 passed, 0
failed
; -p biorouter-cli --lib -- session::builder 20 passed, 0 failed.
Each of the five named fail-before tests was confirmed present and ok by name in
the run logs.

2. docs — a measured subagent count, seven runnable filters, and why BEDROCK_* is shared (ac504b9d)

  • The CLAUDE.md subagent figure was 102 against a real 197. Re-measured on this
    merged tree it is now 198 (main added one in the 133 commits), so the line
    carries 198, today's date, and the fact that it moved by one in a week.
  • Seven cargo test lines written as instructions could not run: cargo test
    takes one positional TESTNAME, so -p X --lib a b exits 1 with a usage line and
    tests nothing. Five were in br71-execution-plan.md, whose own lines 430-437
    explain that exact hazard and then violate it below; two in
    multi-kb-implementation-plan.md. Passages that quote the broken form to warn
    about it, and occurrences under docs/history/, are deliberately left alone.
  • privacy/config_keys.rs: the five shared BEDROCK_* keys now say why sharing is
    correct — all four retry keys become a RetryConfig, the timeout becomes a
    deadline, and none reaches the resolved endpoint tier() asks about or takes
    part in signing.

Measured: -p biorouter --lib -- privacy::config_keys passes inside the 837 above;
-- subagent 198 passed, 0 failed.

3. fix(agent-drafter) — esbuild discovery stops at the checkout it is run from (ce3b86a7)

find_esbuild walked a flat six ancestors, and six is exactly far enough to leave
a worktree — from <repo>/.claude/worktrees/<name>/crates/biorouter-mcp the sixth
step is <repo>. So a worktree with no install of its own silently borrowed the
main checkout's bundler, and every esbuild test passed locally for a reason CI can
never have. The hardest shape of "works on my machine": nothing was missing, the
tool was just not in the tree under test.

The ascent now stops at the nearest ancestor holding a .git entry — tested
after the candidate, because an ordinary clone keeps .git and ui/ in the same
directory, and for the entry rather than a directory, because a worktree's .git
is a file.

Measured: -p biorouter-mcp --lib -- agent_drafter:: 273 passed, 0 failed, 2
ignored
. The neighbouring flake the commit warns about,
a_timed_out_esbuild_reaps_its_whole_process_group, passed in this run.

4. fix(workflows) — an explicitly empty knowledge selection is saved as empty (4dbd0562)

An absent knowledge_bases and a present-but-empty one are different statements
and workflow/runtime.rs reads them differently. The modal emitted the key only
when the selection was non-empty, so the one gesture that says "no knowledge
bases" was stored as the one that says "whatever you have". The condition it
replaces asked whether the selection is empty; the question that decides this is
whether it is known, which is what also made a failed read indistinguishable
from an empty one.

Measured: npx vitest run src/components/workflows/__tests__/ 31 passed (the
figure the commit claims), and the whole src/components/workflows/ directory
121 passed, 7 files.

5. fix(workflows) — a new chat that cannot take its knowledge bases is discarded (15a315b5)

start_agent creates the chat and then runs three steps that can still fail while
it exists. Two take the error, call discard_failed_new_session, then return; the
third used a bare ? and returned straight past the cleanup. Not a rare race — a
workflow whose default names a deleted base fails here on every start, so a
stale workflow minted one orphan chat per press.

Measured: -p biorouter-server --lib -- routes::agent 63 passed, 0 failed;
every_failure_before_a_new_chat_is_returned_discards_it confirmed ok by name.

6. fix(baam) — the marketplace shelves answer a phrase, not a substring (9105063e)

The three shelves asked hay.indexOf(q) !== -1, so a one-word query worked and
every phrase failed: "R scripting ggplot visualization" returned 0 cards and a
"No skills match your search." line. landing/marketplace-search.js ports the
app's canonical matcher rule for rule instead of becoming a fifth implementation.
Licence stops being searchable (the old haystack folded in data-license via
textContent); facet chips are untouched, because they are authored values rather
than prose a person typed; shelves are not reordered, because their order is
editorial.

Measured: node --test landing/scripts/baam-search.test.mjs 17/17 (16 from the
commit plus one added here, below), baam-privacy-facet.test.mjs 14/14,
build-registry.test.mjs 56/56, check-consistency.mjs exit 0.

landing/** deploys to biorouter.ucsf.edu on merge. No registry datum is
touched
git diff origin/main..HEAD -- landing/registry* is empty and
node landing/scripts/build-registry.mjs --check reports all three outputs
current (37 extensions, 129 skills).

Conflicts and how they were resolved

crates/biorouter-server/src/routes/config_management.rs — the only conflict git
reported.
Both sides added a different new function at the same point: ours
declared_model_names, main's provider_details (extracted from the
providers() body it also rewrote). Not competing implementations, so both are
kept, ours first. providers() had already auto-merged to call main's
provider_details, and declared_model_names is reached from the models route at
one call site plus two tests — all four verified wired after the resolution.

ui/desktop/src/components/workflows/CreateWorkflowFromSessionModal.tsx — no
conflict, and the two rules are one rule.
#256 is still open, so main carries
none of it; the collision is with the PR, not with the branch. Read side by side
they are complementary halves of the same serialization decision and touch
different regions of the file:

Composed, a chat with every base switched off yields { default: null, visible: [] } — exactly what the new test asserts — and a chat with bases but no primary
yields { default: null, visible: [ids] }. Measured rather than argued:
git merge-tree of this branch against origin/claude/great-panini-672c85 exits
0 with no conflict list. Whichever lands second needs no resolution.

landing/ — no conflict. main has no landing/ commits since the merge
base, and the licence-as-tag work another session is doing in
ui/desktop/src/components/baam/search.ts / crates/biorouter/src/catalog_search.rs
has not landed either. Worth flagging for that session: this PR's port already
excludes licence from the searched fields and pins it with "the license chip is
not searchable"
, so landing/ needs no licence fix of its own.

CLAUDE.md, docs/agent-loop/designs/br71-execution-plan.md and
docs/knowledge-base/multi-kb-implementation-plan.md auto-merged; main's text is
kept in full and this branch only edits its own lines.

What did not hold up, and was fixed here

Three claims in the six commits stopped being true, all three because of the merge,
plus one that was wrong when written. Each is a separate follow-up commit
(28dce896, d5d5a7b0) rather than a quiet amend.

  1. The canonical matcher moved. Main's fix(skills): rank an installed-skill search by the words of the query, not all of them (F5) #266 renamed
    crates/biorouter/src/marketplace/search.rscatalog_search.rs.
    landing/marketplace-search.js, landing/scripts/baam-search.test.mjs and
    landing/baam.html all cited the dead path — the one pointer a reader of a port
    most needs. Re-aimed, the way search.ts already does on main.
  2. fix(skills): rank an installed-skill search by the words of the query, not all of them (F5) #266 also changed a rule the port mirrors. The verbatim-phrase bonus became
    written_in (word-boundary aware); the port still asked indexOf. Ported, and
    checked against that module's own six assertions — all six agree. Blast radius
    in both directions: the shelves call matching, which collapses rank to a
    membership set, so nothing a visitor sees moves — which is also why the
    drift could have sat there indefinitely, hence the new test.
  3. declared_model_names contradicted its own neighbour. The route's Ok(None)
    arm still read "six of those nine ship a curated with_models(...) catalog"
    the figure produced by the very instrument the function's doc-comment exists to
    warn against, which undercounts by three because ProviderMetadata::new also
    takes a model_names list. In a commit about copy that does not contradict
    itself, the wrong comment to leave standing.
  4. clippy::string_slice. The new source-read test used &body[start..end],
    which the workspace lints warn and scripts/clippy-lint.sh turns into an
    error. Now get + expect, with the boundary argument in the expect rather
    than a comment.

Verification

Command Result
cargo test -p biorouter --lib -- providers:: agents::mistakes privacy::config_keys 837 passed, 0 failed
cargo test -p biorouter --lib -- subagent 198 passed, 0 failed
cargo test -p biorouter-server --lib -- routes:: 510 passed, 0 failed
cargo test -p biorouter-server --lib -- routes::agent 63 passed, 0 failed
cargo test -p biorouter-mcp --lib -- agent_drafter:: 273 passed, 0 failed, 2 ignored
cargo test -p biorouter-cli --lib -- session::builder (isolated HOME, literal CARGO_HOME/RUSTUP_HOME) 20 passed, 0 failed
cargo fmt --check clean
node --test landing/scripts/baam-search.test.mjs 17/17
node --test landing/scripts/baam-privacy-facet.test.mjs 14/14
node --test landing/scripts/build-registry.test.mjs 56/56
node landing/scripts/build-registry.mjs --check all three outputs current; no registry change
node landing/scripts/check-consistency.mjs exit 0
npx vitest run src/components/workflows/ 121 passed, 7 files
npx vitest run src/components/workflows/__tests__/ 31 passed
npm run lint:check exit 0 (tsc + ESLint + themes + 332 contrast assertions + token mirrors)
npx prettier --check on both touched frontend files clean
cargo clippy --all-targets -- -D warnings -A clippy::result_large_err -A clippy::string_slice exit 0, zero findings

On clippy, and why two lints are allowed above

./scripts/clippy-lint.sh fails on this branch — and on pristine main, for the
same two reasons, in files this branch does not touch.
Measured rather than
assumed:

  • result_large_err at routes/reply.rs:1863 (authorize_steer). Reproduced on a
    detached origin/main worktree: cargo clippy -p biorouter-server --lib -- -D warningsexit 101, same line.
  • string_slice at commands/agent.rs:480 (&digest[..62]). Byte-identical on
    origin/main, absent from this branch's diff.

Allowing exactly those two is what lets the sweep reach every other target and show
this branch adds nothing: it went from 2 findings to 1 after fixing ours, then to 0.
Neither is a CI problem — rust.yml's clippy step is cargo clippy --workspace --all-targets --locked with no -D warnings and is labelled informational — but
both are worth someone's follow-up, and neither belongs in this PR.

Check Commit Messages will pass: no Co-Authored-By trailer of any kind appears
in any of the ten commits.

🤖 Generated with Claude Code

Broccolito and others added 10 commits September 11, 2026 19:14
The three shelves each asked `hay.indexOf(q) !== -1` -- whether the WHOLE
lowercased query occurred verbatim inside one card's text. A one-word query
worked and every phrase failed, so the queries a visitor actually types
returned the empty shelf and a "No skills match your search." line:

    "R scripting ggplot visualization"  ->  0 cards
    "SPOKE knowledge graph"             ->  0 cards

This is the same defect `crates/biorouter/src/marketplace/search.rs` closed
for the in-app marketplace, and these were its last two live copies. Rather
than write a fifth matcher, `landing/marketplace-search.js` ports that module
rule for rule: split at whitespace and punctuation, lowercase, de-duplicate,
any-term-matched is a hit, terms under three characters restricted to whole
words, filler dropped unless nothing is left, plural falling back to singular.

Three consequences worth naming.

**License is no longer searchable.** The canonical matcher does not offer it
as a field, and the old haystack folded in `data-license` plus the licence
chip via `textContent` -- so "apache" returned cards that say apache nowhere a
visitor can read as a subject. Callers now hand over a weighted field list
(heading, slug line, prose, org, curated tags) instead of `textContent`.

**Facets are untouched.** The chips still match against the old `hay` string:
they are exact values or curated substrings a chip authored, not prose a
person typed, and baam-privacy-facet.test.mjs pins that discrimination.

**The shelves are not reordered.** `rank()` computes the score, but every hit
is shown here, inside curated sections whose order is editorial -- so DOM
order, which is registry order and the matcher's own last tiebreak, is kept.

Fail-before, on the unmodified page (node --test landing/scripts/baam-search.test.mjs):

    x a natural-language skills query finds the skills it names
        AssertionError: the phrase returned an empty shelf
    x a natural-language extensions query finds the extension it names
        AssertionError: the phrase returned an empty shelf
    x the license chip is not searchable
        AssertionError: a card matched on its license alone

After: 16/16 pass, and baam-privacy-facet.test.mjs stays 14/14.
…scarded

`start_agent` creates the chat first and then runs three steps that can still
fail while it exists. Two of them -- binding the provider and preparing the
workflow prompt -- take the error, call `discard_failed_new_session`, and only
then return. The third, applying the workflow's declared knowledge selection,
used a bare `?`, so it returned straight past the cleanup and left the chat in
the session list.

It was not a rare race. A workflow whose `default` names a base that has since
been deleted fails at this exact step on EVERY start, so a stale workflow
minted one orphan chat per press -- a row the user never asked for and cannot
explain, in a list they do not associate with the workflow that made it.

Copies the siblings' shape verbatim. Adds a test that states the invariant
rather than the fix: between one fallible step and the next, the error path
discards the chat. It reads the handler's source, because reaching the real
handler needs an `AppState`, and `AppState::new` calls `AgentManager::instance()`
and `KnowledgeService::new_default()` -- both of which resolve the developer's
own ~/.config/biorouter, and a test that creates and deletes chats there is
worse than no test.

Two hazards the test documents, both of which bit while writing it: the
handler is `start_agent`, not `create_session` (the name in the body is the
SessionManager call it makes); and the source anchor needs its leading newline,
because `include_str!` reads this file including the test, so an anchor without
one matches the copy inside its own string literal and silently slices the test
instead of the handler -- which fails, but names the wrong step.

Fail-before (BIOROUTER_DISABLE_KEYRING=true cargo test -p biorouter-server --lib
-- knowledge_selection_tests), with the bare `?` restored:

    every_failure_before_a_new_chat_is_returned_discards_it ... FAILED
    `apply_workflow_knowledge_selection(` can return an error without
    discarding the chat it leaves behind

After: 5 passed, 0 failed.

Scope: the `?` sites after this window -- the two `manager.update(...)` calls
and the refetch -- orphan a chat too and are deliberately left alone. Each is
the session store itself failing, where the discard's own `delete_session`
would be failing for the same reason; what to do there is a separate question,
named in the test so the next reader knows they were seen, not missed.
…mpty

An absent `knowledge_bases` and a present-but-empty one are different
statements, and `workflow/runtime.rs` reads them differently.
`apply_knowledge_selection` returns early on `None` -- so every chat the
workflow starts re-derives a selection from the replaying machine and sees
every base -- and calls `set_visible_kbs(session, &[], PrimaryUpdate::Clear)`
on `Some({})`, hiding them all.

The modal emitted the key only when the selection was non-empty, so the one
gesture that says "no knowledge bases" was stored as the one that says
"whatever you have". `service.rs`'s own capture already got this right
(`an_empty_visible_set_is_captured_not_dropped`); this modal was the remaining
half.

The condition it replaces was asking whether the selection is EMPTY. The
question that decides this is whether the selection is KNOWN, which is a
different question, and conflating the two is what also made a failed read
indistinguishable from an empty one. Three independent ways to know, any one
enough: the user edited it (an edit is a statement, even when what it states is
an empty set); the generation carried the daemon's own block; or this modal's
own read landed and did not fail.

The third is read as `knowledgeBaseItems.length > 0 && !knowledgeSelectionUnread`,
and the first half of that is load-bearing: `knowledgeBaseItems` is set
unconditionally at the top of the read's `.then`, BEFORE the `if (!selection)`
bail, so a non-empty list proves the read ran. `!knowledgeSelectionUnread`
alone does not -- a read that never answers at all, because the effect was torn
down first, sets no flag and would have been captured as "nothing selected". A
machine with no bases therefore still captures nothing, which is what
`knowledge_bases_for_session` answers for that case too: there was no selection
to make, so there is no selection to state.

Fail-before (npx vitest run ...CreateWorkflowFromSessionModal.knowledgeCapture.test.tsx),
with the old emit rule restored:

    x stores a chat that has every base switched off as an explicitly empty selection
      AssertionError: expected undefined to deeply equal { default: null, visible: [] }

After: 31 passed across the new spec and the existing modal suite.

The new tests live in their own file because the existing suite's knowledge
block is `describe('when its selection cannot be read')`, whose whole fixture
is a read that FAILS; what is under test here is the opposite fixture, and the
two cannot share a `beforeEach`. Both are asserted side by side in the new file,
because the distinction between them is the thing that must not be erased.

One gap is stated in the file rather than left to be rediscovered: the same
gesture made through the picker is not covered. It reaches the identical emit
via `resourceEditsRef`, so the rule above decides it either way, but Radix's
Collapsible around Advanced options would not open from a fresh spec in jsdom,
and WorkflowResourcePicker belongs to another change in flight.
… from

`find_esbuild` walked a flat six ancestors looking for
`ui/desktop/node_modules/.bin/esbuild`, and six is exactly far enough to leave a
worktree: from `<repo>/.claude/worktrees/<name>/crates/biorouter-mcp` the sixth
step is `<repo>` itself. So a worktree with no install of its own silently
borrowed the MAIN checkout's bundler, and every esbuild-dependent test passed
locally for a reason CI can never have.

That is the hardest shape of "works on my machine" to notice, because nothing
was broken or missing: the tool the tests found was real and working, it was
just not in the tree under test.

The ascent now stops at the nearest ancestor holding a `.git` entry. Two details
that look like style and are not:

* the candidate is tested BEFORE the boundary, because an ordinary clone keeps
  `.git` and `ui/` in the same directory -- a bound that fired first would find
  nothing anywhere, which reads as "esbuild is missing" on every machine;
* the boundary tests for the `.git` ENTRY, not for a directory, because a git
  worktree's `.git` is a FILE. `is_dir()` would look right and stop at nothing.

Discovery moved into `esbuild_in_checkout(start)` so it can be tested at all:
`find_esbuild` reads `current_dir()`, process-global state a parallel test run
cannot set without racing every other test in the binary.

Fail-before (BIOROUTER_DISABLE_KEYRING=true cargo test -p biorouter-mcp --lib --
agent_drafter::bundle::tests::esbuild_discovery), with the flat walk restored:

    a_worktree_without_its_own_install_finds_nothing ... FAILED
    assertion `left == right` failed: a worktree borrowed the outer checkout's bundler
      left: Some(".../BioRouter/ui/desktop/node_modules/.bin/esbuild")
     right: None

After: 3 passed; the whole `agent_drafter::bundle` module is 50 passed, 0 failed.

CI is unaffected either way: the `test` job never runs `npm ci`, so no checkout
there has an install to find, and the jobs that do (`serve`, apps-smoke) run
from a repository root where `.git` and `ui/` are siblings -- pinned by
`the_root_of_a_checkout_is_searched_before_the_boundary_stops_it`.

Note on a neighbouring flake, so the next reader does not attribute it here:
`a_timed_out_esbuild_reaps_its_whole_process_group` fails intermittently on this
machine under load (its shim has one second to write a pid file). Measured on
pristine origin/main: 1 failure in 6 runs; with this change: 2 in 4. Same test,
same rate, not caused by this commit, and owned elsewhere.
…OCK_* is shared

Three corrections to guidance a future session would otherwise act on.

**The subagent count was 102; measured, it is 197.** A figure this wrong is worse
than none, because this repo's own habit is to assert "pre + N" against it -- and
against 102 a shortfall of ninety-five reads as a pass. Measured 2026-09-11:

    BIOROUTER_DISABLE_KEYRING=true cargo test -p biorouter --lib -- subagent
    test result: ok. 197 passed; 0 failed; 3708 filtered out

The line now carries the date and says to re-measure.

**Seven `cargo test` commands written as instructions could not run.** `cargo
test` takes ONE positional TESTNAME; a second is rejected before any test runs,
so `cargo test -p X --lib a b` exits 1 with a usage line and nothing is tested.
Five are in br71-execution-plan.md, whose own lines 430-437 explain this exact
hazard and then violate it five times further down; two are in
multi-kb-implementation-plan.md. Each gains the `--` it needed.

Left alone deliberately: five passages that quote the broken form in order to
warn about it, and two occurrences under docs/history/, which are records of what
was run rather than instructions to run it.

**`config_keys.rs`: the stale "cannot RAISE a tier" claim was already fixed**, and
correctly -- `tier()` really does follow the resolved endpoint through
`ucsf_gateway_tier`, so an off-site value demotes and deleting it restores
Private. The classification stands on a different argument, which the comment
already states: only the UCSF gateway's own host reads Private, so no write can
make an off-site endpoint look Private, and a raise is always a return to the
institution's gateway. Nothing to change there.

What WAS unstated is the five `BEDROCK_*` keys. The 2026-09-11 namespacing split
`VERSA_BEDROCK_ENDPOINT` and `VERSA_BEDROCK_REGION` out of the public card's
names precisely to stop one setup steering the other -- and these five were left
shared, read by `versa_bedrock.rs` (Private) and `bedrock.rs` /
`formats/bedrock.rs` (Public) alike. Sharing that a reader has to infer from an
absence looks like an oversight, so the reason is now written down: all four
retry keys are read in one place, `load_retry_config`, and become a
`RetryConfig`; `BEDROCK_OPERATION_TIMEOUT_SECS` is read in
`load_operation_timeout_secs` and becomes a deadline. None reaches the resolved
endpoint `tier()` asks about, and none takes part in signing or credentials. They
decide how patiently a request is retried and how long it may take -- not where
it goes or who it claims to be.

The row reasons are read as `_why` by both scans, so they are prose, not
assertions. Verified: `cargo test -p biorouter --lib -- privacy::config_keys`
2 passed, 0 failed.
…dels route that answers

Four defects in what a failed or unconfigured provider tells the user, plus the
endpoint whose name and behaviour disagreed.

**1. A turn no retry can fix no longer invites one.** `stop_notice` is reached by
three routes — the error is fatal, the budget is spent, or retries are off — and
it ended with "Please retry if you think this is a transient or recoverable
error" on all three. On the first that contradicts the sentence above it. Measured
on a vendor model rejection, where the text read *"Claude Code 2.1.235 does not
support this model; version 2.1.251 or newer is required."* and the frame beneath
invited a retry of a request that will fail identically forever. The advice now
follows `is_recoverable`, the same predicate the retry decision uses, so the two
cannot drift. The retried count stays on the retryable branch only: Biorouter
never retries a fatal error, so "already retried **it**" there would name a call
that never happened.

**2. Claude Code gets the catalog hint Codex has.** `unknown_model_hint`, mirroring
`codex.rs`'s. The case is stronger here, and `known_models` already documents why:
`claude --model X -p` **accepts an unrecognized id and only warns**, so a typo
neither fails loudly nor gets a pointer — and when the turn does end badly,
nothing in it names the model. Appended at the three structurally identical sites
(`classify`'s detail, the empty-response error, and `resolve_terminal`'s "produced
no result", the most anonymous failure this provider has). Empty for a listed
model, so a genuine outage on a known id reads as it always did.

**3. An unconfigured Versa Bedrock stops saying three contradictory things.** It
rendered:

    error: Error VERSA_BEDROCK_ACCESS_KEY_ID is not configured. Add it under
    Versa API Bedrock in Settings..
    Please check your system keychain and run 'biorouter configure' again.
    ...

Three defects in one line. The `..` came from `"Error {e}.{}"` appending an
unconditional stop to text that had one — `end_sentence` already solves exactly
this for the chat path and is now `pub` so there is one rule rather than two
spellings. The `error: Error` stutter came from that same format string, since
`render_error` already prints `error:`. And the three keychain lines answer "why
can't the store give me the key I saved?", which is the wrong question here and on
the sibling arm a contradictory one — the store-refused message says *do NOT
re-enter it* while `biorouter configure` says the opposite.

`keyring_advice` now takes the detail and withholds itself when the credential was
never set. That is a wording test, because the `anyhow::Error` leaving `from_env`
has already discarded the `ConfigError` that knew — so `CREDENTIAL_NEVER_SET` was
added beside the tier helpers and BOTH producers (`versa_bedrock`, `versa_azure`)
now format with it. One spelling, shared by construction rather than by hope.

**4. `GET /config/providers/{name}/models` answers with the models.** It returned
`[]` for nine of the twenty-three builtins, because `fetch_supported_models`
returns `Ok(None)` unless a provider overrides it and those nine do not — under a
name, a description and a `200 Models fetched successfully` that all promise the
model list.

**Ruling: change the behaviour, not the name.** Three reasons. The fallback is
already the right answer in two other places: the declarative-provider branch at
the top of this very handler returns `config.models` with no live fetch at all,
and the desktop's `fetchModelsForProviders` prefers `metadata.known_models` and
only falls back to this route — so the bug was that the correct rule lived
everywhere except the one place the CLI, an agent, or anything reading the
OpenAPI spec could reach. Renaming would regenerate `openapi.json` and the TS
client (the `Generated API contract` check) and still leave every caller holding
an empty list. And the shape is unchanged here — same path, params and
`Vec<String>` body — so nothing needs regenerating; `git diff --name-only` against
main touches no `openapi.json` and no `api/*.gen.ts`.

⚠ **Measure this with `known_models`, not by grepping `with_models`.** That grep
was the first instrument tried and it said 6 of 9, because
`ProviderMetadata::new` also takes a `model_names` list — `snowflake`, `zai` and
`sagemaker_tgi` looked catalogless and are not. Measured off live metadata
(2026-09-11), all nine declare one: azure_openai 12, aws_bedrock 7, versa_azure 9,
versa_bedrock 5, xai 9, snowflake 8, zai 8, xiaomi_mimo 4, sagemaker_tgi 1.

Fail-before, each with the fix reverted and nothing else changed:

    a_rejection_no_retry_can_fix_does_not_invite_one ... FAILED
      Ran into this error: ... version 2.1.251 or newer is required.
      Please retry if you think this is a transient or recoverable error.

    an_empty_answer_on_an_unknown_model_says_which_model ... FAILED
      Request failed: `claude` returned an empty response

    a_credential_that_was_never_set_is_not_a_keychain_problem ... FAILED
    an_unconfigured_provider_renders_one_stop_and_no_stutter ... FAILED
      left: "Error VERSA_BEDROCK_ACCESS_KEY_ID is not configured. Add it under
             Versa API Bedrock in Settings..\nPlease check your system keychain..."

    a_provider_with_no_live_fetch_reports_the_models_it_declares ... FAILED
      left: []   right: ["gpt-5.5-2026-04-24", ... 12 models]

After: biorouter --lib 98 passed across the four groups, biorouter-server
routes::config_management 20 passed, biorouter-cli session::builder 20 passed.
…kflow-kb-and-docs

# Conflicts:
#	crates/biorouter-server/src/routes/config_management.rs
…re two counts

Follow-up to merging 133 commits of main into this branch. Nothing here changes
behaviour a user can see; each item is a claim in the six commits that the merge
made false.

**The canonical matcher moved, so the port's three citations were dead.** PR #266
renamed `crates/biorouter/src/marketplace/search.rs` to
`crates/biorouter/src/catalog_search.rs`. `landing/marketplace-search.js`,
`landing/scripts/baam-search.test.mjs` and `landing/baam.html` each named the old
path as the module to read for the reasoning -- the one pointer a future reader
of a port most needs. All three now name the new path and say when it moved, the
way `ui/desktop/src/components/baam/search.ts` already does on main.

**And PR #266 did not only move it -- it changed a rule the port mirrors.** The
verbatim-phrase bonus became `written_in`: present as a substring AND not buried
inside a longer word at either end, with the boundary imposed only by an end that
is itself a word character. The port still asked `indexOf(...) !== -1`, so it had
drifted from the module it claims to mirror rule for rule.

Ported, with that module's own six assertions run against it. Worth being exact
about the blast radius, in both directions: the shelves call `matching`, which
collapses `rank` to a membership set, so **nothing on the page moves** -- a
visitor sees the same cards in the same order either way. That is also why the
drift could have sat there indefinitely, and why the rule is now pinned by a test
rather than left to the next reader to notice.

**`declared_model_names`' own doc contradicted the comment 100 lines below it.**
The route's `Ok(None)` arm still said "six of those nine ship a curated
`with_models(...)` catalog" -- the figure produced by the instrument the
function's doc-comment exists to warn against, which undercounts by three because
`ProviderMetadata::new` also takes a `model_names` list. In a commit whose subject
is copy that does not contradict itself, that is the wrong comment to leave
standing. It now reads nine, measured off `known_models`, and points at the
function that explains the measurement.

**The subagent count moved 197 -> 198 while this branch waited.** The whole point
of that CLAUDE.md line is that a wrong figure is worse than none, because this
repo asserts "pre + N" against it. Re-measured on the merged tree:

    BIOROUTER_DISABLE_KEYRING=true cargo test -p biorouter --lib -- subagent
    test result: ok. 198 passed; 0 failed; 3794 filtered out

so the line carries 198 and today's date, and now also records that it moved by
one in a single week -- which is the argument for re-measuring rather than for
trusting any figure written here, this one included.

Verified: node --test landing/scripts/baam-search.test.mjs 17/17 (the browser
half drives the real page, so the baam.html edit is covered);
baam-privacy-facet.test.mjs 14/14; build-registry.mjs --check reports all three
outputs current, and no registry datum is touched.
`clippy::string_slice` is `warn` in the workspace `Cargo.toml`, so under
`scripts/clippy-lint.sh`'s `-D warnings` the new
`every_failure_before_a_new_chat_is_returned_discards_it` was a hard error:
`&body[start..end]` panics when either bound falls inside a UTF-8 character.

It cannot here -- both bounds come from `str::find` -- but "cannot" is the
argument the lint exists to stop being made in a comment, so the bounds are taken
through `get` and the reasoning moved into the `expect`.

Measured on the merged tree:

    cargo test -p biorouter-server --lib -- routes::agent
    test result: ok. 63 passed; 0 failed

    cargo clippy --all-targets -- -D warnings \
        -A clippy::result_large_err -A clippy::string_slice
    exit 0, zero findings

⚠ Those two `-A` flags are NOT this branch's debt and must not be read as a
waiver of it. Both fire on files this branch does not touch, and pristine
origin/main was measured failing the same way:

  * `result_large_err` at routes/reply.rs:1863 (`authorize_steer`) -- reproduced
    on a detached origin/main worktree, `cargo clippy -p biorouter-server --lib
    -- -D warnings`, exit 101, same line;
  * `string_slice` at commands/agent.rs:480 (`&digest[..62]`) -- the line is
    byte-identical on origin/main and absent from this branch's diff.

Allowing exactly those two is what lets the sweep reach every other target and
show that this branch adds nothing. CI is unaffected either way: rust.yml's
clippy step is `cargo clippy --workspace --all-targets --locked` with no
`-D warnings` and is labelled informational, so both are warnings there.
@Broccolito
Broccolito merged commit dc52342 into main Sep 12, 2026
17 checks passed
@Broccolito
Broccolito deleted the fix/provider-copy-workflow-kb-and-docs branch September 12, 2026 08:33
Broccolito added a commit that referenced this pull request Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant