Desktop downloads (built in GitHub Actions from release commit 35ad699f6f760d90233178b63517241573520cbe):
The Mac build is ad-hoc signed and is not Apple-notarized. Both desktop builds identify themselves as 0.0.13-internal.g35ad699f6f76. Checksums and source/build details are attached.
Fresh desktop setup installs its runtime before sign-in
Desktop setup installs local software before asking for an AI connection. Fresh Mac Podman
installations no longer duplicate stack port bindings. The shared installer downloads verified
prebuilt Bun binaries without developer tools; Linux setup also installs its native Podman runtime
and certificate bundle. (#604)
Ctrl+B shows and hides the sidebar on a layout that does not write Latin letters
The sidebar toggle's tooltip names Ctrl+B, or ⌘B on a Mac, and the shortcut was recognised by the
character the key writes. On a Russian or Greek layout the B key writes "и" or "β", so the shortcut
did nothing there. It is now read the way the app's own shortcuts read it since Shift+N was fixed
for the same layouts: from the physical key when the layout writes a character outside ASCII there.
Scrolling a Bot's browser no longer scrolls or zooms the page around it
While somebody drives a Bot's browser, a turn of the mouse wheel over its screen is sent to it, and
the screen was meant to keep the wheel from also acting on the app. React attaches its wheel handler
as a passive listener, which a browser does not allow to do that, so the wheel scrolled the frame
holding the Bot's screen along with the Bot's page, and Ctrl with the wheel zoomed the app. The
wheel is now handled by a listener that can hold it, so it reaches only the Bot's browser.
Typing into a Bot's browser no longer triggers the app's own shortcuts
While somebody drives a Bot's browser, every keystroke is sent to it. The app's shortcuts listen for
keystrokes too, and they heard each one first, so typing a capital N into the Bot's browser, as in
"New York", started a new chat and took the person away from the Bot mid-word, and Ctrl+B there
also showed or hid the sidebar. A keystroke sent to the Bot's browser now reaches only the Bot's
browser. Escape still closes the view, and the paste shortcut still pastes.
The Python LangGraph Bot on Anthropic answers after a skill was picked
A skill somebody picks reaches the Bot as a system message just ahead of their message, and it
stays in the conversation. Anthropic takes one system prompt, and its LangChain integration refuses
a system message that comes after a turn of the conversation, so with BOT_PROVIDER=anthropic
every run in that conversation failed from then on, before the model was asked. On Anthropic the
Python LangGraph Bot now puts every system message ahead of the conversation, in the order given,
where the integration joins them into its one prompt. Runs on OpenAI and Gemini are unchanged.
The LangGraph Bot answers on Anthropic and Gemini
With BOT_PROVIDER=anthropic or BOT_PROVIDER=google, the LangGraph Bot answered nothing: every run
ended in an error before the model was asked. Those two providers take one system prompt, at the top,
and the Bot handed its model several: its own guidance, the context the app sends, and the
coworker's standing role, which the server puts at the head of every run. The provider's LangChain
integration refused the second one. On those two providers the Bot now folds them into one system
prompt, in the same order, and a skill picked for a message joins it. Runs on OpenAI are unchanged.
The proof-of-concept Bot reads a model name set with whitespace around it
A BOT_MODEL carrying a leading space reached this Bot as it was written. Its startup check refuses
gpt-5.6-*, which rejects function tools on the API this Bot speaks, and that check matches from the
start of the name, so a padded one walked past it: the Bot started, reported healthy, and answered
nothing at all on the first turn that used a tool. An empty value was read the same way and asked the
provider for a model with no name. Both now fall back to the default, and the name is used trimmed.
.env was never a route to this — Compose, Bun and the desktop shell each strip the value first — so
it reached only deployments that set the variable directly, such as a Kubernetes manifest or
docker run -e.
Pasting into a Bot's browser works on a layout that does not write Latin letters
While somebody drives a Bot's browser, Ctrl+V or Cmd+V is left to the local page so its paste event
can send the clipboard text across. The shortcut was recognised by the character the key writes,
and on a Russian or Greek layout the V key writes "м" or "ω", so the keystroke went to the Bot's
browser instead and nothing was pasted. The V is now read the way the app's own shortcuts read it
since Shift+N was fixed for the same layouts: from the physical key when the layout writes a
character outside ASCII there.
Reading a large file from Google Drive no longer downloads all of it
read_file_content shows a Bot at most the first 20,000 characters of a file, but it downloaded the
whole file and held it in memory before cutting it. A 200 MB text file raised the server's memory by
more than 600 MB for one call, on the process that serves everybody else. The connector now stops
reading once it has more than it can show and cancels the rest of the download. What the Bot is shown
is unchanged, except that the note on a cut file no longer gives the file's full length, which is no
longer known.
A Bot reads the text an MCP server returns as an embedded resource
A tool result can carry an embedded resource, and a text resource holds content, such as a file the
server read. The MCP connector passed text parts to the Bot and named every other part, so a text
resource reached the model as [resource] and its contents were dropped. GitHub's MCP server answers
get_file_contents for a text file this way: the Bot was told the download worked and never saw the
file. The text of an embedded resource is now passed on like a text part. A resource that carries
bytes is still named.
The Google Drive connector reaches files in shared drives
Drive leaves shared drive items out of any files.get or files.list request that does not say it
supports shared drives, and none of the connector's requests said so. A document the person could
open in a shared drive was "File not found" to get_file_metadata and read_file_content, and never
appeared in search_files or list_recent_files. Those requests now say they support shared drives,
and the listings ask for shared drive items. Listings keep Drive's default user scope rather than
searching every shared drive.
Google Drive search and recent files leave out what is in the trash
Drive's files.list returns trashed files unless the query excludes them, and neither search_files
nor list_recent_files did. A document somebody had thrown away came back to the Bot as a match or
as a recently changed file, with nothing in its line to say it was in the trash, so the Bot could
answer from it as though it were current. Both now ask Drive to leave the trash out. Reading a file by its
id is unchanged.
New conversations are still named once some older ones could not be
Every pass of the job that names conversations offered at most twenty of those still without a name.
A conversation it had tried and could not name, for example because the model answered with no text
or the conversation opened with only an attachment, keeps no name, so it stayed among those twenty
and took a place on every pass, although offering it again did nothing. Once enough of them had built
up, a new conversation could miss out on every pass and keep showing its plain name in the sidebar.
A pass now skips any conversation that the job already holds work for, so new ones get a place. One
it could not name is still tried again later, as before.
An offboarding one app refuses still records the apps that answered
Removing somebody withdraws each brokered account they connected. When the broker refused one of
those apps, the whole act stopped before anything was recorded: apps already withdrawn at Composio
kept their composio_connections row and left nothing on the trail saying the account had ended,
and the retry that #574 made the recovery then asked again, was told there was nothing to withdraw,
and wrote vendorRevocationRequested: false about a withdrawal this deployment had asked for and
got. Each app is now asked, recorded with the answer it actually gave and its row removed, and only
the apps that were refused are left standing for the retry. The act still fails and still answers
500, so a refusal is as loud as it was.
Paging the audit trail no longer skips rows written in the same millisecond
GET /api/admin/audit-events hands out a nextCursor built from the last row's timestamp, which the
server read at millisecond precision while PostgreSQL keeps microseconds. The cursor therefore named a
moment just before that row, and the rows the next page should have started with, written earlier in
the same millisecond, were on no page at all. Anything that walked the trail page by page could miss
them without any sign of it. The cursor now carries the row's full timestamp. A cursor issued before
this change still reads.
The New chat shortcut works on a Russian or Greek keyboard layout
Settings lists New chat as Shift+N, and the app matched the character the keystroke wrote. A layout
that writes another script has no key that writes an N: Shift and the N key write "Т" on Russian and
"Ν" on Greek, so the shortcut never fired there. When the character is not ASCII, the app now reads
the physical key instead. A layout that writes Latin letters, such as Dvorak, still goes by the letter.
Enter that confirms a typed character no longer saves a name, a rule or a wizard step
Japanese, Chinese and Korean are typed through an input method, where Enter confirms the character
being built. In three fields that Enter also acted: editing a coworker's name or title saved it with
the character still unconfirmed, the new-coworker wizard moved on to its next step, and a boundary
rule was saved into the policy in force. Those fields now wait for the character, the way the chat
composer already does, and an ordinary Enter works as before.
A built-in coworker can be edited where the deployment's own Bot is on localhost
A coworker created as Built in is stored pointing at the managed Bot's address. Editing its name,
title, role or visibility sent that address back as though somebody had typed it, and the server
checks an endpoint it is sent the way it checks a person's. scripts/start.sh puts the managed Bot on
http://localhost:4201/ag-ui, which that check refuses unless private hosts are opened, so every edit
failed with "That address is inside this deployment's own network, so an agent may not live there."
The dialog now leaves a built-in coworker's address where it is stored. A coworker somebody hosts
still sends its own endpoint, as before.
A long page's text is cut between characters, not through one
A navigation hands the Bot the first 6000 UTF-16 code units of the page's readable text. When that
limit fell between the two halves of an emoji, the Bot was handed text ending on half a character,
which reads as U+FFFD: a broken character that is not on the page. It now stops one code unit short
in that case, which is what a control's name and value in a page snapshot already did.
Tool selection reads a skill choice the model wrapped in a code fence
Before a run, the deployment's model picks which of a Bot's skills the message needs, so a Bot holding
many tools is offered only the relevant ones. The request asks for bare JSON, but an endpoint that
ignores response_format, as Anthropic's OpenAI-compatible one does, lets the model fence the object
or lead with a sentence. Every such answer read as no answer, so the Bot was offered every tool it
holds and the audit row said unavailable. The object is now read out of the answer, the way the
router already reads its own. A bare JSON answer is read as before.
A long message reaches the coworker it is for, and is recorded
A message over 10,000 characters, such as a pasted email thread or log, was refused by the router
since it started capping the text it reads. The home composer carries on past a routing that fails,
so the message went to the default coworker rather than the one it is for, and a coworker chosen with
@ or from the To: field started with no channel.routed row. The app now asks the router about the
message's opening, and the whole message still goes to the coworker. Shorter messages route as
before.
Removing somebody is recorded even when retiring what they owned fails
Removing somebody denies their access and ends their sessions, then retires the credentials and
brokered connections they had granted this deployment. When that second half failed — a vault or
Composio not answering — the removal was already committed but nothing was written to the audit trail,
and removing them a second time reported success without retrying it, leaving those connections
standing. The removal is now recorded as soon as it takes effect, and removing somebody already
removed finishes the retirement that failed.
Removing a connector takes its grants with it
A grant naming a connector's tool outlived the connector. Removing an app revoked every credential
and every brokered account and deleted the app itself, and left the grant rows behind, naming a
server that no longer existed. Nothing showed them: the page that reports grants a connector no
longer advertises reads them off the connector's own row, and there was none. Adding the same app
back — which mints the same id, and so the same tool names — put every action it had back on every
Bot that used to hold it, with nobody granting anything and no row in the trail saying a grant had
been made. An app's grants are now removed in the same step as the app, the removal records which
grants it released and from which Bots, and a migration drops the grants earlier removals left
behind. Grants for other connectors, and skill grants, are untouched.
A vendor that broke no longer reads as a refusal to a Bot running its own loop
When a Bot that calls tools back from its own process, such as the LangGraph Bots, called a tool
whose vendor failed, or hit a fault in this deployment, the answer began "Refused." like a boundary
holding. The conversation drew it as blocked and the model read it as not allowed, while the audit
trail recorded a failed call. Only a refusal is marked now. A vendor that broke reads "That tool could
not be called: …", the way it already did for a Bot running here, and a refusal reads as before.
A Bot running its own loop is told a vendor's error is the vendor's
A vendor that says no by answering with an error, the way an MCP server refuses a call, reached a Bot
running here as "The vendor reported an error: …", and reached a Bot calling tools back from its own
process, such as the LangGraph Bots, as the bare sentence. Those Bots pass the answer on as they
receive it, so their model read something like Google's "The caller does not have permission" as an
ordinary result, and could tell the person they had no access rather than that the vendor had refused.
Both kinds of Bot are now told the same thing. A result that is not an error, and this deployment's
own refusals, read as before.
A long Composio result or failure is cut between characters, not through an emoji
A Composio action's answer over 20,000 characters, and a failure sentence as long, were cut by UTF-16
code unit. When the cut landed inside an emoji or any other character outside the Basic Multilingual
Plane, the text handed to the model ended on half of it: a lone surrogate that JSON carries as a bare
\ud83d and UTF-8 turns into a replacement character. The cut now stops one unit short in that case,
the way the MCP and built-in transports' cuts already do. Anything that fits is untouched, and the
note saying the answer was cut reads as before.