Rebuild the deployment from nothing, and fix what that turned up - #283
Merged
Conversation
Every values file under `ci/` named `wss://gateway.cloud.copilotkit.ai`, which does not resolve, and `https://api.cloud.copilotkit.ai`, which resolves to something answering 404. A target file is meant to be the shortest honest starting point for a cluster, so copying one produced a deployment whose realtime plane pointed at a host that has never existed. `.env.example` has carried the working pair the whole time.
`--reuse-values` leaves a key this release added simply missing, so every template fallback is reached on exactly the deployments nobody is watching. Two were checked against values.yaml and two were not, because the check could only read a rendered `name:`/`value:` pair and the routines schedule and the culler's deadline are fields on a CronJob. The routines schedule had already been wrong once, by a factor of five, which is the argument for asserting it rather than trusting the comment beside it. Read off the rendered workload, found by its component label rather than its name, so the check is not pinned to a release name and a suffix.
…eeds before it `docs/README.md` indexed eight pages and none of them was Kubernetes. `charts/openbot/README.md` was linked from nowhere a reader would look, and the deployment page's AWS note offered ECS Express Mode and plain Fargate and stopped, so the shape that gives a Bot a computer of its own and runs the routines schedule was invisible to anyone following the docs. The chart README began at `helm upgrade --install` and assumed three things it does not create. Two were described further down; the third was written down nowhere: the published image carries `linux/amd64` only, so an arm64 node group cannot pull it and says `ImagePullBackOff` rather than anything about architecture. The Secret holding the database URL is now shown being made, because the key name the chart reads it by was stated only in a comment. The image measured 5.3 GB when the container work landed and measures 1.4 GB now. The reason given for it was right and is kept: 595 MB is still Firefox and WebKit, which nothing here launches. `ci/` has held five targets since the sandbox one arrived and the README counted four.
…the right to address another The changelog's `Unreleased` section covered routines, screenshots, unread dots and thirty smaller things, and said nothing about one Bot handing work to another. It clears the bar the file sets for itself: two new environment variables, two new chart values, a grant kind that did not exist, and a capability that stays off until an administrator turns it on. Turning it on is the part that was wrong. The configuration reference said the grant "is made per Bot like any other grant", and every other grant is made on a screen at `/admin/plugins`. This one is not: `bot` is a kind the store and the API both accept, it has no catalogue entry, nothing in the app renders it, and `listForAgent` filters to `mcp` and `skill` so the read path drops it too. The call that does work is written out instead, naming which of the two Bots is which, because the pair is directional and guessing wrong grants the opposite handoff.
The prerequisites above say what a cluster needs; this is the shortest thing that produces one, because "create an EKS cluster" is where a first install actually starts and the chart's docs began one step later. Written from the run that rebuilt this deployment from an empty account, so the `gp2` warning below it is not a caution copied from somewhere: that is what `eksctl` leaves behind, with the in-tree provisioner, unmarked. The note about letting the EBS CSI addon finish is there because creating it by hand while eksctl is creating it fails the cluster create with a message about pod identity associations, which says nothing about the race that caused it.
…ody here has seen
…who did what The Audit screen's Bot column reads `payload.bot` and renders a dash without it. Both handoff events carried the Bot under `from` and never under `bot`, so on a screen headed "Every action a Bot took" the two handoff rows were the only ones naming no Bot — while `agent.escalated`, written twenty lines away in the same feature, sets it. Found by reading the trail on a real deployment after driving a handoff through it, not from the code. Both tests fail without the change.
…ywhere in the thread A conversation that used the features this release is named for became permanently unusable, and the only thing it said was that a tool result was missing. `repairUnansweredToolCalls` exists so a stored thread stays provider-valid, and it decided which calls were answered by collecting every tool result in the array regardless of position. Threads do not come back in that order. Read back from the platform after one Bot handed work to another, the result sat three messages AHEAD of the call that produced it, and the same inversion held for `ask_person` and for an MCP tool call; only the computer's own tools came back in order. So the set said "answered", the array was returned untouched, and the provider — which matches a result to the call above it — saw a call with nothing after it and threw `AI_MissingToolResultsError` while converting the prompt. That fails the whole conversation rather than one turn: every later message in that channel died the same way, including "what is 2 plus 2". A call is now answered only by a result later in the array than the call. An early result is MOVED to sit after its call rather than replaced, because it is the real one and says more than this function's apology; only a call with no result anywhere still gets the apology. A result whose call never appears is dropped, because it answers nothing and a provider refuses it for the mirror-image reason. Checked against the 45-message thread this was found in: seven ordering faults before, none after, and the three real results kept.
davidmckayv
requested review from
MikeRyanDev,
guidovizoso and
tylerslaton
as code owners
August 28, 2026 02:18
The capability shipped with no way to switch it on. `bot` is a grant kind the store and the API both accept, it had no catalogue entry, nothing in the app rendered it, and the read path dropped it, so an administrator following the configuration reference found no screen and no answer. It belongs on the Bot rather than in the connector catalogue: a catalogue entry has a fixed list of tools somebody else maintains, and the Bots a deployment has are whatever was made here. It is also the question a person asks while looking at a Bot. `GET /api/agents/:id/handoff` reports the grants, whether the person looking may change them, and whether the deployment's caps leave the capability on at all. The last one is reported rather than inferred: with the caps at zero a grant is a row nothing will read, and a switch wired to nothing is worse than no switch. Writing reuses the grant endpoint every other grant uses, so the audit row and the refusals are the ones already in place. The pair is directional and the screen says so, because it is the one thing here that is easy to get backwards: the list is who this Bot may ask, not who may ask it.
A Bot card is a fixed 144px in a four-column grid, so the cards overlap as soon as the column holding them is narrower than the card. Opening a Bot takes that width out of the roster's column at any window size, so the cards behind an open Bot sat on top of each other on an ordinary screen. Tracks sized by `auto-fill` follow the container, which is the thing that actually changes here. The switch on the Bot's screen now reads the way the heading above it does: "Let this Bot ask Knowledge", not "Let Knowledge be asked by this Bot".
… Browser Bot The heading was the string "Browser Bot", so the screen called whichever Bot you opened by a name no deployment necessarily has, with the right one already resolved two lines away. It is the defect the route default under it was fixed for and says so in its own comment: a Bot name written into a route is wrong on every fork but the one it came from, and a Bot name written into the markup above it is wrong in the same way. The line under it still describes the screen, because that part is true of every Bot here.
…uster The chart refuses to install without `secrets.intelligenceApiKey` and `secrets.licenseToken`, and named neither anywhere a person would find before hitting that refusal: the README's prerequisites did not mention them and values.yaml carried two empty strings with no comment. A laptop is told three times over, in the README, `.env.example` and `start.sh`. `--print` rather than the `--write` the laptop uses, because `--write` puts the token in a local `.env`, which is not where it is going here.
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
The EKS deployment this was proven on was destroyed to nothing — cluster, database, registry — and
rebuilt from an empty account following the docs as written, to find out what a person actually hits.
Then the release was driven through it: Bots, routines, and one Bot handing work to another, happy
path and crappy path. Everything below is something that run turned up.
A conversation that used this release's features died for good
The one that matters. After a handoff, a routine firing and an
ask_personin the same channel,every later turn in it failed — including "what is 2 plus 2" — and the only thing the person was
shown was
Tool result is missing for tool call call_gIyW298bIC9e32UiB6d3MIXt, a raw SDK errorclass. The conversation could not be recovered.
Reading the stored thread back from the platform explains it. The tool result is stored before
its own tool call, for exactly the tools this release added:
computer_navigatemessage_botcreate_routineask_personreport_refusalplus a result at 37 whose call was never stored at all.
repairUnansweredToolCallsexists to keep athread provider-valid and decided "answered" by collecting every result regardless of position, so
an inverted result marked its call answered, nothing was repaired, and the provider — which matches a
result to the call above it — threw while converting the prompt. That fails the conversation, not the
turn.
A call is now answered only by a result that follows it. An early result is moved to sit after
its call rather than replaced, because it is the real one; only a call with no result anywhere gets
the apology text. A result whose call never appears is dropped. Checked against the real 45-message
thread: seven ordering faults before, none after, all three real results kept. Two tests added, both
failing against the previous implementation.
This makes the client resilient. It does not stop the platform storing them in that order, which is
worth someone's attention separately.
The chart's own examples pointed at a host that does not exist. All five files under
charts/openbot/ci/setgatewayWsUrl: wss://gateway.cloud.copilotkit.ai, which does not resolve,and
apiUrl: https://api.cloud.copilotkit.ai, which resolves to something answering 404. A targetfile is meant to be the shortest honest starting point for a cluster; copying one produced a
deployment whose realtime plane pointed at a host that has never existed.
.env.examplehas carriedthe working pair all along.
The release was named for a feature the changelog did not mention.
Unreleasedcovered routines,screenshots, unread dots and thirty smaller things, and said nothing about one Bot handing work to
another. Written now — and rewritten once, because the first draft said the answer comes back into
the asking conversation. It does not, and it is not supposed to: the addressed Bot answers for
itself. Driving it is what caught a changelog entry that would have shipped describing the opposite
of the behaviour.
Bot-to-bot grants had no screen, and the docs said they did.
botis a grant kind the store andthe API both accept, it had no catalogue entry, nothing in
app/srcrendered it, andstore.listForAgentfiltered tomcpandskillso the read path dropped it. The only way toswitch on the capability this release is named for was a
curl.It is now on the Bot's own screen, under Bots it may ask: a switch per other Bot, with the
directional pair said out loud, because that is the one thing here that is easy to get backwards.
GET /api/agents/:id/handoffreports the grants, whether the person looking may change them, andwhether the deployment's caps leave the capability on at all. That last one is reported rather than
inferred: with the caps at zero a grant is a row nothing will read, and a switch wired to nothing is
worse than no switch. Writing reuses the grant endpoint every other grant uses, so the audit row and
the refusals are the ones already in place.
The Bot screen called every Bot "Browser Bot". The heading was that string, with the right Bot
already resolved two lines away. It is the defect the route default beneath it was fixed for and
says so in its own comment: a Bot name written into a route is wrong on every fork but the one it
came from.
The roster overlapped itself whenever a Bot was open. A card is a fixed 144px in a
four-column grid, so the cards sat on top of each other as soon as the column holding them was
narrower than the card, which opening a Bot does at any window size. The tracks now follow the
container.
The Audit screen named no Bot on either handoff row. Its Bot column reads
payload.botand showsa dash without it; both handoff events carried the acting Bot only as
payload.from, whileagent.escalatedtwenty lines away sets it. So on a screen headed "Every action a Bot took", the twohandoff rows were the only ones naming no Bot. Fixed, with a test on each event that fails without
the fix. Found by reading the trail on the live deployment, not from the code.
The docs never mentioned Kubernetes.
docs/README.mdindexed eight pages and none was the chart;charts/openbot/README.mdwas linked from nowhere; the deployment page's AWS note offered ECS andstopped. The chart README also began at
helm upgrade --installand assumed three things it does notcreate, one written down nowhere: the published image carries
linux/amd64only, so an arm64node group cannot pull it. The cluster this was rebuilt on is now written down as the shortest thing
that produces a working one.
Two template fallbacks had no drift test.
check-new-values-keys.tsasserted the--reuse-valuesfallback against values.yaml for the two handoff caps and nothing else, because itcould only read a rendered env var. The routines schedule and the culler's deadline are CronJob
fields, and the routines schedule had already been wrong once, by a factor of five. Both are checked
now, read off the rendered workload by component label. Verified by drifting both deliberately.
Stale measurements. The image is 1.4 GB, not the 5.3 GB written when the container work landed;
595 MB of it is still Firefox and WebKit, so the reason given was right and the number was not. The
README counted four chart targets and
ci/holds five. Helm 4 renamed--atomic, so the rollbackadvice now names the flag on both majors.
What the drive proved
Install from zero on the first attempt,
--atomic, twelve resources. Migrations ran as a pre-installhook and deleted themselves; the container correctly did not also migrate on start against an
external database, and started no embedded PostgreSQL. Default
gp3StorageClass, both computervolumes Bound. A Bot answered, was given a sandbox computer of its own, opened a real page and put
the screenshot in the transcript.
Routines end to end: refused while ungranted, created by asking once granted, and
every single minuterefused by the 15-minute floor without creating anything. One Bot handed work to another andthe addressed Bot answered a question only it could have seen.
ask_personreached the person.Self-grant and unknown-Bot grants both refused, 403, named. All thirteen chart install-time refusals
fire with the right message.
What this run could not prove
config.singleUserwas on, so every request is one administrator. The non-admin paths — theenumeration oracle, the admin-versus-user roster divergence, the role a hop is resolved as — are
structurally unreachable in that configuration, and are covered by tests rather than by this run
(
bot-access.test.ts,agent-handoff.test.ts,plugin-routes.test.ts).networkPolicy.enabledleaves the routines CronJob unfenced on main:networkpolicy.yamlselectsserverandcomputerand nothing selectscomponent: routines. Not fixed here because #277already owns that file and two PRs editing it would collide — but #277 predates routines, and its own
new check fails on rebase. Reproduced: merge origin/main into #277, render eks-sandbox with
networkPolicy.enabled, and its gate reportsnone selects ci-openbot-routines.