Repository navigation
Biorouter v1.89.0
Biorouter v1.89.0 release notes
Release date: August 10, 2026
Repository: github.com/BaranziniLab/biorouter
Biorouter v1.89.0 is the release where the app learns the difference between a model your
institution has an agreement with and one it does not — and refuses to send your private work to
the wrong one. Every model now carries a privacy tier and an institution; every chat, knowledge
base and clinical connector carries one too. A chat that has touched PHI will not run on a
commercial model, its history will not reach one through recall, ingest, branching or
summarisation, and a UCSF-approved model cannot reach another site's clinical connector, because
UCSF's BAA does not cover that flow.
Alongside it: Workspace Control, which lets Biorouter operate across your conversations
instead of only inside one, and turns delegated subagents from an opaque spinner into a tab you
can read, steer and stop. Machine-wide memory stops being injected into every chat's system
prompt. A chat can now use several knowledge bases at once with one of them marked primary. And
a knowledge digest that wrote nothing to your knowledge base now fails instead of reporting
success.
The second half of the release is about the app being dependable while you use it. A message you
type while Biorouter is working is no longer thrown away. Opening several chats at once no longer
wedges the window. Tearing a tab off now actually produces a window, with the tab following your
cursor onto the desktop. Tool output is treated as untrusted because of where it came from rather
than because it matched a list of suspicious phrases. Windows users get a knowledge-base export
that works at all. Built-in skills have moved out of the Skills counts into their own Contexts
section, so the number next to Skills is the number you installed. And several hundred strings
across the app were rewritten to say one thing per control, in plain words.
Existing settings, sessions, providers, extensions, skills, workflows, applications, knowledge
bases and usage history remain compatible. Two things do change for an existing install —
your chats are classified once on first launch, and memories previously saved as "global" stop
being read automatically. Both are described under
What changes for the chats you already have.
Downloads
| Platform | File | Install |
|---|---|---|
| macOS Apple Silicon | Biorouter-1.89.0-arm64.dmg |
Open the DMG and drag Biorouter.app to /Applications |
| macOS Intel | Biorouter-1.89.0-x64.dmg |
Open the DMG and drag Biorouter.app to /Applications |
| Windows x64 | Biorouter-win32-x64-1.89.0.zip |
Unzip and run Biorouter.exe |
| Linux desktop Debian / Ubuntu | biorouter_1.89.0_amd64.deb |
sudo apt install ./biorouter_1.89.0_amd64.deb |
| Linux desktop Fedora / RHEL | Biorouter-1.89.0-1.x86_64.rpm |
sudo dnf install ./Biorouter-1.89.0-1.x86_64.rpm |
| Linux CLI + daemon Debian / Ubuntu | biorouter-cli_1.89.0_amd64.deb |
sudo apt install ./biorouter-cli_1.89.0_amd64.deb |
| Linux CLI + daemon Fedora / RHEL | biorouter-cli-1.89.0-1.x86_64.rpm |
sudo dnf install ./biorouter-cli-1.89.0-1.x86_64.rpm |
| Linux headless any x86_64 | biorouter-headless-linux-x64.tar.gz |
Extract and run ./biorouter-headless serve |
The macOS builds are signed with the UCSF Developer ID, notarized by Apple, and stapled. Signed
updater ZIPs for both Mac architectures and latest-mac.yml ship alongside them, so macOS users
get the one-click "Restart & Update" path.
Private work cannot reach a public model
A chat that has touched private data will not run on a public model. Every session now
carries a classification — the most sensitive thing it has ever touched, a one-way ratchet — and
every model carries a tier. Binding a commercial model to a private chat is refused at the
database write itself, and the desktop app shows that refusal instead of silently switching your
default provider and telling you it worked. If a chat is already running when the mismatch turns
up, Biorouter first tries to put it back on the private model the session itself names, and only
refuses the turn if it cannot.
A private chat's history never reaches a public model by any route Biorouter offers. A public
model cannot read another chat's transcript through Chat Recall (both search and load are
filtered in SQL), cannot ingest another session's conversation into a knowledge base, and cannot
pick up a private chat's history through a copy, a diverge, an import, an auto-generated name, a
summary or a compaction. Branching a private chat over the API now requires an action from the
person at the keyboard — clicking Diverge in the app still works; an agent doing it on its
own does not.
UCSF OMOP and CDW are unreachable from a public model, and say so rather than looking broken.
The private BAAM extensions cannot be called, enabled, listed, or have their instructions read by
a public-capability chat, and a public chat cannot switch itself to a private model to get at
them. In the composer's extension picker a private extension under a public model appears greyed
with the reason, never omitted, so the state reads as a boundary rather than as a missing tool.
Knowledge bases are tiered, and only you can make one public. A base takes its tier from the
model that created it and ratchets to private when a private chat ingests into it; a
public-capability chat can neither read nor write a private base. Publicising a base is
user-only: the dialog states the blast radius (page and raw-source counts computed from the
actual tree) and requires you to type the base id. Privatising is a single click. Both write a
provenance entry, and the tier is visible on the base list and in the knowledge-base palette
before you act.
Badges everywhere, and a model switcher that refuses ahead of time. Session lists, the chat
header, the tab strip, the model chip, the provider grid, extension cards and the Add-extension
form all carry a Private/Public badge from one shared component. The model switcher disables
public models in a private chat and prints why on the row — including for custom, unlisted model
names — so you find out before the turn, not after a refusal.
You can declassify a chat yourself. A chat that merely ran a turn against a private endpoint
declassifies on a single click with a five-second undo. Anything else — a branch of a private
chat, a chat that used a private extension, a chat classified by the upgrade — requires retyping
the last six characters of the session id. Every declassification writes an audit row and is
reachable only from a human surface: the History view, or biorouter session declassify <id> for
chats History does not list. Declassification is one-way.
Before your first turn on a public model, Biorouter tells you what that model can reach. A
one-time dialog — with a short form in tooltips, the provider grid and biorouter configure —
states that the model is not HIPAA-compliant, not on-premise and not local, that it can read
files on this computer through the shell, and names the three things Biorouter does stop. The
copy is served from a single definition in the backend so the app, the CLI and the docs cannot
drift apart.
Settings → Privacy, with one switch that turns the whole thing off. Enforcement is on by
default. Turning it off takes a typed confirmation and is recorded in its own file next to
config.yaml rather than in config.yaml itself; it removes every refusal and every ratchet.
Badges stay visible and read "enforcement off", because a machine with the protection disabled
should not look identical to one with nothing private on it. The disclosure is not switched off
with it.
On macOS this switch cannot currently complete — it ends in an explicit refusal after about
a minute, and nothing is changed. See Known limitation on macOS. Windows and Linux
are unaffected.
Institutional affiliation: HIPAA compliance does not transfer
Until now the privacy controls asked only how sensitive something was. A UCSF-approved model
reaching another site's private clinical connector passed every one of those checks — even though
no BAA, DUA or IRB approval covers that flow. Both ends are private; neither agreement reaches
the other.
- Every model and every private extension now carries an institution, and a mismatch is
refused at tool dispatch, at resource and prompt reads, and when the agent tries to enable the
connector — with a message naming both institutions, the extension, and what would cross.
UCSF's Versa reaching UCSF's OMOP and CDW agents is unaffected, and a local model still reaches
everything private, since nothing is disclosed. - A cross-institution tool is flagged, not hidden. Where the tier gate hides a private
connector from a public model — the tool's existence is itself the secret — an institutional
mismatch does the opposite: both ends are private and you are entitled to know the connector is
there. The tools stay in the list, with a short marker on each saying whose data it holds,
whose model this chat is on, and that a call is refused. So the agent tells you what it needs
and why, instead of routing around a tool it cannot see. - Knowledge bases remember whose material went into them. Each base records the union of the
institutions whose data it has ingested and is readable only from a model covered by all of
them — a base holding both UCSF and another site's content is out of reach of either
institution's model, and only a local model gets through. Exported.brkbarchives carry the
owner set, and importing one stamps the base with the archive's institutions plus your own, so
an archive cannot be used to launder clinical content into another institution's chat. Bases
created before this update carry no institutions and stay readable as they were. - Recall and search are filtered by institution. A chat accumulates the institutions of every
private connector it touches. Recalling it, searching across chats, or ingesting it into a
knowledge base from a model covered by a different institution is refused rather than quietly
trimmed — and search filters before the result limit, so a properly covered model still gets
its full set of hits. - Delegation cannot hand another institution's model your clinical connector. A parent chat
could name a different institution's model for a subagent and pass along its inherited
extension list; enabling a connector is what spawns the server and pulls its credentials, so a
refusal at the first tool call was already too late. Mismatched extensions are now dropped from
the child rather than failing the delegation, and the parent is told which ones and why.
Limit worth knowing before you upgrade. The acceptance path — approving a specific
cross-institution flow for one chat — exists in the daemon, but no desktop dialog calls it in
this build. In practice the way past an affiliation mismatch is to switch the chat's model to
one covered by the right institution. Two paths also do not warn yet: the Agent Drafter app
catalog still reads the tier alone, and the knowledge-base refusal offers a model switch rather
than the grant. The marketplace registry carries affiliation data that nothing in the app renders
yet.
What this does not cover
Stated plainly, because a narrower guarantee misread as a wider one is worse than none.
There is no general filesystem read barrier in v1. A public model with shell access can still
read files a private chat wrote outside Biorouter's own session store — derived artifacts,
exports, anything on disk. What the gates stop is the agent-mediated path (asking Biorouter for
another session's content), the transcript path (a private history being sent to a public model
on a turn, summary, auto-name, copy, diverge or import), and the tier-escalation path (a public
chat acquiring private capability or reaching a private connector). The shipped disclosure says
exactly this, and it is why the disclosure ships at all.
Declassification is gated by a typed confirmation and proof that a person — not an agent —
initiated it. It is not gated by Touch ID, Windows Hello or a system password prompt.
What changes for the chats you already have
Your existing chats are classified once, on first launch, and you are told what happened. The
migration marks every chat whose last bound model was a private-tier provider — Versa (Azure),
Versa (Bedrock), Llama Server or Ollama — as private. Everything else stays public, including
chats with no recorded model at all, which on a real machine is usually the largest group. A
first-run notice quotes your own numbers: how many chats were classified, broken down by the
model that classified them, and how many are visible in History. It also names any enabled public
extension that declares clinical credentials, because nothing else will tell you. The notice is
shown only to installs the migration actually changed, and the migration never re-privatises a
chat you have declassified.
The backfill reads one field — the model a chat was last bound to — and never scans a
message. So a chat that ran on Versa and was later switched to a commercial model is marked
public even though its transcript contains private work, and a chat that ran on a commercial
model and was switched to Ollama for one turn is marked private even though nothing sensitive is
in it. Fixing either is quick: switch a chat to a private model and it is private from its next
turn, or declassify one that should not be. The full account is in Privacy tiers — what happens
to your existing chats, which ships with this release under docs/security/.
Memories previously saved as "global" stop being read automatically — see the next section.
This takes effect the moment you update, on memories already on disk.
Memory is no longer machine-wide by default
- Global memories no longer leak into every later chat. Anything a session stored with
is_global=trueused to be read at startup and pasted verbatim into the system prompt of every
subsequent session — any project, any model — with no tool call, nothing in the transcript, and
nothing shown to you. Global memories are now only indexed by category name; a session that
wants the contents has to callretrieve_memories, where you can see it. Project-local
memories in.biorouter/memoryare unchanged. - Reading or writing machine-wide memory now asks you first, in every permission mode. A
consent card names the category and the direction ("read the machine-wide store, category
clinical") and cannot be bypassed — it survives Auto mode's blanket approve, an "Always allow"
on the tool name, a read-only risk grade, and aPermissionRequesthook that says allow.
Asking to read the entire store at once is refused outright rather than asked about, since a
card that says "everything" is not something anyone can consent to informedly. Deletions get
three separate cards sized to what is lost: one memory, one category, or the whole store. - New: Settings → Chat → Memory lists both stores with their absolute paths — the
machine-wide one, and the project's when the window has one open — with each category's
memories, the model's own tags, and the file's size and last-modified date. Individual memories
and whole categories can be deleted behind a confirmation that quotes what is about to go.
Until now the only way to audit or reset the store was to edit text files by hand. - Forgetting one thing no longer deletes everything that mentions it.
remove_specific_memorymatched the given text as a substring, so "forget that I use black"
also destroyed "we use black for formatting" — one call could empty a category. It now removes
the one entry whose full body was named, and reports an error instead of silently succeeding
when nothing matches. - Limit: running the memory server standalone over stdio (
biorouter mcp memory, pointed at
a third-party MCP client) no longer offers machine-wide memory at all. Its global read, write
and delete calls fail and it is not handed an index of your global category names, because
nothing in that setup can put an operation to you for approval. Project-local memory works as
before.
Workspace Control: Biorouter across your conversations
A new built-in capability lets the agent operate the workspace itself: list your
conversations, open one as a tab, split pane or window, read another chat (as a summary, a
transcript, a list of its tool calls, or a subagent's spawn context), send it a prompt as a new
turn, a mid-turn steer or a silent note, change which extensions, skills, model or knowledge
bases it may use, wait for it to finish, and close its tab, its turn or its agent. So "run the
alignment in a second chat while I write the methods, and tell me when it's done" is one request
instead of a manual juggle.
It ships on, and it is a capability rather than an extension. Workspace Control is now a row in
Settings → Chat → Capabilities, not an entry in the Extensions view, and it is enabled by
default. Turning it off there removes the whole surface.
Read this before you upgrade if you had it off. Enabling it grants the full set of workspace
tools, including reading another conversation's transcript and sending it a prompt, not just the
subagent tool the delegation path uses. Because the built-in default is only consulted on an
install that has never saved an extension setting, a one-time migration turns Workspace Control on
for existing installs, and that migration does not distinguish an install that never had it from
one where you switched it off on purpose. If you want it off, check
Settings → Chat → Capabilities after your first launch on 1.89.0.
Anything one conversation does to another asks first and is labelled forever. Handing a chat
a process-spawning or networked extension, taking away a security-relevant one, switching its
provider, or adding a skill raises a confirmation card naming the target and the reason — in
every permission mode, including Fully Automatic. The target's tab gets a toast saying what
changed and who did it, and every injected message carries a stored provenance chip ("injected by
<conversation>", "direct user message", "spawn context") that survives reload, History and
JSON/YAML export, so you can always tell later which words in a chat were yours.
A setting for people who want nothing to appear on its own. Settings → App → Workspace →
"Never open tabs automatically" downgrades every tab, window or focus request into a notification
naming the conversation, and tells the model no tab was opened so it cannot claim otherwise. The
work still runs; open it from History when you want it. Off by default — and even with it off,
agent-opened tabs open in the background and never steal the composer you are typing in.
Without the desktop app the surface is smaller, by design. With a bare biorouterd and no
window, everything runs but nothing displays, and workspace_send_prompt mode:"turn" is refused
outright if your machine is in an approval permission mode, because a tool confirmation nobody is
watching would sit unanswered until it timed out. With no daemon at all, the inspecting tools
work and the ones that drive a session refuse by name — "requires the Biorouter daemon" — rather
than failing obscurely.
Subagents you can watch, steer and stop
- A delegated child now runs in its own background tab with a
subbadge, a link to the
conversation that spawned it, its spawn instructions and grants, and its transcript streaming
live. Type into that tab to correct it mid-run, or press Stop to cancel it. Closing the tab
does not kill the child, and if you did intervene the parent is told, so it does not treat the
child's self-report as an untouched run. - A subagent's approval prompt now finds a person instead of parking. When a delegated child
hits a tool that needs approval, the ask walks up the chain: an ancestor may answer it only if
it already holds that permission itself and only for that one call, and anything an
always-confirm rule raised is never answered by an agent at any depth. When it does need you,
the card appears in the conversation that delegated the work as well as in the child's own tab,
and answering in either place resolves it once. - History keeps subagent runs out of the way until you want them. Your session list stays a
list of your own conversations; turn on "Show subagent runs" and each child appears nested
under the conversation that spawned it, with a live marker while it is still working. Children
are now named after the task they were given rather than carrying a generic label. - An ambiguous task comes back as a question instead of a guess. Asked to "fix it and make it
consistent with the other one", a delegated agent used to pick a meaning, rewrite two files and
report success. A delegated agent cannot see the conversation it was delegated from, so a
reference that is obvious to you can be genuinely undecidable to it, and in an autonomous mode
acting was the cheapest way to settle it. Now: the delegating side is told to write references out
as real paths and names, and the delegated side is told to try a tool first, to prefer a
reversible option and say which it took, and to stop before anything it cannot undo. A task
returned as a question counts as finished rather than failed, so stopping is not penalised. The
answer comes back marked as blocked with the candidates named, and the conversation that delegated
it is told to settle the question or put it to you, rather than reading a question as a report of
completed work and doing the job itself. - A delegated agent's tab now lists what it actually has. A child's tab showed every extension
enabled anywhere on your machine rather than the ones it was granted, and separately advertised
workspace tools it was never allowed to call. The permission boundary held in both cases, so
nothing was reachable that should not have been, but the tab described a grant that did not exist
and the agent wasted a turn on tools that always refused. The header is also one band now, the
same height as an ordinary chat's, instead of growing with each grant. - Limit worth knowing. Delegation only happens in the Completely Autonomous permission mode,
in a session that is not itself a subagent, with at least one ordinary extension loaded, on a
non-Gemini model. That gate is the usual reason a request for a subagent quietly does nothing.
One chat, several knowledge bases
- A chat now uses a set of knowledge bases, with one marked primary. The knowledge-base
palette and the chat chip choose which bases this chat uses; a separate action marks one of
them the primary. The primary is where a knowledge write lands when the agent does not name a
base, the base a single-base read resolves to, and the subject of the Knowledge view — so you
can search across several bases without giving up a definite place for new pages to go. - A knowledge write with no base named never guesses. If the chat has no primary, the write
fails and lists the bases it could have meant, with the exact command to pick one, rather than
landing in whatever base was selected machine-wide. The CLI does the same, and prints "using
primary knowledge base <id>" before any--kb-less write. - Removing or deleting a base no longer strands a chat. Hiding the base that is currently
primary promotes another base in the same chat, written at that chat's own scope so other chats
are untouched. Deleting the base clears the pointer instead of inventing a replacement. - A chat can go back to following the machine-wide default. A "Follow the default
(<base>)" strip appears above the palette list while this chat holds a primary of its
own;biorouter knowledge active --session <id> --inheritdoes the same from the terminal. - One chat's choice no longer leaks into another. The daemon kept a single in-memory "active
knowledge base" shared by every session, so a chat that had not chosen its own base inherited
whatever the last chat had set. That cache is gone; resolution is per session. Relatedly, a
workflow now applies its whole declared knowledge-base set instead of only the first entry, and
an Agent Drafter worker profile's declared base is its own session's primary rather than your
machine-wide selection. - Limit: in the CLI only
knowledge activeis session-aware. A--kb-less CLI ingest still
names the machine-wide primary.
Knowledge digests tell the truth about whether they worked
- A digest that added nothing to your knowledge base now fails instead of saying it worked.
Ingest used to squash-commit its transaction even when the sub-agent had written no page, so a
provider that errored mid-request still produced a commit, a "Digest complete" in the Knowledge
view, and an empty knowledge base. Ingest now checks theknowledge/tree before committing and
fails with an error naming the reason, the tool calls that errored, and the model's own last
words — or the fact that it said nothing, which is itself the diagnosis. Your raw source is
kept, so you can retry. - The same false success is gone from workflows. The Meditation workflow writes through
platform__ingest_conversationinto the ingest macro, so it reported "Ingested 1
conversation(s)" with a commit hash while the Soul held only the raw transcript. Same defect,
same fix, now pinned by its own regression test. - The Knowledge view never claims a digest finished on the strength of a dropped connection.
If the backend stream ended before reporting a result, the panel used to show "Digest complete"
and clear the source off the staged list, taking the only evidence with it. An unreported
outcome is now a failure: you get "Digest error" explaining the connection ended mid-stream, and
the source stays staged. Pressing Stop yourself is still "Stopped before completion". knowledge query --saveandlint --fixstop reporting writes that never happened. A query
whose model answered in prose without saving a page still printed "saved as a page"; a--fix
pass with nothing to fix added an empty "lint autofix" entry to the change log. Both now check
the tree before committing. Neither call fails — only the false claim goes.- Ingest uses the model you configured, not a hardcoded Anthropic one. The ingest panel used
to fall back to a fixedanthropic/claude-sonnet-4-6whenever the base had no
default_model, so anyone on Versa, Azure, Bedrock, Ollama or the bundled Llama Server had a
direct-vendor model preselected and could dispatch a digest to a provider they never chose. It
now reads the same provider and model the chat composer uses, treats a base'sdefault_modelas
an override, and shows "No model configured" with digestion disabled when neither resolves. It
also waits for the selected base to be confirmed before running, and distinguishes "loading"
from "unavailable" (with a Retry) from "no model is configured".
A turn now belongs to the session, not to the request
Reloading the window, or navigating away and back, used to kill whatever the agent was doing. The
turn was tied to the HTTP request that started it, so when the request went, so did the work —
tokens already spent, answer never delivered.
A turn is now owned by the session. Closing or reloading the window leaves it running, and
reopening the conversation attaches to the turn in progress: the transcript catches up to
where the agent has got to and then continues live, with no visible replay seam. If you had
started typing when the connection dropped, what you typed is still there — a rejoin never eats a
pending message. Because the stream is no longer request-scoped, cancelling mid-turn now works
from anywhere the session is open.
A message you type while Biorouter is working is no longer lost
If you typed a second message while a turn was running, Biorouter queued it and showed it waiting.
When the turn finished, the queue emptied and the message was thrown away. Nothing was sent, no
error appeared, and the text was gone from the composer, so the only sign was an answer that never
came to a question you were sure you had asked. This was not an occasional race: the queued send
arrived while the previous turn was still unwinding, and the code that emptied the queue did not
check whether the send had been accepted.
Sending is now something that can be refused and retried, and every path that holds your words
honours the refusal:
- A queued message that cannot be sent yet is put back at the front of the queue and retried
shortly after. If it still cannot go, it stays in the queue and you get a notice, rather than
disappearing. - The first message in a brand-new chat was the worst case, because the composer never held a
copy to restore from. If it cannot be sent, it goes back into the composer with a notice and the
chat waits for you, instead of the tab clearing your text and opening an empty conversation. - Typing directly into the composer during a turn restores your text, and any images you pasted, if
the send is refused.
Several chats open at once no longer wedge the app
Opening enough chats made Biorouter stop responding: the composer would spin on a turn that was
never sent, while the daemon behind it was idle and healthy. Closing a few tabs fixed it, which
made it look like a problem with turns rather than with tabs.
Each open chat holds one long-lived connection so its transcript can follow along live, and a
browser engine allows only about six connections to one address, shared across every window of the
app. Past that limit the interface could not make any request, including the one that sends
your message. Two chats started at the same time in two windows crossed the line where the same two
started one after another did not, which is why it looked random.
Biorouter now caps how many chats follow the live tail at once. A chat over the cap still shows
its full conversation; it just does not stream until a slot frees up, and it no longer degrades
into a once-a-second poll while it waits.
A second, unrelated freeze is also fixed. Starting a chat kicked off extension loading in the
background and answered immediately, so a turn beginning in that window ran with only part of its
tools loaded. Turns now wait for extensions to finish loading, and that wait is bounded at 30
seconds, so a single extension that never finishes loading cannot park the turn forever. This
matters on ordinary installs, not exotic ones: an extension that needs credentials you do not have
is exactly the case that used to hang.
Tool output is treated as untrusted because of where it came from
When a tool returns text, that text is very often written by someone else: a web page, a paper, a
row in a database, a file from a collaborator. If it contains instructions, the model reads them in
the same breath as your request.
Biorouter used to flag this only when the text matched one of seven patterns, which made the
protection exactly as good as the list. It was defeated on the first attempt in testing, by hostile
instructions written as a polite checklist. Nothing matched, so nothing was flagged, and the model
declined only because it judged the request odd, which is not a control.
Every successful tool result is now marked as untrusted, whatever it says, because the reason to
distrust it is where it came from and not what it looks like. There is no list of trusted tools,
since even the built-in ones return other people's text. The pattern list survives, but only to add
a visible warning above the marking rather than to decide whether to mark at all.
You will not see the marking. It is for the model, and it is removed everywhere a person reads
tool output: the chat transcript, saved and shared conversations, the terminal, session attach,
editors connected over ACP, subagent task instructions, and text on its way into a knowledge base.
The warning line still shows, because that one is meant for you. JSON and YAML exports keep the
marking on purpose, so that what you export matches what the model was given.
The cost is about 19 extra tokens per tool result, with the policy written once into the system
prompt instead of repeated on every result.
Two detection fixes ride along. A column of numbers is no longer reported as a phone number, which
had been firing on line counts, identifiers and any numeric output. And instructions hidden in
code-execution output are caught again, after a text-encoding difference had been hiding them.
Tabs can be torn off into their own window, and merged back
Drag a tab out of the strip and release it over empty space to open it as its own window. Drag it
onto another Biorouter window's tab strip to merge it in, with a caret showing exactly where it
will land. Escape during the drag cancels it; an interrupted gesture no longer commits one.
The dragged tab now follows your cursor out of the window. Until this release it could not: a
window can only paint inside its own frame, so the ghost stopped dead at the edge and the gesture
looked like the tab being swallowed rather than like one about to become a window. The ghost is now
a real window of its own that flies under the cursor anywhere on the desktop, including off the
side of a display, and it lands exactly where it was drawn, because the ghost and the window it
becomes are positioned by the same calculation.
Pressing a tab now registers, including on a window with one tab. The tab strip used to hand
macOS a titlebar-drag region computed from the current tab list, and macOS routes a click using the
region set from the last time the window painted. On a window with a single tab the whole strip
counted as titlebar, so the press moved the window instead of picking up the tab, and on a
just-opened tab the press was lost for as long as the window was busy. The strip now declares one
fixed region regardless of how many tabs are open, with a dedicated handle at the right end of the
band for moving the window. Merging a lone tab into another window works as a result, which the old
behaviour made impossible.
Three more things worth knowing:
- A window's only tab still does not tear off into a new window. There is nothing to gain from
turning a window into an identical window, so the gesture is refused. - A tab you have not sent anything in yet cannot be torn off. It has no chat behind it, so the
new window had nothing to open and the tab disappeared from the window you dragged it out of.
The drag is now refused and the tab stays where it is. - A running turn no longer locks its tab in place. Earlier designs blocked moving a tab with
work in flight; since a turn now belongs to the session, the tab can move freely and the work
follows it.
Tabs the agent opens land in the window that asked for them. A chat spawned from a conversation
now opens beside its parent rather than in whichever window the operating system last considered
focused, and a new chat window is positioned relative to the window that requested it.
Built-in skills are now Contexts, and the Skills count is yours again
Biorouter ships five skills of its own: what Biorouter is and how its pieces fit together, how to
work on its codebase, how to build an extension, how to write a skill, and how to keep a personal
knowledge base current as a chat goes on. They were counted and listed as Skills, so a fresh install
claimed skills you had never installed, and the number next to Skills never matched what you had
added.
They are now Contexts, with their own section in Settings → Chat. They are on by default,
and each can be switched off individually. The Skills view, the composer's skills popup and both the
desktop and terminal skill counts now show only skills you installed, so those two numbers agree
with each other and with what you did.
Windows fixes
Three of these will be visible immediately to anyone running Biorouter on Windows.
- Knowledge-base export works. Exporting any knowledge base to a
.brkbfile failed every time.
The exporter held a lock on one of the base's own files and then tried to read it, which Windows
refuses. Nothing was wrong with your knowledge base, and no export was possible until now. - Archives written on Windows are readable elsewhere. Entry names inside exported archives were
written with backslashes, so an archive produced on Windows opened on macOS or Linux as files with
backslashes in their names rather than as files inside folders. This had always been wrong; the
export failure above was simply preventing anyone from getting far enough to see it. - Memory categories with punctuation no longer fail silently. A category name becomes a filename,
and names Windows cannot open were accepted and then failed with an unhelpful error. Category names
are now checked against what every supported platform allows, on every platform, so a store written
on one machine opens on another.
Two more: enabling a skill inside a pack (biorouter skill enable pack-a/tool) works on Windows,
where the name was previously printed in a form you could not type back; and --no-session runs no
longer leave their temporary database behind.
Behind these, Biorouter's Windows test coverage was largely fictional. The test run stopped at the
first failing crate, so four of the test suites had never run on Windows at all, and the shell used
on the Windows runner does not stop on a failing command, so two more had been reporting nothing for
as long as they had existed. Both are fixed, and the suites that had never run passed on their first
real run.
The interface has one design system
The desktop app has been rebuilt on the Astryx design foundations:
- Figtree is the interface typeface, replacing the per-platform system stack, so the app looks
the same on macOS, Windows and Linux. - Controls, dialogs, toasts, list views and the sidebar share one size scale, one radius
vocabulary and one set of interaction tints instead of each surface carrying its own; every
dialog now comes from a single shell. - The sidebar rail is compact, and the chat transcript is quieter — metadata no longer
competes with the conversation. - The bands across the top of the window are shorter and aligned. The sidebar's title bar, the
chat header and the artifact panel's tab strip are one height, set in one place, so they meet at a
single continuous edge instead of drifting apart. - The composer is the input and nothing else. The box that used to wrap the whole composer is
gone; the only bordered object is the field you type into, which reclaims about 80 pixels above it
and gives 32 of them back to the conversation. - Long messages fold. A message over 10 lines or 600 characters collapses to a fixed height
behind a fade, with a control that says how much is hidden. This applies to what you send, and it
is based on length alone, so nothing is hidden because of how it was typed or pasted. - Notifications moved to the top-right corner. They used to appear part-way down the chat pane,
where a report about extensions loading landed in the middle of the conversation. - Three theme families ship as before (Parchment, Alma Mater, Roche Limit), and each is audited
for contrast on every build: 330 assertions across three families in both light and dark. - All three families now share one set of surfaces and greys. What distinguishes a family is its
ink, its accent and its status colours, not its backgrounds. This also fixed a real defect: in
three of the six family and mode combinations, the canvas and the surface one step above it were
the same colour, so a card meant to sit above the page sat exactly on it. - The sidebar toggle works again when the sidebar is collapsed on a chat route (issue #74).
An Electron drag region covered it, so the button looked live but the click moved the window
instead. Drag regions are resolved by the OS before the DOM is consulted, which is why no
amount ofz-indexhelped.
Command line
biorouter session attachjoins a live session, prints its transcript from the top and lets you
steer it (--ofpicks a subagent of a given parent,--read-onlywatches without
participating).session send,session watchandsession cancelcover injecting a prompt,
waiting for a turn to end and stopping one;session list --subagentsshows child runs nested
under their parent with live/done marks. The four that drive a live turn needbiorouterd
running; the listing works off disk.- The session banner gains a
privacyrow showing the chat's classification (not the bound
model's tier). A chat refuses before the provider is created and names which of the four
precedence slots picked the public model; the refusal lists the private models this install has
and the exact command to re-run on one. biorouter sessionaccepts--provider/--model, and a workflow pinning a public model is
refused at load time, naming the workflow.biorouter session declassify <id>declassifies a chat History does not list.
Elsewhere
- The marketplace shows which extensions are private, in the app and on the website. The
registry carries an explicit privacy tier per extension; the BAAM shelf gains a badge on every
card and a Private/Public filter, the docs table gains a Privacy column, and the in-app install
modal states the badge the extension will actually get. The app keeps a last-good copy of the
registry, and a fresh fetch can only raise an extension's tier, never lower it. - Tool output reached the model differently depending on which provider you used. Tools can mark
parts of a result as being for you, for the model, or for both. Four providers filtered on that
mark and two of them asked the wrong question, while Anthropic, the OpenAI Responses path and
Snowflake did not filter at all. The same tool result therefore reached the model on some
providers and not others, and one OpenAI account could behave differently depending on which model
name it used. There is one rule now, applied everywhere. - Reading part of a file actually reads part of it. Asking the file viewer for a line range
showed you the range but sent the model the whole file, so a ten-line request against a large file
was billed for the whole file, and the protection that stops a very large file being opened
without a range was defeated by complying with it. - The terminal shows tool output again. In its default configuration the classic command-line
renderer filtered out every tool result, because the minimum priority it shipped with was set
above the priority every tool actually produces. Both documentation pages had the correct value;
the code did not. biorouter projectsrecovers from a damaged file instead of failing forever. A partly-written
projects file made the command fail on every run afterwards, naming the file but never saying it
could be moved aside. It is now written atomically, and a damaged one is set aside with a message
saying where it went.- A fresh install gets an error, not a crash. Running
biorouter sessionorbiorouter run
before configuring a provider ended in a Rust panic and a backtrace, and quietly left an empty
"CLI Session" row behind on every attempt. - The greeting is back. The rotating opening lines return, and the unrolling animation plays
whenever you arrive at a new chat. It respects your system's reduced-motion setting. - A scheduled job that has been failing now looks failing. The Schedules list and detail views
show a "Failed" badge and the last error, suppressed while a run is in flight. A scheduled run
mints a fresh session each tick, so before this a job that had never once succeeded was
pixel-identical to a healthy one and the only evidence was a line in the daemon log.
Interface fixes in this build
Reported against the release candidate and fixed before it shipped.
- A chat's icon now says what kind of chat it is. Every row in the sidebar, in History and on
the tab strip used to draw the same speech bubble, with a separate dot for a private chat — one
bit of information, and none about what any row actually was. Six kinds now have their own glyph:
chat, branch, sub-agent, app, scheduled run, terminal. A private chat is a padlocked bubble
rather than a tinted one, so the marking survives for anyone who cannot separate the two inks.
The sub-agent tab'ssubtext chip is gone with it. - A renamed branch still reads as a branch. The sidebar could only tell branches apart by their
default name, so anyone who renamed one lost the marking. On the machine this was found on, one
branch in five had been renamed. - The model chip states the model's tier, not the chat's. It read
gpt-5.5-… (Public chat) (Affiliation: UCSF)— the chat's tier standing between the model's name
and the model's institution, with the model's own tier stated nowhere. A UCSF Versa model
therefore looked public in every chat that had not yet run a turn. The same mismatch greyed out
institutional extensions the daemon would have dispatched without complaint. - The extension count means one thing. The load notification counted a different set from the
composer's extension menu — two definitions of "ships with Biorouter", five entries apart — so the
two numbers disagreed on sight. Both now count the extensions you added; a built-in capability
that fails is reported by name instead of inflating the ratio. - A wider window buys content, not margin. The reading measures were flat pixel caps, so
dragging the window wider added empty space either side. They scale with the pane now, with the
old values as the floor — nothing narrow changes. - Screenshots ask for the permission they need. The screen-capture tools checked macOS's Screen
Recording permission but never requested it, so they told you to enable Biorouter in a list
Biorouter had never been added to. They now request it, which is what puts it there. - The in-app terminal works in a packaged build, and Privacy has moved out of its own tab into
App, between Configuration and Workspace.
The app says one thing per control, in plain words
A sweep over every string a user can read found the same faults repeatedly: two names for one thing,
a tooltip that restated its own label, and jargon from a developer's terminal in a tool aimed at
clinicians. What changed, and what it means in practice:
- One word for a chat. 169 strings that said "session" or "conversation" now say chat, matching
what the interface has always called it. - One word for stopping a job. "Job killed" and "Job cancelled" are both "Job stopped".
- Branch, not Diverge. The control that starts a new chat from an existing one is called Branch
everywhere it appears. - The two app lists have two names. The adjacent sidebar rows that were a word apart are now
Built apps and MCP apps, named after what each one actually lists. - One description per built-in extension. Auto Visualiser was described four different ways
across the app, the marketplace and the docs; Tutorial and Knowledge three ways each. Each now has
one sentence. - Rules stop naming one institution. Seven strings explained a general rule using UCSF as though
it were the only case. They say "your institution" now, which is what the rule means. - Toast titles and error text. 54 toast titles were half Title Case and half sentence case; they
are sentence case throughout. Error messages no longer open with "Please", and confirmations no
longer say "successfully". - Two controls that described themselves differently to different people. A screen reader
announced "Kill [name]" where a sighted user read "Stop the running job", and two more controls
had labels that disagreed with their tooltips outright. Extensions' settings button could be
reached by keyboard but never became visible, and was unreachable entirely on a narrow window.
Privacy wording that was wrong, not just clumsy
Four corrections here are factual, and one of them was actively misleading:
- "Private" no longer means "hosted at your institution". Six refusal messages explained a
private model that way. A model is also private when it runs on your own machine, so on an
install whose only private model is a local one, the refusal sent you looking for an institutional
model that does not exist on your computer. Refusals now say what private means and list the
private models you actually have. - A panel no longer contradicts the disclosure printed above it. The privacy switch's own text
claimed a public model "can't reach your knowledge bases through the shell". There is no general
filesystem barrier in this release, and the disclosure four elements higher on the same screen says
so. - Two strings promised a confirmation dialog that may not appear. They said the operating system
will ask you to confirm. macOS can approve from a recent authentication without showing anything,
so they now say it may ask. - Capability and permission are no longer conflated. "Only private models can read this chat"
became "Biorouter only lets a private model open this chat". The first states a property of the
world; the second states what Biorouter does, which is the true and narrower claim.
Breaking changes
- The
subagent_statustool is gone. Its jobs moved to the workspace tools —
workspace_listfor what is running,workspace_read_conversationfor what a child did,
workspace_watchfor waiting on completion,workspace_closefor stopping one. Prompts, skills
or workflows that namesubagent_statusby hand need updating; a build gate now fails if the
model's own instructions mention a retired tool name. - Branching a private chat over the API requires a user action. The desktop Branch button
is unaffected; an automated caller reproducing the old request will be refused. - Workspace Control is enabled by default, including on existing installs, and has moved from
the Extensions view to Settings → Chat → Capabilities. See the Workspace Control section above for
what that grants and how to turn it off. - Four provider modules are gone:
claude_code,codex,cursor_agentandgemini_cli. Each
drove another vendor's command-line tool as a subprocess. None of them has been selectable for
months, so no working configuration changes, but their configuration keys are no longer read. Cost
figures already recorded against them are unaffected, and one of them is now more accurate: usage
recorded undergemini_clihad been priced with Google's per-token rates, which was wrong for work
billed through your own subscription. - The provider list in Settings hides nothing. The filter that used to hide these four is gone
rather than left in place over an empty list.
Internal hardening
The privacy boundary is enforced by mechanism rather than convention: the model bind is a single
conditional SQL statement, not an if followed by a write, so a concurrent ratchet cannot
interleave; the running-turn gate is re-asserted in the provider accessor so auto-naming,
compaction and the stall judge cannot slip past it; one comparison function decides the
affiliation axis for both spellings of the gate; extension tiers are re-derived from the
generated registry rather than stored, so a stale tier cannot outlive a registry change; and the
registry, the Rust private set and the desktop fallback snapshot are one generation with a drift
check wired into check-everything and the Pages deploy. Institution ids are interned and
compared by normalised content, so "UCSF", "ucsf" and " ucsf " are one institution. One
proof-of-user mechanism covers every human-only operation, with repo-walk audits capping where it
may be constructed. The master switch moved out of config.yaml into its own file with exactly
one writer, so the environment-variable read path can no longer turn protection off.
Elsewhere: a per-session event bus is now the spine for turn streaming, with the workspace socket
hardened against cross-site WebSocket hijacking and bounded on ids and frame sizes; subagent
lifecycle, approval delegation (bounded depth, cycle-proof, never delegating an always-confirm
ask) and the renderer's observer stream were made correct under failure; every memory-store
mutation holds a cross-process advisory lock for the whole read-modify-write and lands via
temp-file rename, and deletes are compare-and-set against a digest; inspection verdicts merge
into a strict Deny > RequireApproval > Allow lattice regardless of registration order; the three
hand-rolled session-copy builders became one helper; knowledge-base selection writes are
all-or-nothing under one root lock; and the empty-digest guard compares the knowledge/ subtree
rather than the whole tree, because three sub-agent tools move the tree without writing
knowledge.
Upgrading
macOS users on 1.88.x can take the in-app update. Everyone else: install the artifact for your
platform above over the current version.
The first launch after upgrading runs a one-time session-store migration and shows the privacy
notice described above. No configuration changes are required, and no action is needed unless you
want to correct a chat the classification got wrong.
The five operations that need you to confirm in person now work on all three platforms: turning
enforcement off, granting a cross-institution flow, setting the mixing policy, declassifying a chat,
and exporting a private session. On macOS the prompt is raised by a signed helper application that
ships inside the app bundle, which is what makes the dialog appear; this was verified on the
packaged, notarized build, with enforcement turned off in 3 seconds after a Touch ID approval and
turned back on with no prompt, since that direction is the safe one.
Biorouter also checks who answered. Approval used to be a file containing a secret, so anything
running under your account could have written an approval without a dialog ever appearing. The
helper now answers over a socket and Biorouter asks the operating system which process is on the
other end and whether it is signed by UCSF. On Linux the same class of hole was found and closed:
the authentication tool is now run from an absolute path, so a program earlier in your PATH cannot
stand in for it. Neither check makes forgery impossible for someone who already controls your
account, and they are not offered as though it did.
One caveat worth knowing on macOS. The operating system may approve one of these prompts from
a recent authentication rather than showing you a dialog. Biorouter asks it not to, but that is a
request the system can decline to honour, so an approval you did not see may represent an
authentication you performed a moment earlier. This is why the wording in the app says the system
may ask you to confirm rather than promising a dialog.
Verification
Measured against this build, not inherited from a previous one.
| Gate | Result |
|---|---|
| Desktop suite | 2,690 tests, 0 failures across 276 files |
| Themes | generated artifacts current; 330 contrast assertions across three families in both modes |
| Versions | all six version strings agree, checked mechanically |
| Continuous integration | all four workflows green on macOS, Windows and Linux at the release commit |
Windows is a real gate for the first time this release rather than a nominal one. The test run used
to stop at the first failing crate, so four suites had never executed on Windows at all, and the
shell on the Windows runner did not fail a step when a command inside it failed. With both fixed,
the suites that had never run passed on their first real run.
Every shipped artifact was executed, not merely inspected: both macOS apps pass Gatekeeper as
notarized and stapled, the Intel app's bundled backend is confirmed x86_64, and the dmgs, the
Debian and Fedora packages, the CLI-only packages and the headless tarball each start their CLI
and daemon — the Linux desktop packages under Xvfb, the headless tarball answering its health
endpoint — in clean debian:bookworm-slim and rockylinux:9 containers.
The build was then driven as a user would drive it, with real operating-system input rather
than synthetic events, because a signature and a green suite establish that software was built
correctly and not that it works. That pass found three defects no gate could see — a tab merge
that closed every window involved, a privacy control that hung indefinitely, and a window reload
that lost a transcript the daemon had already finished and saved. All three are fixed in this
build, and the pass was repeated against it.
Three guards were added for defect classes that had shipped more than once each: a server test
binary that could reach a developer's real session database, a colour token declared but never
mirrored into the utility layer (which renders silently wrong), and an enforcement clause
re-derived instead of asked for. Each was proven by deliberately breaking it and watching it fail.
Related documentation
- Privacy tiers — the design, the gates, and what the guarantee
does and does not cover - Workspace Control extension
- Permission modes