Skip to content

Release v0.1.0 - #726

Merged
davidmckayv merged 1 commit into
mainfrom
release/publish/v0.1.0
Oct 3, 2026
Merged

davidmckayv merged 1 commit into
mainfrom
release/publish/v0.1.0

Conversation

@github-actions

@github-actions github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Release v0.1.0

Merging this publishes the image, tags the commit and creates the GitHub Release.

Before merging:

  • The notes below describe what a deployment does differently
  • The smoke journey passed against a licensed deployment, and the result is
    pasted in a comment: bash scripts/start.sh && bun run test:smoke,
    with OPENBOT_SMOKE_COOKIE set to a signed-in session (docs/releasing.md)

Merging runs the full suite against this commit before it builds, so there is
nothing to check about CI here. The journey is the part CI cannot do: it needs a
licence, and a licence belongs to the machine it was issued for.


**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](docs/parallel-research.md).

### `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](docs/automatic-learning.md). 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](docs/configuration.md#live-voice-calls).

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.

@github-actions github-actions Bot added the release Release PR: merging publishes label Oct 3, 2026
@github-actions github-actions Bot added the release Release PR: merging publishes label Oct 3, 2026
@davidmckayv
davidmckayv merged commit cb5dc32 into main Oct 3, 2026
@davidmckayv
davidmckayv deleted the release/publish/v0.1.0 branch October 3, 2026 00:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release Release PR: merging publishes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant