Skip to content

Rebuild the deployment from nothing, and fix what that turned up - #283

Merged
davidmckayv merged 15 commits into
mainfrom
fix/zero-to-running
Aug 28, 2026
Merged

Rebuild the deployment from nothing, and fix what that turned up#283
davidmckayv merged 15 commits into
mainfrom
fix/zero-to-running

Conversation

@davidmckayv

@davidmckayv davidmckayv commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

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_person in 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 error
class. 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:

tool call at result at
computer_navigate 5 6 correct
message_bot 12 9 result three messages ahead of its call
create_routine 20 17 inverted
ask_person 31 27 inverted
report_refusal 23 24 correct

plus a result at 37 whose call was never stored at all. repairUnansweredToolCalls exists to keep a
thread 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/ set gatewayWsUrl: wss://gateway.cloud.copilotkit.ai, which does not resolve,
and apiUrl: 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; copying one produced a
deployment whose realtime plane pointed at a host that has never existed. .env.example has carried
the working pair all along.

The release was named for a feature the changelog did not mention. Unreleased covered 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. bot is a grant kind the store and
the API both accept, it had no catalogue entry, nothing in app/src rendered it, and
store.listForAgent filtered to mcp and skill so the read path dropped it. The only way to
switch 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/handoff reports the grants, whether the person looking may change them, and
whether 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.bot and shows
a dash without it; both handoff events carried the acting Bot only as payload.from, while
agent.escalated twenty lines away sets it. So on a screen headed "Every action a Bot took", the two
handoff 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.md indexed eight pages and none was the chart;
charts/openbot/README.md was linked from nowhere; the deployment page's AWS note offered ECS and
stopped. The chart README also began at helm upgrade --install and assumed three things it does not
create, one written down nowhere: the published image carries linux/amd64 only, so an arm64
node 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.ts asserted the
--reuse-values fallback against values.yaml for the two handoff caps and nothing else, because it
could 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 rollback
advice 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-install
hook and deleted themselves; the container correctly did not also migrate on start against an
external database, and started no embedded PostgreSQL. Default gp3 StorageClass, both computer
volumes 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 minute refused by the 15-minute floor without creating anything. One Bot handed work to another and
the addressed Bot answered a question only it could have seen. ask_person reached 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.singleUser was on, so every request is one administrator. The non-admin paths — the
enumeration 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.enabled leaves the routines CronJob unfenced on main: networkpolicy.yaml selects
server and computer and nothing selects component: routines. Not fixed here because #277
already 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 reports none selects ci-openbot-routines.

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.
…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.
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.
@davidmckayv
davidmckayv merged commit 118b6f6 into main Aug 28, 2026
13 checks passed
@davidmckayv
davidmckayv deleted the fix/zero-to-running branch August 28, 2026 04:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant