github CopilotKit/OpenBot v0.1.0

2 hours ago

Before upgrading. Six things change for an existing deployment:

  • Automatic Learning is on unless an administrator saved it off. It does nothing until a Learning
    container is assigned; see below.
  • A Bot's computer refuses the network until the server pushes its policy. A computer run without
    an API server can set EGRESS_POLICY_REQUIRED=0 for the old behaviour.
  • The upgrade runs migrations 0042_user_preferences, 0043_plugin_logos, 0044_voice_sessions,
    0045_channel_activity_source, 0046_automatic_learning, 0047_agent_pinning,
    0048_coworker_parity, 0049_coworker_parity_lanes, 0050_review_fixes and
    0051_routine_enabled_at.
  • An existing Windows clone checks text files out with LF only after
    git rm -r --cached . && git reset --hard on a clean tree.
  • The signed-in app shows a bar offering CopilotKit's help self-hosting OpenBot, unless the
    deployment is on a paid Intelligence plan. Set OPENBOT_SELF_HOST_BANNER=false to remove it for
    everybody.
  • An egress rule with a malformed IP range, such as 10.0.0.5/, used to be read as /0 and allow
    every IPv4 address. It is now refused, and a saved network policy that contains one is refused as
    a whole: the Bots under it fall back to an allowlist with nothing on it, so they reach nothing
    until the rule is corrected under Admin → Enterprise.

A group reply the owner allows still reaches the Bot it names

A reply held until its owner allowed it to be shown in a group was written into the transcript and then stopped. The same reply allowed immediately was handed to the Bot it named. Allowing it now hands it on the same way.

Before upgrading. Four things change for an existing deployment:

The app offers help self-hosting OpenBot, until you close it

A slim bar at the top of the signed-in app links to CopilotKit's engineers for help self-hosting
OpenBot. Closing it is saved to your preferences, so it stays closed on every device. A deployment
on a paid Intelligence plan (pro, team, team_self_hosted or enterprise, or a licence bought
through AWS Marketplace) never shows it; any other plan, or an entitlement that cannot be read,
shows it. A fork running OpenBot for its own organization hides it for everybody with
OPENBOT_SELF_HOST_BANNER=false.

A request to the approvals API that is not JSON answers 400

A body that could not be parsed as JSON, sent to any approvals route that reads one, such as
PATCH /api/approvals/preferences or POST /api/approvals/rules, answered 500 with the parser's own
message. It now answers 400 "Supply a valid request.", as the delivery routes do.

Syncing a memory source a policy refuses says why

When a connected app's policy refused the read behind a memory source's sync, POST /api/memory/sources/:id/sync answered 503 "Memory is unavailable. Try again.", although the
refusal's own sentence was already saved on the source. It now answers 400 with that sentence, as
the plugin routes do for the same refusal.

@Ops Lead in a group addresses Ops Lead, not Ops as well

In a group conversation, a reply naming @Ops Lead also addressed a Bot called Ops, because the
shorter name matched at the same @, so both answered. An email address addressed a Bot by its
domain: jo@sam.com reached a Bot called Sam. Where two names start at the same @, only the
longer one is now addressed, and an @ straight after a letter or digit is not a mention.

A webhook with many long top-level fields no longer stops the server

A trigger's event is cut to 32 KiB before it is recorded, keeping each top-level text field up to
1000 characters and an excerpt of the rest. A flat payload whose fields alone came to more than
that, such as forty 1000-character fields, left the excerpt nothing to give up, and the loop
trimming it never ended. That loop runs on the server's only thread, so every request stopped being
answered. The kept fields now get at most half the space, and the rest is still in the excerpt.

A malformed IP range in an egress rule is refused instead of widening the rule

An egress cidr rule written 10.0.0.5/ was stored as 10.0.0.5/0, which is every IPv4
address, so a typo for one host opened an allow-list to all of them. /0x8 and 10.0.0.0/8/9 were
accepted the same way. A zone id such as fe80::1%eth0 was accepted too, and then threw from the
filter on the first connection that policy judged. Each is now refused when the rule is saved, with
the sentence a malformed range already got. A rule like this saved earlier matches nothing.

Deleting a channel twice is recorded once

A second DELETE of the same channel, from a retry or a second tab, still answers 204 as before.
It no longer tells every member again, and no longer writes another channel.deleted row to the
audit trail for a deletion that did not happen.

A Bot's saved reply in a group is no longer replaced by a later error

In a group conversation, a Bot's reply was saved, then handed on: to Activity, to any consent
cards, and to the Bots it named. A fault in that hand-on, such as the audit trail being
unreachable, wrote the error's text over the saved reply and marked it failed, and a retry did not
bring the reply back. The reply now stays as saved, the fault is logged as
group-turn-after-reply-error, and any consent cards or handoff the fault interrupted are still
posted.

The egress filter reaches an IPv6 upstream proxy and asks it for IPv6 hosts correctly

An upstream proxy configured at an IPv6 address, such as http://[fd00::1]:3128, could not be
reached: the filter handed the address to the socket with its brackets, and the socket looked it up
as a name. A CONNECT to an IPv6 host was also sent to the upstream without the brackets an
authority needs, as CONNECT ::1:443. Both now work.

The Helm chart configures Slack, Teams, text messages, push, SCIM, inbound email and OpenTelemetry

These settings had no chart values and could only be passed through config.extraEnv. They now have
their own: config.opentag, config.sms, config.push, config.deliveryPublicUrl, config.scim,
config.inboundEmail and config.otel, with the OpenTag secret, the Twilio auth token, the Expo
access token, the SCIM bearer tokens and the OpenTelemetry headers under secrets (or an existing
Secret or store, by key). The install refuses what the server would refuse at boot, such as an
OpenTag secret under 32 characters or a partial set of Twilio settings. With none of them set, the
chart renders exactly as before, so a deployment already passing these through config.extraEnv
keeps working unchanged until it moves them across.

A coworker at its own endpoint can hand work to another Bot

A grant letting a remote Bot (a coworker at its own endpoint) hand work to another Bot was accepted and stored, but the grant read kept only built-in Bots, so the remote Bot was never
offered message_bot and a call it made anyway was refused as not granted. The read now counts a
grant whatever the Bot's type, so a remote Bot hands work on through the same signed callback, grant
check, caps and audit rows as a built-in one, to a built-in Bot or to another remote Bot. The
coworker's handoff panel now offers it the same switches.

A channel cursor with a malformed time reads as the first page, not a 500

GET /api/channels only checked that a cursor's time was a string before casting it with
::timestamptz, so a hand-edited or corrupted cursor such as {"recency": "not-a-date"} answered
500. A time that is not the UTC timestamp the list writes now reads as the first page, the way every
other malformed cursor already did.

A playground component's Published switch publishes its source too

The Published switch on an admin component page called the generic publication endpoint for every
kind. For a browser-authored component that endpoint promoted the description and marked it
published without copying the playground draft, so Bots were offered a component the renderer could
not draw. Playground components now publish and withdraw both rows in one transaction; a missing or
empty description or HTML is refused instead of leaving a half-published component, and repeating
an unchanged publish no longer advances the revision.

A proxy password containing % no longer stops every shell command

A proxy password with a % that does not start an escape, such as p%zz, made decoding it throw.

  • In the computer's shell, which strips proxy credentials before every command, every /exec
    failed as a result.
  • Resolving a Bot's egress proxy failed the same way.

Such a password is now taken as written, and it is still kept out of the shell's environment.

Revoking a credential twice says so, instead of answering a server error

Revoking a credential that was already revoked, or that does not exist, now answers 404 with the
reason. Rotating one that is gone answers 404, and rotating one that is revoked or does not match
the key answers 409. Before, each answered a plain-text 500, as if the deployment were broken; a
double click on Revoke was enough to cause it. The refused-rotation audit row is written as before.

The channel list no longer skips channels made in the same millisecond

The channel list's page cursor kept the last channel's time to the millisecond, while PostgreSQL
keeps it to the microsecond. Channels later in that same millisecond, as a package sync or an
import makes them, sorted after the cursor and were on no page. The cursor now carries the time to
the microsecond, as the audit trail's cursor already did.

A package skill's slug has the same shape as one made in the app

The skills screen, the skills API and the store all accept a slug of 2 to 40 lowercase letters,
digits and hyphens that starts and ends with a letter or digit. skills.yaml accepted any length
and a trailing hyphen, so a package could seed a, a- or a sixty-character slug that nobody
could then edit. A package with such a slug is now refused at load, with a sentence naming it.
Every slug in examples/fintech already has the shape.

A tenant package that repeats itself is refused by name, instead of failing at boot

Validation now refuses each of these, with a sentence naming the file and the repeated id:

  • A skill named twice by one agent, or an agent listed twice in one channel's permitted_agents.
    Before, the server stopped at boot with a raw SQL error.
  • An agent id repeated within agents.yaml, a channel id within channels.yaml, or a skill slug
    within skills.yaml. Before, the last entry silently won, though two files declaring the same
    agent were already refused.
  • A remote agent left blank in agents.yaml but declared with an endpoint under agents/ was
    dropped from every channel that named it. It is now treated as declared.

start.sh and stop.sh see their own processes on Windows

In Git Bash on Windows, which has no lsof, pgrep or pkill, every process and port lookup in
the two scripts came back empty.

  • bash scripts/stop.sh reported the app, the routine worker and the API server as not running,
    and left all three up.
  • start.sh could not see a port held by another process.
  • start.sh started another routine worker on every rerun, then reported that the one it had just
    started "did not stay up".

Where those tools are missing on Windows, the scripts now ask PowerShell, which ships with Windows.
Everywhere the tools exist they are used exactly as before.

The supervisor and the Python Bots compare their tokens in constant time

The supervisor compared its bearer token with a plain string comparison, and the eleven Python Bots
compared the shared agent token as text, which answered 500 rather than 401 on a header carrying a
non-ASCII character. Both now compare bytes in constant time, as the server and the computer already
did. A wrong or missing token is refused exactly as before.

A clone on Windows builds an image that starts

On Windows, where Git converts line endings by default, a clone checked every text file out with
CRLF. docker build copied the s6 service files into the image that way, so a service's type
read longrun\r and its scripts stopped on set: -: invalid option, and bun run format:check
failed on every file. .gitattributes now checks text files out with LF on every system. An
existing clone with nothing uncommitted picks this up after git rm -r --cached . && git reset --hard.

A routine switched back on gets a fresh count of failures

A routine that fails ten times in a row is switched off, and someone has to switch it back on.

  • Before: the failure count ignored that, so the first failure after re-enabling counted as the
    eleventh. The routine was switched straight off again with "failed ten times in a row", and the
    first-failure message never appeared.
  • Now: failures are counted from when the routine was last switched on, recorded in a new
    routines.enabled_at column. The migration sets it to the time of the upgrade, so any failure
    streak already under way starts again from zero at that point. Adds migration
    0051_routine_enabled_at.

Generated workspace files can be downloaded intact

GET /api/computers/:botId/files/download?path=... streams a generated file as an opaque
attachment instead of returning the 64 KB UTF-8 text extract. Downloads use the separate
computer_download_file / download_file permission, remain confined to the Bot workspace, are
capped at 100 MiB with 413, and are recorded on the computer audit trail. Switching off Cloud
computer use
under Admin → Enterprise refuses downloads as it refuses reads. Existing read, list
and write APIs are unchanged.

Bots work as coworkers

A Bot can now carry on without anyone watching it. It runs standing Responsibilities fed by
schedules, signed webhooks, GitHub, Linear, Sentry, PagerDuty, inbound email and Slack messages,
drives its own computer while the app is closed, and keeps its browser profile, cookies and files
across restarts. Its questions and approval requests reach the person who owns the conversation in
Slack or Microsoft Teams (through OpenTag), by text message or by push, and the conversation
resumes when they answer; charts reach Slack and Teams as native charts. Reachability links
each of those places to a conversation.

Approvals become one personal flow across the browser, connected apps, shell, files, the host and
remote Bots, with custom rules, auto-review and host command modes (Approvals). Bots can
message each other, share a group conversation with attributed speakers, and be published to
teammates as Team Bots. Memory imports facts from connected apps with their source, and
optional background research suggests next steps. A browser demonstration can be recorded and
turned into a skill. Administrators get capability toggles, SSO-required sign-in, SCIM, network
egress policy, action recording, OpenTelemetry export, and Passwords with private sign-in
requests.

Upgrading runs migrations 0048_coworker_parity, 0049_coworker_parity_lanes and
0050_review_fixes.

A Bot's computer refuses the network until its policy arrives

A computer used to allow every connection until the server had pushed its Bot's network policy,
which left up to 30 seconds of unfiltered access after every wake. It now refuses until the policy
arrives, and the server pushes it as the computer wakes. Cloud metadata and link-local addresses are
refused in every mode, including allow_all, and the browser's WebRTC traffic now goes through the
filter instead of around it. A computer run without an API server can set
EGRESS_POLICY_REQUIRED=0 to keep the old behaviour.

Parallel Search is in the plugin catalogue

Two catalogue entries reach Parallel's public-web search and extraction at
https://search.parallel.ai/mcp: Parallel Search, anonymous with provider-managed limits, and
Parallel Search (API key), which sends a deployment credential as a bearer token. Nothing is
granted automatically. When a Bot holds both web_search and web_fetch, the built-in Bot guidance
describes them for public-web research, and the fintech example's Research Desk ships a
research-public-web skill that declares them. See
Public-web research with Parallel.

lodash-es is pinned to the patched 4.18.0

A root overrides entry pins the transitive lodash-es to 4.18.0, the patched release, wherever a
dependency pulls it in.

Provider and Bot lookups ignore inherited object properties

Unknown names such as constructor and __proto__ no longer return an inherited
JavaScript object as a provider or Bot entry. Unknown providers return no spec, and
missing Bots raise the existing startup error. Configured providers and Bots are unchanged.

A malformed % in a stream URL no longer returns a 500

A request to /api/computers/<id>/stream whose id held a broken percent-escape, such as %zz,
made the server throw and answer 500. It is now treated as not matching the stream route and goes
through normal routing. Valid ids behave as before.

start.sh names the port to change on macOS

When the API server's or the app's port was held by another process, start.sh was meant to say
which setting to change, such as Re-run with SERVER_PORT=<free port>. On macOS, whose bash is
3.2, the run ended on bad substitution before printing it, because the hint upper-cased the
name with a bash 4 expansion. It is upper-cased with tr now, so the hint prints on either bash.

start.sh starts the Docker services on Compose v5

On Docker Compose v5, bash scripts/start.sh stopped at
1/4 Docker services with failed to get console: provided file is not a console. The script
sends compose's output to /dev/null while its errors still reach the terminal. Compose saw that
terminal, chose its interactive build display, and could not draw it. The script now asks compose
for quiet progress through COMPOSE_PROGRESS, which older Compose versions ignore, so nothing
changes where it already worked.

The skills pages say when the skills could not be read

When GET /api/plugins failed, Agent Skills said "You don't have any skills yet.", Admin →
Skills
said "No skills yet.", and opening a skill to edit said "That skill no longer exists, or it
is not yours to edit.", to people who may have written a dozen. They now say the skills could not
be loaded. A list that arrived empty still reads as before, and a failed refetch over a list already
on screen keeps that list.

A coworker can be pinned to the top of the Agents screen

A coworker's Manage tab has a Pin switch beside Hide, and pinned coworkers move into a
Pinned section at the top of /agents. Like hiding, pinning is personal: it changes nothing for
anyone else. It is stored next to hidden_at in agent_preferences as a new pinned_at column
(migration 0047_agent_pinning), and each write sets only its own column, so pinning a hidden
coworker keeps it hidden and it comes back pinned when unhidden. POST /api/agents/:id/pin and
/unpin record bot.pinned and bot.unpinned on the trail, as hiding does.

A hidden coworker can be found again on the Agents screen

Hiding a coworker took it off both lists on /agents, and Unhide is only in the coworker's dialog,
which only its card opens, so a hidden coworker had no way back short of typing its id into the
address bar. The screen now ends with a collapsed Hidden section listing them, each card opening
the dialog as before. It appears only when something is hidden. Hiding still changes nothing for
anyone else, and nothing on the server changed: the screen reads the GET /api/agents?hidden=true
list the server already served.

A connector's own pages say when the plugin list could not be read

When GET /api/plugins failed, a connector's admin page said "Not a plugin", a tool's page said
"This deployment has not enabled that connector.", and a person's connected-account page said "This
is not a service you connect for yourself.", each about a connector that may be added and granted
right now. They now say the list could not be loaded, as the per-Bot grant page beside them already
did. A list that arrived without the connector still reads as before.

Boundaries says when the policy could not be read

When GET /api/computers/policy failed, the Boundaries screen showed its title over nothing, for as
long as it was open. It now says "The boundary could not be read.", or the server's own reason, as
it did before the screen's read moved into a query.

Browser controls are visible in chat and the Computer sidebar

Channel chats and standalone Bot chats now have a labeled Computer button and always-visible
Take control or Hand back controls. The same ownership state is shown in the chat, live Computer
sidebar, and full-size viewer. Standalone Bot chats can open the current live browser alongside
the conversation without losing the selected Bot or chat history.

An administrator cannot grant a skill nobody has written

POST /api/plugins/grants checked that a skill existed for everybody except an administrator, whose
grant was stored whatever it named. A Bot's skills are read by slug alone, so the row waited for
whoever wrote a skill under that name next, and on a Bot the deployment shares that was one person's
instructions answering everybody. It is now refused with "There is no skill called …", as a grant
naming no app already was. Revoking one by hand still works.

A remote Bot keeps answering when it asks for a learned skill that is not there

With Automatic Learning delivering skills, a Bot reached at an AG-UI endpoint (every shipped Bot
except a built-in one) ended the whole turn with "Skill is unavailable." in place of an answer when
its model asked for a skill by a name the snapshot does not hold, for a file the skill does not
list, or sent arguments that were not JSON. A built-in Bot's model is handed that sentence as the
call's result and carries on. A remote Bot's model now gets the same result and carries on too.

An app or skill cannot be granted to a Bot that does not exist

An administrator's POST /api/plugins/grants for an app or a skill checked that the app or skill
existed but not the Bot, so a mistyped Bot id reached the insert, failed on the plugin_grants
foreign key, and answered 500 with no body. It is now refused with "There is no such Bot.", the
sentence the bot kind already used, and nothing is stored. Revoking still checks nothing.

A malformed OAuth client is refused with a 400, not a 500

POST /api/plugins/servers/:id/oauth-client called .trim() on the client id and secret without
checking they were strings, so {"clientId": 12345, "clientSecret": "s"}, or a secret of {}, threw
outside the route's try and answered 500. It now answers the same 400 as an empty value, as the
other plugin routes do for their own fields, before the store or the audit trail is touched.

Browser challenges can be handed to a person without losing the Bot's page

Bots pause for actionable browser challenges and resume from a fresh page snapshot after an explicit
handback. Requests survive viewer reconnects and distinguish completion from cancellation, expiry,
or an interrupted browser session. Managed browsing now uses full Chromium in headless or headed
mode. Local API deployments can opt into installed Chrome with dedicated per-Bot profiles, the same
in-app viewer, a loopback-only computer endpoint, and host shell execution disabled.

A hidden Bot's app grants stay on the Plugins screens

Hiding a Bot from your own roster took it off the Plugins screens too: its row went from By Bot and
from each tool's switches, the counts could read "3 of 2 Bots", and its own page said there was no
such Bot. Its grants stayed in force, and nothing on any screen could take them away. The Plugins
screens now follow the rule the Handoff panel already does: a hidden Bot is shown when it holds one
of the grants the screen is about, marked "Hidden from your roster", and its own page draws its
grants. Nothing on the server changed.

Revoking a function from a component that does not exist answers 404

DELETE /api/components/:name/functions/:function was the one grant write that did not check the
component exists. Against a name nobody has, it deleted nothing, answered revoked: true and wrote a
component.function_revoked row naming a component that was never there. It now answers 404 and
writes nothing, as granting a function and withholding a component already do. A function grant
cannot outlive its component, so there is no stored row this stops anybody removing.

A wiped or restarted shared computer no longer leaves refs pointing at the dead page

Snapshots are ordered on the run of the browser that took them as well as the generation, so a
computer that is replaced cannot have its old page mistaken for its new one. That ordering was
reaching only the deployments that give each Bot its own container or sandbox, because the run was
read off the infrastructure and the deployment with one shared computer has none to read.

Two failures followed there, and both are fixed. A snapshot still in flight when somebody pressed
Reset brought the wiped page back, and the boundary went on deciding about its elements: a rule about
"Confirm transfer" firing on a click nowhere near one, or failing to fire on one that is. And a
computer that restarted counted generations from one again, so its first snapshots were dropped as
stale and refs kept resolving against a page nobody was on until the counter climbed back past it.

The computer now mints a run for each Bot's browser session, mints a new one when the Bot is reset,
and answers which run it is on. Nothing changes for a deployment that already reports one, and a
computer too old to answer leaves the ordering exactly where it was rather than refusing anything.

scripts/start.sh no longer needs python3

The runtime health check in step 3 was a python3 heredoc. On a machine without Python, and on
Windows, where python3 is usually the Microsoft Store alias that exits 49, the run stopped there
with the server and worker up and the app never started. The check now runs in Bun, which the
script already requires, with the same output and exit status, and python3 is gone from the
prerequisites in docs/development.md.

An MCP tool that answers with a resource link is no longer read as an empty name

A tool that points at a file or a page often returns a resource_link: a URI, a name, and a
sentence of what it is, rather than the contents themselves. That part was named [resource_link]
and the URI was dropped, so the model was told a link arrived and never shown where it went. A
search that answered with pages produced no page it could open. The URI, name and description are
now read, each on its own labelled line. The URI leads and the name and description are bounded, so
a long name cannot push the pointer past the result cap; a server's title is shown over its name
when it gives one. A part that already carried text is unchanged.

The Microsoft Agent Framework Bot answers on a plain OpenAI key

Picked with an OpenAI key, the Microsoft Agent Framework Bot failed every run with "Connection
error.". Compose writes OPENAI_BASE_URL empty when the choice is a plain OpenAI key, and the
OpenAI SDK only defaults an absent URL, so it was given "" as the address. The Bot now falls back to
https://api.openai.com/v1 for an empty value, as its Anthropic branch already did for
ANTHROPIC_BASE_URL. An OpenAI-compatible endpoint is unchanged.

OPENBOT_ONE_COMPUTER_EACH=false in .env is honoured by start.sh

scripts/start.sh read OPENBOT_ONE_COMPUTER_EACH from the environment alone, so the line that
docs/configuration.md tells people to put in .env was ignored: the supervisor was still started
and the server still told to give each Bot its own computer. It now reads the key as it reads every
other setting, the environment first, then .env, then the default of true.

Skill selection keeps capabilities named across multiple JSON replies

When a model wraps its skill choice in prose or sends a revised JSON object, OpenBot reads each
complete skills list and offers the union of the named skills' granted tools. This also works with
Anthropic's OpenAI-compatible endpoint, which may ignore the request for bare JSON. Previously a
reply containing multiple objects fell back to offering every tool; replies with no valid skills
list still do.

The AG2 Bot answers on a plain OpenAI key

Picked with an OpenAI key, the AG2 Bot failed every run. Compose writes OPENAI_BASE_URL empty when
the choice is a plain OpenAI key, and the OpenAI SDK only defaults an absent URL, so it was given ""
as the address. The Bot now falls back to https://api.openai.com/v1 for an empty value, as its
Anthropic branch already did for ANTHROPIC_BASE_URL. An OpenAI-compatible endpoint is unchanged.

The live screen keeps reconnecting after it has recovered

A dropped live screen retries five times, waiting half a second, then one, two, four and eight, and
then asks for Retry. The count of retries never went back to zero after a retry worked, so a screen
left open through five short drops over an afternoon gave up on the sixth, although each had
recovered within a second. The count now starts over once a reconnected screen shows a frame again,
so only five failures in a row end in Retry.

A failed save of a Bot's browser control leaves no copy behind

The computer keeps who holds a Bot's browser, and its handoff requests, in one file per Bot under
the profiles volume, written to a temporary file first and renamed over it. When the write or the
rename failed, the temporary file stayed, a readable copy of that state beside the real one, and
every later failure added another. It is now removed whether or not the save succeeds, as the
learning setup and the model sign-in file already do.

Every Bot's provider defaults live in one spec file

shared/model-providers.json now holds the provider facts and the default provider and model of
the thirteen Bots that read it: the three TypeScript Bots through shared/model-providers.ts and the
ten Python Bots through shared/model_providers.py. The Claude Agent SDK Bot does not read it.
BOT_PROVIDER and BOT_MODEL still win over both, and each Bot keeps its existing default,
including Mastra's gpt-4o-mini. Moving a Bot to a different model is one row in one file.

Both loaders check the file against their own list of providers and refuse in the same words, so a
wrong row now stops all thirteen at startup, naming the key, where the Python Bots used to start
clean and meet it at their first model call. The Mastra Bot also refuses a BOT_PROVIDER it does
not recognize (such as google) instead of quietly answering through OpenAI with a different model.

Compose used to substitute gpt-5.5 for agent-langgraph whenever BOT_MODEL was unset, whatever
BOT_PROVIDER named. It now passes the unset value through, so the Bot's own row answers: an OpenAI
deployment keeps gpt-5.5, and a Google or Anthropic one stops being handed a model its vendor has
never heard of. The picked harness in Compose now also receives GOOGLE_API_KEY and
GOOGLE_GENERATIVE_AI_BASE_URL, so a harness picked on BOT_PROVIDER=google has its key.

Automatic Learning, on by default

Every shipped Bot can now contribute completed conversations to a CopilotKit Intelligence Learning
container and receive the skills published from it. Admin → Automatic Learning chooses a default
container, overrides or excludes individual Bots, and pauses Learning. Chat, channels, routines and
handoffs all follow the same settings.

Learning is on unless an administrator has saved it off, and a saved off stays off. It collects and
delivers nothing until a container exists in the Intelligence project and is assigned:
bun scripts/setup-learning.ts creates or reuses one called openbot and writes it to .env, or
set CPK_INTELLIGENCE_LEARNING_CONTAINER_ID (Helm: config.learning.containerId), or enter it on
the Admin page. OpenBot starts and chats without one. Skills are reviewed and published in
Intelligence; nothing is approved automatically. See
Automatic Learning. Adds migration 0046_automatic_learning.

Dictate messages and talk to a coworker in a live voice call

Deployments can configure transcription separately from their Bots' models, with a waveform composer
for recording, cancelling, transcribing, or sending speech. Optional OpenAI Realtime and Grok voice
adapters add a floating call widget. The live model handles conversation directly and delegates tools
and actions to the existing Bot in the same thread. Ending a call saves its transcript and a short
summary; later voice calls and typed messages receive that history. Calls start silently, and muting
affects the person's microphone. See configuration.

Voice summaries use the configured chat provider, including Anthropic keys and Claude or ChatGPT
plan sign-in. Retrying a failed summary refreshes its sidebar preview without replacing newer
activity. A call the voice service turns down says why, such as "The voice service is busy. Please
retry shortly.", and a call that cannot be saved says "Could not save this voice chat." and stays on
its card to retry. Caps on a call's context, captions and answers cut between characters, never
inside an emoji.

The Pydantic AI Bot answers on a plain OpenAI key or an Anthropic key

Picked with either key, the Pydantic AI Bot failed every run. Compose writes the endpoint the choice
did not need as empty (OPENAI_BASE_URL for a plain OpenAI key, ANTHROPIC_BASE_URL for an
Anthropic key), and Pydantic AI builds each provider's client from the environment, so the SDK was
given "" as the address. The Bot now removes an empty value before building the model, as
agent-langgraph-agui already does, so the SDK uses its own endpoint. A real endpoint is unchanged.

The Langroid Bot answers on a plain OpenAI key

Picked with an OpenAI key, the Langroid Bot failed every run with "Connection error.". Compose
writes OPENAI_BASE_URL empty when the choice is a plain OpenAI key, and the OpenAI SDK only
defaults an absent URL, so it was given "" as the address. The Bot now drops an empty
OPENAI_BASE_URL before it builds its client, as it already does for an empty OPENAI_API_KEY.
An OpenAI-compatible endpoint is unchanged.

Find older conversations and keep chat preferences across devices

The sidebar loads older conversations as the person scrolls. Settings save the choice to emphasize
the agent or thread name in the database, with a preview. Agent directory cards have more space,
connected accounts use individual entries with stored app logos, and browser steps appear in a compact
expandable group instead of filling the conversation with screenshots.

Don't miss a new OpenBot release

NewReleases is sending notifications on new releases.