Using the brain¶
The brain is an LLM orchestrator: you hand it your robot as a set of MCP tools, it turns a plain-language command into a plan, calls your tools, watches the run, asks when unsure, recovers from failures, and learns. You never touch the LLM — you just expose good tools and drive it from a web page.
This page is about driving a brain that already runs: the mental model, the layer model, the chat + Debug panel, the Visual Command / observer view, the Programming view — where you build a plan yourself and no model is involved at all — keeping the plan a run just used, and how it learns from you. The neighbouring pages own the rest:
| You want to | Go to |
|---|---|
| Run, update or configure an instance | Running a brain |
| Set up your own robot + sim (the brain already runs on the 4090) | Set up your own robot + sim |
| Run the simulator — on Linux, a VM, or a Mac | Running the simulator |
| Connect your robot — every tool + return field | Integrating a robot |
| Understand why the system is shaped this way | Architecture |
You know how to write robot code and wrap it as MCP. That's all you need.
First visit: it will ask you for an operator key
A brain can move a robot and spend money, so it only accepts operators of its deployment (BIN-534). The first time you open one in a browser you get "This brain is locked" and a single field. Paste the key — whoever set the brain up has it, and it is one key per deployment, not per person — or follow the link to sign in under Admin if you have the admin password instead. It is kept in that browser, so you enter it once per device.
Where the key comes from, and what to do when a brain refuses you: Running a brain → the operator key.
Mental model¶
"bring the blue box to the workbench"
│
▼ (brain: plan → call tools → observe → react)
your MCP tools: locate_3d → get_grasp → pick → navigate_to → place
│
▼
your robot code / sim does the actual sensing + acting
The brain is high-level only — it issues intent and awaits results. It never runs a tight control loop; your controllers do.
Where competence lives (the layer model)¶
The brain is one layer of a stack, and each layer owns a different kind of knowledge. Putting a capability in the right layer is what keeps the system maintainable: one brain image serves every robot and customer, and making the robot better at a task never means changing the brain. The full five-layer model — and the abstraction seam between them — is Architecture §2; this is the short version you need to drive it.
── CORE BRAIN ──────────────────────── one shared image; robot/
conversation · planning · a generic task/customer-agnostic.
plan runtime (data-flow · success Parameterized only by
gates · loops · survey/retry) · CONFIG (prompt fragments ·
oversight · memory plan templates · named
places · model · servers).
│ discovers + calls, over MCP
▼
── SKILL / TASK SERVICES ───────────── per object/task — the
one high-level tool per competence, "competence layer".
e.g. grasp("coke can"): The closed loop lives
perceive → grasp → check → refine INSIDE one MCP tool.
→ retry, until verified or honest fail
│ built on
▼
── CAPABILITY PRIMITIVES ──┬── PERCEPTION / WORLD MODEL ──
move · navigate · pick │ ground · segment · localize ·
· place · get_pose — │ grasp geometry · scene capture
atomic, self-verifying │ · get_frame. Per sensor/model,
per robot / gripper │ swappable; may host its own
│ AI models
Rules of thumb:
- The brain orchestrates intent; it never pilots a control loop. Anything real-time or robot-specific lives below it.
- Competence lives in the lowest layer that can own it end-to-end and verify
itself. "Reliably pick up a coke can with this gripper" is neither a brain
feature nor a bare
pickprimitive — it's a skill servicegrasp("coke can")that perceives, grasps, checks the grip, and refines/retries (nudge the base, try the other arm) until it verifies success or gives up honestly. It calls thepickprimitive + grasp-pose perception underneath, and may bundle its own models. The brain just calls the one tool; the plan stays a single step. - A refinement loop belongs in the layer that holds the feedback signal — the skill service or the primitive — never in a prompt.
- Everything robot/task/customer-specific is a registered MCP server or config, never a fork of the brain. Add generic capability to the brain (everyone benefits); add specific competence as a service or config (isolated — changing task A on robot B for customer C touches only that service/config).
- A skill service is just another MCP server the brain discovers and calls like any tool — so you add the competence layer exactly the way you expose primitives.
Connect your robot¶
Connecting a robot means exposing it as MCP (SSE) servers on the tailnet, making
your tools coarse/blocking/self-verifying with clear descriptions and structured
returns, and shipping your robot's prompt via a prompt://system resource. The
full connect-a-robot contract — every tool + return field, how to publish over the
tailnet, the layer model, and each optional capability — lives in
Integrating a robot. In short: MCP SSE servers on the tailnet bound 0.0.0.0;
coarse/blocking/self-verifying tools; local safety independent of the brain; the
expected tool set (or adapted prompt/plan templates); then ask the admin for an
instance (Running a brain).
The planner LLM is chosen per robot, by the admin
The planner (Claude Opus vs a Groq model) is set per instance at launch, not in the robot's code. You provide the tools + prompt; the admin picks the brain's model (how).
Who it thinks it is — profiles¶
The picker in the top bar (the ⧉ Layers control) switches the brain's profile. A profile is not only a persona: since BIN-419 it also decides what the brain is allowed to do, so the same instance can be shown to a customer without the developer surface. Three ship in the image; the file that defines them, and how to override it per instance, is running the brain:
| Profile | What it unlocks | What it is for |
|---|---|---|
| Binabik | everything | the generic profile — what you use day to day |
| Audi | Missions + Statistics | a customer demo: one material-handling use case |
| Körber | Missions + Statistics | a customer demo: the three Philip Morris tasks |
Switching is immediate and takes effect on your next turn; nothing needs restarting.
Each profile carries an allow-list of features it has unlocked, and anything not on it is locked. The full list of features, and which file to edit, is running the brain; what follows is what a locked one looks like from the outside.
No robot control. A customer profile is handed no tool that can make the robot act. It is not a model politely declining — asked to move something it says the capability is coming and lists the use cases it does offer, and there is no path from that conversation to the robot. That holds even if the model names a tool it was never offered: the request is refused where the action would happen, not only where the option is presented. Such a profile also opens a fresh chat on its use cases as clickable options instead of an empty box, so an audience has something to press.
No spend anywhere. The spend chip disappears from the top bar and Admin → Cost is gone from the nav. Hidden rather than blurred, deliberately: a greyed-out money chip still tells a customer we are counting dollars, which is the thing being withheld.
Most of the admin menu is gone. Config, Credentials, Logs and Skills are absent — machinery, not capability, and nothing a customer should be reading. So is Demo events, which fires tool-call cards the robot never produced: a debugging aid at your desk, and a lie the UI tells on your behalf in front of an audience. Missions, Statistics and the API reference stay — what the robot has been asked to do, how it performed, and that it has an interface at all.
That applies to the overview page you land on after logging in, not just the sidebar: its tiles are the same list of pages and answer to the same rule, so a locked page has no tile either. And the API reference is genuinely reduced rather than merely shown — the OpenAPI schema behind Swagger and ReDoc is filtered to what the profile has unlocked, so the endpoints behind Cost, Credentials, Config, Fleet, Demo events and Waypoints are not listed in it.
Two tabs are gone from the main UI's right-hand rail: Config and Waypoints. If one of them is open when you switch profile it closes itself, rather than sitting there with no tab left to dismiss it.
And the UI can wear the customer's colours. A profile may carry an accent colour that
replaces Binabik red while it is active, and a logo shown beside ours as a Binabik ✕ Körber
lockup — ours stays, because it is our brain running their use case. Körber also switches the
body font to Noto Sans. Their title faces are licensed corporate typefaces we cannot ship, so
headings stay on ours; the details are in
running the brain.
Fleet and Recoveries stay, blurred. They keep their nav entries (marked soon) and render behind a Coming soon card, because a customer should see those capabilities exist. The blur is unreachable for a keyboard and a screen reader too, not just a mouse — and the routes behind those pages refuse independently, so the blur is a curtain rather than the thing doing the work. The "Remember this run" control is gone rather than greyed out, for the same reason as the spend chip.
Everything is enforced on the routes, not only in the UI. A locked feature's endpoints answer 409, so no removed button is the only thing between a customer and a write.
One thing a profile switch cannot undo
Fleet registration happens once, when the instance starts, using the profile it booted
with — before anything is selectable. So switching a running developer brain to Körber stops
every further write but does not un-register the robot it already announced. For a real
customer instance, make their profile the first one in that instance's prompts file
(or --set scenarios_file=…), and it never registers at all.
Drive it¶
Open your instance's HTTPS URL — the Tailscale-Serve one, https://<host>.<tailnet>.ts.net
(brainctl ls prints it as open <name> → https://…). It's a secure context, so the
full UI works and the app is installable. Plain http://<host>:<port> also works but limits
browser features that need a secure context (voice input, PWA install). Then type a command and
open the Debug panel to watch it: streamed reasoning, the compiled plan, each tool call, and
the vision watchdog.
If your instance was started without one or more of its models, some of this is simply not there — the view goes away with the model behind it, and the Programming view is always available. See when a brain runs with its models switched off.
All five panels — Debug · Context · World · Waypoints · Config — hang off the same rail of tabs down the right edge, one open at a time. Their labels are written sideways and the tabs share the rail's height, so on a short window the rail switches to icons rather than clipping five words into five small slices; hover any tab to see which panel it is (BIN-365).
- It asks when unsure ("which shelf?") — answer in chat, or on the question card that now interrupts the Visual view too; it then plans. The mission does not advance until you answer, and if nothing comes back it eventually closes itself out incomplete.
- Every question is a decision box — the answers it will act on as buttons, plus a free-text field when none of them is true. One card, and clicking an option is the whole answer; details under clarifications.
- Stopping is a barge-in. Type or say stop (or use the UI Stop control): the robot halts at once and the plan aborts at its next step boundary. The brain then asks Continue / Abort — as a decision box like any other question, so a click ends it.
- It's honest — if a request needs a capability you didn't expose as a tool, it says so instead of flailing.
- The waveform means it can actually hear you. The bars in the top bar appear only when speech-to-text is genuinely live (BIN-121): an STT provider is configured, a voice modality is on, the socket is up, and the audio hardware that modality needs is there — the robot's probed mic (plus its speaker, since replies play on the robot) for server-side voice, or your browser's mic permission for browser push-to-talk. On a chat-only brain the top bar stays empty, because breathing bars there read as the robot is listening when nothing is. They also show the correct state the moment the page loads, not just after the first turn — a brain with an always-on VAD mic shows listening immediately.
Reading the attempts — "Attempt 2 / 3"¶
The Debug panel shows one card per planning attempt, with ‹ Prev / Next › to page between them. Each card carries that attempt's own reasoning, its compiled pipeline with per-step status, its sub-module telemetry and the watchdog findings observed while it ran — so you can compare what the brain thought and did before vs. after a failure.
An attempt is a plan. Not a turn, not a retry.
The card count is the number of plans the brain compiled, and nothing else can move it. Three things it deliberately is not:
- not a planner turn. The brain takes turns that produce no plan at all — narrating
a run that just finished, asking a clarifying question, calling
finish_mission. None of those is an attempt. Counting turns is what put a summary of a whole mission alone on a card with no pipeline, while the attempt that did the work showed nothing (BIN-102, round two). - not a skill's own retry. A
grasp()that tries twice internally is one step of one attempt — itsattempt=2lines are the skill's counter, inside the box nested under the step. Likewiseafter_attempts=2on anescalate, and the recovery tier'sstep=Nlines, which carry no counter at all. Three different counters, one card. - not a step re-run. A step the plan retries reuses its slot in the pipeline; the nested box shows the run in flight, and the attempt keeps the earlier one in its log.
What to expect while it runs:
- Reasoning fills in live, on the attempt it belongs to. A replan's "that broke, here's what I'll do instead" reasoning streams into the attempt it produces — it moves there the moment that plan compiles, because the attempt exists only once there is a plan in it. The closing narration of a run that gives up stays on the attempt it is describing.
- Two thoughts are separated. Successive planner turns are separated by a blank line rather than glued together mid-sentence.
- An attempt keeps its pipeline for its whole life — on attempt 3 as much as attempt 1, including while the brain is thinking about something else.
- Cards only ever appear. The count never renumbers or shrinks mid-mission, so a card can't vanish while you're reading it — and it never runs ahead of the truth either: there is no transient "Attempt 3 / 3" that a later frame takes back.
The brain decides where the boundaries are and stamps the index on every transparency
event (agent/attempts.py); the browser only ever files an event under an attempt that a
compiled plan already opened. A mission that works first time shows a single "Attempt" with
no nav; a mission that replanned twice shows exactly three cards. The Visual view's
Reasoning panel deliberately shows only the current attempt — the Debug panel is where
you compare them.
Steps the brain did not plan — auto-N¶
Most steps come from a plan the brain compiled up front. A step whose id starts with auto-
did not: the planner called a skill (grasp, pick, place) turn-by-turn instead of planning
it, and the brain promoted that call into a step so it would show up here at all.
That promotion exists because of what used to happen instead. On rap-0 the planner gave up after
two plans were lost and called grasp directly — and the mission's only real manipulation ran
with no step, no status and nothing streaming into this panel. The robot picked up the box and the
Debug window showed an empty mission.
So an auto- step is not an error. It means "this ran, and it was not part of a compiled plan" —
which is worth knowing when you are reading a mission back, and used to be invisible.
Only skills are promoted. The brain looking, reading, asking or navigating between steps is ReAct working as designed and creates no step, because a pipeline full of unplanned reads would drown the plan you actually want to read.
What a step's status actually claims¶
A step goes green when the tool reported success, not merely when the call came back. Those
were the same thing until BIN-379: succeeded meant "nothing raised", set without the result
ever being read. A tool that answered {"ok": false, "error": "NavigationError"} went green.
It is worth knowing what that looked like, because it is the failure you cannot see by reading the mission. Asked to "turn left a little", the robot's nav stack vetoed both spins on the costmap and turned 5.7° of a requested 30°. Both steps showed succeeded. The mission summary was honest — "the nav stack aborted each full spin … but the robot did rotate left a little" — only because the planner read the detail itself and said so. The status, which is what the panel, the mission record and the KPIs all read, said the opposite of the result it was carrying.
A red step is not the same as a failed mission
The planner still decides whether a partial result satisfies the instruction, and it should: 5.7° may well be "a little". So a mission can finish succeeded with a red step in it, and that combination is not a contradiction — it is the planner having exercised judgement the executor no longer exercises for it. What changed is that the judgement is now visible instead of being applied silently before anyone else saw the result.
A step also stays green for a tool that answers in plain prose, or one that never sends an ok
field at all — most do not. Only an explicit ok: false turns it red, because guessing the other
way would redden nearly every step in the system.
If the Reasoning pane is empty, check thinking_display first
The planner's reasoning is Claude's thinking summary, and on this model family the API
returns thinking blocks with empty text by default (display: "omitted"). The brain
therefore asks for thinking_display: summarized in
config/llm_planner/anthropic.yaml. Set it back to omitted and every turn whose only
output is a tool call goes silent — the pane then shows nothing at all until a turn that
writes prose, which reads as "attempt 1 has no reasoning". Thinking is billed the same
either way, so there is no cost reason to turn it off (BIN-102).
Sending the whole mission to someone else¶
The copy button at the Debug panel's top right puts the entire mission on your clipboard as JSON — the sibling of the World panel's, in the same corner, and free in the same way. Paste it straight into a Linear issue or hand it to a tool.
It copies every attempt, not the one you are looking at. This is the point of it. You page between attempts one at a time, so the plan that failed and got replanned away is two clicks off-screen exactly when it is the thing worth sending — and transcribing it out of a narrow scrolling dock by hand is what people were doing instead. What lands on the clipboard is every attempt's reasoning, strategy and outcome; every pipeline with each step's status, detail and how many times it ran; the sub-module and watchdog telemetry, including the run of a retried step that the nested box has stopped showing; the watchdog's state; and what the planner saw and was told.
A few things it says out loud rather than leaving you to infer:
- Whether there was an image, not the image. You get
had_frame, the caption, what triggered it and whether it was the billable annotated overlay — never the encoded picture, for the same reason the World panel leaves camera frames out. It also tells you it is the mission's last frame rather than a per-attempt one. - What it could not read. An endpoint that did not answer comes through as
read: falsewith a note, so a section that is missing from the brain never looks like a section that was empty. - What it truncated. The browser keeps a bounded tail of telemetry per attempt; if an attempt hit that bound the export says so, because a tail pasted as the whole channel reads as this is everything that happened.
- Anything that looked like a secret is redacted on the way out, since a plan's tool arguments go in verbatim and this is a payload meant to be pasted in public.
The planner and observer prompts in it are the static layers as this brain composes them right now. The per-mission additions — matched process documents, recovery lessons, the situation block — are appended when a mission starts and are not on that endpoint; for the complete per-mission record use Admin → Missions, which has its own untruncated export.
The button is enabled whenever a mission has run, including one that failed — that is when you need it — and only greys out when nothing has run at all. If it says Copy failed, it is the same plain-http clipboard problem described above: open the brain over its HTTPS URL and try again.
Seeing inside a skill — the live sub-module box¶
An L3 skill-service owns a whole competence behind one blocking MCP call, so a step
like grasp(the coke can) would otherwise sit there opaque for the minute it runs. It
doesn't: the skill streams its own progress and the Debug panel nests it as a second
box under the step that invoked it (violet, collapsible, newest line at the bottom):
▶ 5 Grasp coke can
grasp()
⤷ grasp + surveillance — sub-module learn · 12.4s
0.4 locate object=the coke can tool=perceive stage=coarse
1.1 strategy object=the coke can name=side
3.8 plan_execute approach=side group=arm stage=pre attempt=1
5.0 set_gripper approach=side side=left state=open attempt=1
6.1 vlm the gripper looks high above the can
7.2 escalate after_attempts=1 reason=missed
9.6 recover step=1 tool=compute_grasp_joints
12.4 learn recovered=True promoted=True
So you can watch the grasp choose its approach, walk its side/top/two_hand state machine, miss, escalate to the recovery tier, and record what it learned — live, while it happens, instead of only seeing the one-line result afterwards. Two details worth knowing about the lines themselves:
- Each primitive line says which call it is (BIN-128) —
stage=prevsstage=grasp,side=left state=open,group=upper_body octomap_guard=True. Without that, thetwo_handFSM's pre-grasp pass and its squeeze are the same line and you cannot tell which one is stuck. It is a short whitelist by design: joint arrays and poses stay off the channel (see the grasp-service spec). - The watchdog's observations are on the same lines (BIN-127) — the amber
vlm/propertyentries above. That's the point: you see the watchdog complain while the grasp was closing, not only in a separate window afterwards. A line is a ticker; hover it for the full text.
How it works, end to end:
- the skill calls
ctx.report_progress(...)per FSM phase, with the event as JSON in the message (MCP progress notifications carry no structured field); - the brain passes a
progress_callbackonsession.call_tool, which is what puts aprogressTokenon the wire — without it a skill'sreport_progressis a no-op; - the notification lands on the MCP backend's own event-loop thread and is marshalled
onto the app loop, then emitted as
submodule_progresskeyed by the pipeline-step index the call belongs to.
This is THE one channel, not a grasp special case: any sub-module that reports MCP
progress shows up the same way (a plain-string reporter renders as free text), and since
BIN-127 the surveillance watchdog rides it too — it is a brain-side producer rather
than an MCP one, tagged with the step that was executing when it observed, and carrying a
severity (info / warn / alert) so a warning stands out. The Surveillance window is
simply the source == "surveillance" slice of the same store, so the two views can never
disagree. There is no separate surveillance_finding event.
A step that restarts (a loop retry reuses its index) starts a fresh box, so you see the current run rather than both — but nothing is discarded: the watchdog's log is mission-long and keeps what it saw during the earlier iteration. Two side-effects worth knowing:
- A reporting tool gets an idle timeout, not a total one. While progress keeps
arriving the brain knows the skill is alive and each notification restarts the clock
(
call_timeout_s, 180s on a deployed brain); silence for that long still aborts. This matters because a grasp's own safety-net deadline is 480s — longer than the brain's per-call timeout, which used to cut a long recovery off mid-FSM. - Nothing extra to enable, and no polling. The telemetry is pushed by the skill during a call you already made, so it costs no external API calls.
Prefer a compiled plan if you want this: on the ReAct path the box needs the
planner to have tagged its call with a step_id, otherwise there's no step to nest under.
See it with no robot
Admin → Overview → Replay debug window replays a scripted two-attempt mission (fail → replan → succeed), including a full grasp FSM (approach → miss → recovery → learn) under its step, a watchdog alert, and a reasoning turn that streams while a plan is executing — so the per-attempt rendering is exercisable too. Nothing but the brain needs to be running.
Reading the Surveillance box — four badges, four different questions¶
The watchdog's window carries four indicators that are easy to conflate, so it is worth knowing which question each one answers:
| Indicator | Question |
|---|---|
the armed / off badge |
is the eye on, i.e. will the watchdog watch during a mission? |
● watching and the amber ring |
is the model watching right now — and therefore billing? |
● body watched and a sky ring |
are the free body checks running, with the camera unwatched? (any mission, either planner path) |
VLM polling (billable) · robot state polling |
which of its two sources is feeding it this tick? |
robot state is the watchdog's proprioception input — the robot's own joints, grip and
effort, read locally and for free. The VLM is a paid model call every tick. That asymmetry
is why only one of them is marked (billable), and why both say polling / idle rather
than on / off. Hover either dot for whether that source is merely connected or actually
being polled.
What you are protected by with the eye off. A mission in flight is watched either way: the
deterministic body checks read the world snapshot every tick and can stop the robot on their own.
That is what ● body watched means, and it is the state of most missions, since the paid eye is
off by default. What you lose by leaving it off is the picture — nothing is looking at the
camera, so "the box slid off the table" or "someone walked into the workspace" will not be
noticed. The two failures are different in kind, which is why they get separate indicators and
why only the paid one gets the amber alarm colour.
Disarming the eye is not the same as an instance that has no eye. The armed / off badge
is about the toggle you can press; an instance can also be started without the eye, or without
the body checks, or without both — and then the box's surfaces go away rather than reading
off. Which of those you are looking at, and why, is
when a brain runs with its models switched off.
Every mission, not just the ones the planner compiles (BIN-416). The brain has two ways of
running a mission and it picks between them itself: it either compiles a plan and hands it to the
state machine, or it drives the tools turn by turn (ReAct). Nothing in the chat distinguishes
them — you get a step list either way — and the choice is not stable, so the same instruction can
go one way today and the other tomorrow. Until BIN-416 the watchdog started only on the compiled
path, which meant whether the robot was watched was decided by that invisible coin-flip. If you
are on a brain that predates it, an empty Surveillance box during a running mission is that bug,
not an idle watchdog. The one way to tell after the fact is the trace: a compiled run records
plan_run_start, a ReAct one does not.
On a ReAct mission a confirmed finding halts the robot and is put to the planner as an
interruption, rather than stopping a plan — there is no plan to stop. You will see the halt in the
activity log as Watchdog: <what it found>.
A finding that will not clear halts once (BIN-434). The behaviour above deadlocks if the
condition persists: the watchdog halts, the planner reacts by commanding a motion whose whole
purpose is to clear the condition, the condition is still true when that motion starts, so the
watchdog halts the recovery as well. Seen on rap-1 as four halts in a row, with the brain's own
mission summary reading "Attempts to home the arm repeatedly tripped the safety watchdog." So a
soft finding now interrupts on its first confirmation only; after that it keeps appearing in
the Surveillance box every tick but stops halting, and the trace records the repeats as
watchdog_interrupt_suppressed. What this looks like in practice: one Watchdog: … line in the
activity log rather than one per attempt, and a mission that gets to try its recovery.
A hard finding — e-stop, self-collision, a declared driver fault — is exempt and halts every single time, because the robot should not be moving at all and there is nothing to recover into. So if you see repeated halts, the condition is a hard one and the robot is telling you it is not fit to move; if you see one halt and continued reporting, it is soft and the brain has been left room to act on it. The distinction resets per mission.
Before BIN-377 the eye being off meant nothing was watching at all
The body checks landed in BIN-290 and were documented — here and in the code — as running
"every tick whether or not the paid observer is armed". They did not run at all: the loop
that reaches them was itself started only when the observer was armed, and the observer is
off by default. Verified on brain-pascal/rap-1 with a real grasp in flight —
/api/surveillance reported active: false for the entire mission, and three complete
missions' traces held zero body records. If you are looking at a brain that predates
BIN-377, ● body watched will never appear and the body checks genuinely are not running.
A brain with no world service attached is still unwatched with the eye off, and says so in those words ("nothing is watching this run"): there is no snapshot to check.
Where the looking happens — and where the bill lands (BIN-290)¶
The watchdog's visual half used to run on the robot, and the brain received one sentence about a picture it never saw. It now pulls the frame itself and forms its own judgement over the image together with the body measurements — which is what lets a single finding say
"the arm has not moved in 8 s and the image shows the gripper still open"
instead of "the arm is stuck". Only the first can be acted on, because only the first can be checked against what the joints actually report.
Two consequences you will see:
- The bill moved from the robot to the brain. It is the same one model call per tick it always was, but it is now the brain's model and the brain's key. The armed status says which side is judging, so the cost is attributable on the screen where it is incurred.
- During a grasp it looks through a wrist camera. The head camera usually cannot see the arms, so a grasp going wrong is precisely what it cannot show.
There is no fallback, and that is deliberate. The brain runs two models — the planner that decides and the watchdog that checks — and both need the world state and an image. So three situations are treated as something wrong with the stack, not as a quieter kind of watching:
- the brain's model cannot read images
- no world service is attached, so there is no frame and no body snapshot
- no frame arrived (the camera, the vision service or the adapter is broken)
Each says which one it is, in words that name the fix, and it appears in the findings feed rather than in a log nobody reads. After three ticks like that the plan stops — a mission running under a watchdog that cannot see is a mission nobody is watching. One hiccup is tolerated, the same way one jittery frame is.
You will also see the watchdog report itself inactive in that state, even though you armed it. That is on purpose: an armed indicator over an eye that cannot open is worse than an honest "not watching".
If the model answers with something unreadable, that is reported as a fault in the observer — with what it actually said — and never as a healthy robot. It does not count against the robot either: the strike count is left exactly where it was, so a garbled reply can neither erase two genuine bad ticks nor invent a problem out of the watchdog's own flakiness.
A billable indicator is derived, never remembered (BIN-165)
All the live badges are computed from GET /api/surveillance (enabled, active,
observing) plus whether a mission is in flight. None of them is drawn from the socket event
alone,
and that distinction is the whole of BIN-165: agent_active is an event saying who took
the floor, nothing ever un-says it, so with the eye off the UI sat on ● watching,
MONITORING in the Visual view, and an orange Surveillance · watching execution for
the rest of the session — while the API correctly reported
{"enabled": false, "active": false}. Nothing was being billed; the indicator was
simply lying, which is the more corrosive failure, because an indicator that cries wolf
is one you learn to ignore on the day it is telling the truth.
The derivation is deliberately biased toward the truth in both directions: an
authoritative live watch loop shows even with no socket event (a lost event must never
hide a bill), while a disarmed eye or a finished mission wins on its own and no stale
event can override it. Practical consequence: disarming the eye clears every one of
these indicators within a round-trip, and a mission that ends any way at all leaves
them at rest. If you ever see ● watching again while /api/surveillance says
enabled: false, that is a bug, not a display lag.
An autonomous run with the observer off is labelled as what it is — Executing plan ·
observer off — body checks only, the camera is not being watched — instead of borrowing the
watchdog's amber dot. The cost badge reads observing, not active: since BIN-377 a
watch loop runs on every plan for the free half, so active is true all mission long with
nothing being spent, and a bill drawn from it would be BIN-165 in reverse — an operator told
they are paying when they are not stops believing the marker just as fast.
Visual Command + observer view (the camera GUI)¶
The header's view switch has three segments — Chat · Visual · Program (the third is the
Programming view). Visual switches the main area to a live head-camera view
with the scene segmented + labelled (needs your get_frame/capture_scene tools — see
Integrating a robot §3h;
stream_start/stream_stop are optional on top, and make it a real ~30 fps stream instead of a
picture every 0.7 s — see Two camera views).
Two things in one:
- Direct manipulation — click an object to Pick, tap a spot to Place, pick a named place to Navigate, drag an arrow to Arrange, and jog the base. Each click becomes a grounded mission through the planner.
- Observer / livestream mode — a top bar with a ● LIVE badge, a status pill (Reasoning / Executing / Monitoring / …), and two toggles: Controls and Reasoning. Turn Controls off for a clean broadcast shot — the tool buttons and jog pad disappear and the robot's chain-of-thought + the live plan render in translucent glass panels over the camera stream (plus per-arm hold status and a speech caption). This is the marketing/stream view: camera + segmentation + what the robot is thinking, with no operator chrome.
The reasoning/plan overlay is brain-side (it reads the mission + reasoning
streams), so it needs no extra robot work beyond the camera tools. Preview it with
no robot at http://<host>:<port>/?mockVision=1 — add
&mockSegmenter=box&mockModal=ready to also preview the
segmenter surfaces
without a robot in that state, or &mockAsk=1 to see the
question interrupt on a mission that stops and asks.
Two camera views, and how to tell which one you have¶
The head camera reaches you one of two ways, and the difference is the difference between teleoperating and watching a slideshow.
| What you see | How it gets there | |
|---|---|---|
| Live stream | smooth, ~30 fps | H.264 straight from the robot to your browser over the tailnet — the brain is not in the path at all |
| Picture feed | a new picture about every 0.7 s | the robot answers get_frame, the brain relays it to you |
The stream needs three things: it must be switched on in the brain, the robot must be running its stream sender, and your browser must be able to reach the robot over the tailnet. The first two are on by default as of 2026-09-11 — a current brain offers the stream, and a robot running the vision service runs the sender — so on a healthy pair you should simply be watching video. When any of the three is missing you get the picture feed — the view still works, it is just slower, and the browser falls back on its own without you doing anything.
You do not have to guess which one you are on. The view says so whenever a stream was on offer and did not work, in the words of whichever side declined:
- "The live stream is switched off on this brain." Nothing is wrong with the robot — ask
whoever runs the brain to set
video_stream.enabled— it is on by default, so someone turned it off. - "This robot advertises no
stream_starttool, so it cannot send a stream." The other end. Usually the robot has no sender installed (it needs a GStreamer stack that is not part of the ROS environment) or is not running the vision service at all — seeSTREAM_ENABLEin the R1 robot stack. The robot's ownr1ctl upsays which. - "The robot could not start a stream: …" — the robot tried and said why, usually that its camera has not published a frame yet.
- "The stream connected but no video arrived." The negotiation looked fine and no picture
ever decoded — usually a network path that will not carry the media, so check that your
machine and the robot are both on the tailnet. Being on it is necessary and not sufficient:
Tailscale relays through a DERP node when it cannot make a direct connection, which is slower
and lossier and which nothing on this page can show you.
tailscale ping <the robot>from a terminal will say —direct connection not establishedmeans you are on a relayed path, and a stream that stutters only for you is the expected symptom of one. A spectator who has never granted this page microphone access is a little more exposed to this, because browsers hide some of the address information the robot needs until you do. Over the normal HTTPS URL the brain supplies that address itself, so it should still work — and granting the mic once removes the question entirely. Over a plain-HTTP URL it cannot: the brain has no way to learn where you are without the HTTPS front end, so a viewer who has not granted the mic gets the picture feed there. Another reason to open the HTTPS URL.
Not in a hardened browser
ungoogled-chromium and similar builds disable the address-gathering the stream depends on
outright, and report a healthy connection carrying nothing. You will get the picture feed
every time. Safari, Chrome and Chromium all work.
One more, if you are testing against a development brain over an SSH tunnel rather than
the HTTPS URL: Safari does not treat http://localhost as a secure context, so it cannot
grant the microphone there and cannot expose the candidates the stream needs. Chrome and
Firefox do. Measured 2026-09-10.
When the picture feed is behind, it says so¶
This part is about the picture feed only — a stream that stops being watchable falls back to the picture feed rather than freezing, so the badges below are what you see afterwards.
A picture fetched every 0.7 s means that when the robot is slow to answer — or its camera stops producing new pictures — the last one simply stays on screen. Nothing about it looks different from a live one.
That is what made this worth fixing rather than explaining. Driving the robot with the jog controls would refresh the world state — base pose, posture, joints — while the picture sat perfectly still, so the one surface that should make teleoperation feel direct was the one lagging, with no way to tell the difference from a robot that simply was not moving.
So: once the picture is clearly not current, it dims and a badge says why. Two different stalls, and it tells them apart because they mean different things:
- "No new camera frame for 2.3s — this image is not current." Nothing is arriving: the robot is slow to answer, or the connection has stalled.
- "The robot is sending the same image repeatedly — the camera view is frozen." Frames are arriving on time, and they are all the same picture. That is the robot's camera, not the network.
Dimmed, not blanked
The last known view stays on screen, because while you are driving it is still the most useful thing you have — it just must not read as live. It brightens again the moment a new frame lands.
Neither badge appears on a live stream
Both of them measure a poll: how long since a picture arrived, and how many identical ones the brain has counted. A video track has neither, so left running they would call a perfectly live 30 fps view stale within two seconds. Liveness on a stream is answered differently — if the video stops decoding, the view drops to the picture feed and tells you, rather than sitting on a frozen frame claiming to be live.
This does not make the view faster
It makes the view honest. If the camera lags while you drive, that lag is still there — you can now see it rather than mistaking a stale picture for a stationary robot. The real fix is streaming the camera properly (BIN-262).
The arm chips (bottom-right) — what "empty" does and doesn't mean¶
Two chips, one per gripper, showing what the robot is holding:
| Chip | Meaning |
|---|---|
LEFT · empty |
that gripper holds nothing as far as the brain knows |
RIGHT · coke can |
held in that gripper — a one-arm (side or top) grasp |
both chips · both, same label |
one object held between both grippers — a two-hand box grasp. Place puts it down with both and does not ask you which arm |
GRIPPER ? · mug |
the robot confirmed the grip but never said which gripper. Still placeable |
The chips are inferred from what the robot reports for each pick / grasp / place —
nothing polls the grippers. A hold that starts or ends outside the brain (you moved the arm
by teleop, the object slipped, the robot or the brain restarted) therefore won't show, so
read an empty chip as "the brain has not been told otherwise" rather than as a sensor
reading. The one thing it no longer does is sit empty through a successful grasp —
including a box, which reports side: "both" and used to label neither gripper.
A pick or place puts a wrong chip right; there is no resync button, because there is
nothing robot-side to resync from. What a robot must report for this to work is the
integrator's half of the contract:
Integrating a robot §2c.
The app's top bar peeks over this view — pull it down by the grip (BIN-163)
The brain's top bar is unpinned by default: it collapses to a thin gutter so the camera runs edge-to-edge. Two things to know while driving from this view.
- Pull it down deliberately. A small chevron grip hangs from the centre of the top edge. Click it (or tab to it and press Enter) to bring the bar down; click it again, or press Escape, to send it back. Resting the pointer on the grip for about a third of a second also opens it — but merely moving the pointer past the top edge does not, which is the point: until BIN-163 the trigger was an invisible full-width strip, so reaching up for the Controls / Reasoning / Segmentation toggles was itself the gesture that dropped the bar on top of them, and neither the bar nor the toggles could be read.
- The view's chrome steps aside, the camera does not. While the bar is down, this view's own top-edge overlays — the LIVE badge and status pill, the toggle stack, the notice column, the Reasoning and Plan panels, a collapsed question badge — slide down far enough to clear it, so you can read both at once. The camera frame itself never moves or resizes; only the floating chrome shifts. The dark band the chrome sits on slides with it, so the toggles stay as readable over a bright scene as they are with the bar closed — and the video is never dimmed further to achieve that (BIN-168).
- A cost warning is never underneath the bar. When a billable service is running, its warning strip (with its off switch) sits under the top bar on every route. The peeked bar now comes down below that strip and passes behind it, so opening the menu can never hide what is costing money, or put the switch that stops it out of reach (BIN-168). With no warning showing, the bar behaves exactly as before.
Prefer it always visible? The pin button in the bar keeps it there permanently, and the choice is remembered. In the installed PWA drawing into the OS titlebar (window-controls-overlay), the bar never collapses at all — the OS window buttons own that edge.
Segmentation runs on request — and automatic re-scans are bounded (BIN-139)
Every capture_scene costs money: it always runs the Gemini-Robotics-ER grounder
(plus segmentation), so a capture is billable even when local SAM on a GPU draws the
masks. The camera stream itself (get_frame) is free.
The brain re-segments on its own in only two cases, both bounded:
- Once the robot stands still, after it moved — one capture, because you moved it. Standing still is when you look at the view and reach for an object, so the overlay is bought just before you use it, and it is fine that this costs a world-state update (BIN-391). It is not exempt from the empty-scene budget (BIN-158): around a scene that has objects every jog re-segments, but on a scene that keeps coming back empty it stops after the retries below rather than billing a call per jog.
- On an empty scene, a couple of retries and then it stops. A scene that genuinely contains nothing would otherwise re-capture every ~6 s for as long as the tab stayed open (≈600 paid calls/hour); until BIN-139 it did exactly that.
When it stops you'll see "No objects found — automatic re-scanning stopped" with a Scan again button, rather than an endless "Scanning…". Clicking Re-segment / Scan again is always allowed and renews the allowance, as does leaving and re-entering the view. So: if you want a fresh scan, ask for one — the robot will not keep paying for retries on your behalf.
The overlay drops the moment the robot moves — and won't be clicked while stale (BIN-158)
The masks were captured at one robot pose. As soon as the robot drives, the camera image moves on but the outlines do not — they end up sitting over whatever is now at those pixels, still carrying their old labels. Because the overlay is the click-to-pick target, clicking a drifted mask would send a pick at a 3-D point that is no longer that object, so a confidently mislabelled overlay is worse than no overlay.
What you'll see instead, from the first moment of motion — a jog, a drive_by, an
approach, a grasp, arm motion, whether you drove it yourself or a mission did:
- the masks and labels go away (if any are still on screen they render greyed and dashed, labels struck through — a record of where things were);
- an amber notice: "The robot moved — the overlay is out of date." with a Re-segment now button;
- clicks are refused. Clicking an outline prompts "re-segment first — that outline
is where the object was, not where it is" rather than commanding a pick. The brain
refuses it on its side too (a
SCENE_STALEerror), so this doesn't depend on the browser being up to date.
The overlay comes back on its own once the robot stands still, or immediately when you press Re-segment. What the robot moving also does now is tell the world state that its object inventory is outdated — free, and independent of whether anyone has the camera view open (BIN-391). A colleague who joins the view mid-drive gets the same stale signal — never the last good overlay, which is what BIN-158 fixed.
A Re-segment now refreshes the World panel too — and only costs one capture (BIN-564)
Pressing Re-segment used to fill only this view. The World panel, the planner and the surveillance watchdog all read the world state, and it went on reporting "scene never captured" over the freshly labelled objects on your screen — so the next thing that wanted an object list ground the same, unmoved camera view a second time. One press, two Gemini calls, about 15 seconds, for one picture.
The brain now hands the capture it just paid for to the world service, so one Re-segment is one capture and both views hold it. Two things you can see from this:
- the World panel's scene block fills in right after a Re-segment, and its
sourcenames your brain instance (brain-<name>) rather than the world service — that is how you tell a scene that was handed over from one it went and bought; - that capture shows up in the robot's shared fill budget under your instance name. It is counted once, not charged twice, and it is never refused on cost — but it does bring the robot's hourly ceiling closer for every brain attached to it, which is the point of a shared ceiling.
If the world service is down, disabled or refuses the hand-over, the Visual view behaves exactly as it always did: you get your overlay, at the same speed, and only the sharing is lost.
Before BIN-158 the trigger list still named tools from the retired :9206 capability
server and was missing approach and grasp, so a mission that drove with them left
the old masks on screen for the whole run.
Segmentation off means Pick and Arrange are off too (BIN-394)
The Segmentation toggle in the top-right stack is not only a display setting. Those outlines are the click targets — there is no invisible hit-box behind them — so with the overlay hidden there is nothing on the stage for Pick or Arrange to select.
Until BIN-394 nothing said so. The tool gate asked only whether the robot had found objects, which it had, so the button stayed enabled and the cursor stayed a crosshair over a stage with no targets: clicks landed nowhere, silently. That is indistinguishable from a broken Visual view, and it was reported as one.
Now the two agree. With the overlay hidden and objects available, Pick and Arrange are disabled with the reason on hover, and a notice on the stage says how many objects are waiting behind the overlay and offers Show segmentation to bring them back. Place and Navigate are deliberately unaffected — Place deprojects a free pixel against the robot's live cloud and Navigate picks a named place, so neither needs a mask.
The order of the wording matters: on an empty scene you still get "no objects — re-segment", not a complaint about the overlay. A hidden overlay is only the answer once there is something behind it to reveal.
Click the object, not our outline — and a dead click now explains itself (BIN-394)
When the segmenter returns a real mask the outline hugs the object, so "click the outline" and "click the object" are the same gesture. When it returns no mask the outline is a stand-in — a detection box, or a small dashed square around the object's centre — and an object is usually several times bigger than that square. A click aimed at the thing itself therefore missed, and nothing happened at all: no selection, no message. Labels on screen, Pick enabled, silence. That is what kept this issue open through three fixes.
A Pick (or the first Arrange) click now resolves to the object it landed on, and failing that to the nearest stand-in outline within about one square's width. When it resolves a near miss it says so on the stage, naming the object, so you can see that the view chose for you and correct it.
Three things it deliberately will not do:
- It never snaps to a real mask. A mask is a measurement of the object's extent, so missing one means you meant the gap between things. Only a stand-in — drawn dashed — is snapped to, because only there is the shape our guess rather than the segmenter's answer.
- It refuses an exact tie, instead of picking whichever of two equidistant objects came first in the list.
- It never resolves silently to nothing. A click that names no object says "No object there — click an object…".
Nothing here loses precision: a pick sends the object's identity, not your pixel, so where inside (or just outside) it you clicked cannot move the robot. And nothing here weakens the stale-overlay rule above — a stale overlay still refuses every object click, checked before anything else.
The same change fixed a second silence in this view: the "that camera has no depth" and "re-segment first" nudges were rendered inside the stale-overlay notice, so a refusal on a perfectly fresh scene displayed nothing. All of them now appear in one place, just above the tool palette, whatever the state of the overlay they are explaining.
The robot's questions interrupt this view — they used to be invisible here (BIN-160)¶
The brain asks a structured clarification when it is genuinely stuck — a question plus clickable options — and then waits: the mission makes no further progress until you answer, and after about a minute of silence the planner is nudged and eventually ends the mission incomplete.
Until BIN-160 that question was rendered in chat only. Visual Command — the view you are in exactly when picking and driving raise these questions — showed nothing, so a real mission asked "I can't reach any box, how would you like me to proceed?", got no answer, and closed itself out; from the operator's chair the robot had simply given up for no reason.
What you see now, whatever you have hidden:
- a centred question card over a dimmed camera — deliberately an interruption, not another notice pill, because ignoring this one costs you the mission. It carries the question, the options as buttons, and the free-text field;
- a waiting clock ("waiting 2m 14s") and an amber Waiting for you status pill in the top bar, so a blocked mission never looks like an idle one;
- Hide collapses the card to a pulsing badge — the answer often depends on what the camera shows — and the badge keeps saying how long the mission has been on hold. A new question always opens full-size again.
Clicking an option sends exactly what typing it in chat would, so the mission resumes the same way, and the chat transcript keeps the card as the record. Only the controller can answer; watchers see the question read-only, and it clears on their screen too as soon as the controller replies. One caveat: a hard browser reload loses the question (the transcript is client-side) while the robot is still waiting — answer it in chat.
Every question the brain asks is one of these cards, and it asks each one once (BIN-215). Two things used to break that:
- A safety stop asked in prose. After a barge-in the brain said "Stopped. Should I continue or abort?" as plain text with no buttons, so the one question you get while the robot is halted was the one you had to answer by typing abort exactly. It is now a Continue / Abort card.
- An option that reads like a command was taken as one. Options are the planner's own words, and "don't continue" is naturally phrased "Stop here (leave the box)" — clicking it tripped the stop-word barge-in, so the robot halted and asked continue-or-abort, i.e. put the decision you had just made back to you as a second question. Picking an offered option is now always an answer. Typing a bare stop or abort still halts the robot, even while a question is open — a one-word stop is a stop.
If the card says your answer didn't arrive, it didn't — send it again (BIN-197)
The card marks itself answered only once the brain acknowledges the message. If no ack comes back within 8 s it re-arms and tells you, so the answer can simply be re-sent.
That is not belt-and-braces. It used to mark itself answered the moment it sent, which made three different outcomes look identical: delivered, refused because this client is a watcher, and lost because the socket had just dropped. A real mission asked "Where is the base position?", the operator answered, and the mission closed itself incomplete reporting "received no reply" — with nothing anywhere to say the answer had gone missing.
Two related behaviours you may notice:
- A watcher's click now explains itself instead of doing nothing at all. If you get "you're watching", take control and answer again.
- A reconnect no longer demotes you. The brain's socket reconnects often, and control used to stay with the departing session until its disconnect was processed — so the reconnecting operator could briefly be a watcher without knowing.
If you answer just after it gave up, the brain now says so — "that looks like the answer to “…”, but that mission already stopped waiting" — and hands the instruction back so you can re-run it in one step. Previously that turn was answered as ordinary conversation, so you got a plausible reply and reasonably believed the mission had your answer.
"Why do my masks look like boxes?" — and the paid cloud segmenter (BIN-136)¶
Segmentation on the robot is a fallback chain, resolved per boot by
binabik-r1-vision: local SAM on a working GPU → a free self-hosted SAM 3 box → Modal,
a billed cloud GPU → plain bounding boxes. An unusable GPU skips its tier rather than
running SAM on the CPU, and the paid tier is never entered automatically. Which tier you got depends on the robot and the day, so the brain
tells you — rather than leaving you to guess why the outlines went rectangular.
| Where | What you see |
|---|---|
| Header (next to the Server/Robot dots) and the Visual view's top bar | A segmenter chip — SAM · GPU, SAM 3 · box, Modal · paid, Boxes, No vision. Hover it: the tooltip lists the robot's own reason for every tier it skipped (local_gpu: CUDA is not usable — torch 2.6 needs driver ≥ 525, this host has 470.239). That tooltip is the answer to the question in the heading. |
| Visual view, when masks are boxes | A warning that no GPU segmenter is available, with those reasons — objects still enumerate and stay pickable, the outlines are just coarse. |
| Top bar, whenever a Modal GPU is warm | A red banner: "Cloud segmentation is running — this costs money", with a Turn off button. Like the observer warning it renders on every route, including /admin. |
Starting the paid tier is always your explicit action. When masks are boxes and a Modal control surface is attached, the Visual view offers Use cloud segmentation… → a menu stating what it costs, that it shuts itself down after ~180 s idle, and the robot-side prerequisite. Nothing about "no GPU was available" ever brings Modal up on its own: a GPU bills from the instant it boots.
The spin-up control is inert without a token — by design
No Modal credentials live on the brain. The control surface runs where the credentials
are (segmentation-modal's lifecycle MCP tools, or its sam3-control HTTP server via
MODAL_CONTROL_URL), and the brain holds only a URL. So you will normally see the Start
button disabled with the reason on hover, in one of two distinct wordings:
- "No Modal control is attached to this brain" — nothing to drive; attach the server.
- "Modal is not configured where the control server runs — ask Pascal for the token."
Starting Modal also needs SAM3_ALLOW_DEPLOY=1 on that host. And even a running Modal
app won't change your masks unless the robot's vision service was started with
VISION_MODAL_ENABLE=1 and an endpoint URL — the chain is resolved once at startup.
The brain's own pollers now suspend after 10 idle minutes — the robot's do not (BIN-140)
Turning a billable poller on is still a commitment to turn it off again, but the brain now has a backstop. After 10 minutes with no UI interaction the idle gate suspends every background poller it knows about, and resumes exactly the ones it suspended on your next interaction. A poller you switched off by hand is never switched back on for you — the gate only restores what it itself took away — and the banner says "paused for inactivity" so a dark observer eye reads as a saving rather than a broken toggle.
Check it on a running brain: /api/surveillance must report a non-zero
idle_timeout_s (600.0), and the startup log must carry idle_gate_ready. A
0.0 there means no gate was built — which is exactly how this feature spent a day
merged-but-inert: the code shipped in main.py while config.yaml carried no
idle_gate: key, so the builder's getattr(…, None) guard returned None and neither
live brain had a gate at all. The key is now in config.yaml, and the guard fails
safe — deleting it falls back to the same defaults with a warning, because a cost guard
that goes quiet is worse than none.
Two limits worth keeping in mind:
- It is brain-side only. It suspends this brain's pollers; anything spending on the
robot is governed there, by
r1ctl. (Nothing on the robot polls on a cadence today — the one that did, the ambient perception buffer, was retired in BIN-306 — but the gate cannot reach across the seam, so that is a property of the fleet, not a guarantee of the gate.) - It caps an abandoned session, it does not decide for you. While you are using the UI an armed observer keeps billing, as it should. Every "deliberate, temporary opt-in" on this page and in Architecture still means what it says.
Where a mission's time went¶
Admin → Missions answers two questions and keeps them apart. The panel above the list is the trend across recent missions (below); open a mission and, beside the reasoning-vs-tools breakdown, an Optimisation panel answers the questions you ask when a single pick takes four minutes (KOE-16).
Three shares, not two. Acting is the robot moving; idle is waiting on a model, a solver or a query; unaccounted is everything else that elapsed — planner overhead between calls, a socket round-trip, someone taking a while to answer a clarification. That third number is the one worth looking at first, because summing call durations only ever gives you accounted time, and a breakdown without the remainder makes two buckets add to 100% of a number that is not how long the mission took. The heading says whether the total is the wall clock or only accounted time; it never substitutes one for the other quietly.
Acting is split by actuator — base, arm, gripper, head, and L3 skills separately. A skill
like grasp is one blocking call that drives several actuators itself, so it gets its own row
rather than being guessed at.
Waiting inside a skill gets its own row, labelled "already in its acting time". That label matters: those seconds are inside the skill's duration. Adding them to idle would count them twice; not showing them would lose the only view of why a three-minute grasp took three minutes. This is where a stall like "97.8 s between IK and the arm trajectory" becomes visible.
Per step: first run, corrected, failed. A step that succeeded only after a retry or a recovery counts as corrected, and the panel shows what that cost in wall clock against a clean run. Detection is not just "attempt 2" — grasp-service opens attempt 1 and escalates when the first approach fails, so an escalation inside one attempt is a correction too.
A dash is not a zero
A KPI with no samples reads —, never 0%. "Corrections are free" and "we had no correction to measure" are different findings, and the second must not look like the first.
Read the caveats under the panel
They are not decoration. The commonest one is that a sub-module's elapsed_s did not look
like seconds — it carries whatever that sub-module put in MCP's progress field, and a
sub-module sending a sequence number instead would otherwise produce a tidy-looking
fiction. When you see it, the phase timing for that skill was withheld, not zero.
A caveat is a claim about the record, so it can also be wrong. On the first real mission
after KOE-16 shipped, "grasp: elapsed_s went backwards" was the panel's own bug, not the
robot's: the mission called grasp twice, each run legitimately restarting its clock at 0,
and the analysis was reading both runs as one series (BIN-492). If a caveat blames a
sub-module for a mission where that skill ran more than once, suspect the reader first.
Everything here is in the copied record too. The Copy record / JSON buttons carry an
Optimisation section and the whole analysis object, so a mission pasted into an issue brings
its time breakdown with it. A record whose trace the analysis could not read simply has no such
section — its absence means "no views", never "no time spent".
Across missions — the KPIs that are actually trends¶
One record tells you where that mission went. A retry rate only means something across several: a step that needs a second try one mission in three reads as bad luck once and as a broken step across twenty. The panel above the mission list is that view — the same three shares and six KPIs summed over a window, the actuator and idle-cause splits, and a per-step table ordered worst first-run rate first, which is the row you are looking for.
Switch the window between the last 10 / 25 / 100 missions. It re-asks the server rather than re-summing in the browser, and the server sums the parts and re-derives each ratio instead of averaging the per-mission ones — averaging would let a ten-second mission that was 100% idle outvote an hour of real work. It reads the traces already on disk, so it covers history rather than only missions run from now on.
It degrades on its own
The sweep reads every trace file in the window, so one unreadable record turns the panel into a one-line warning and leaves the mission list and the per-mission views working. A page that took the whole list down over a single bad file would be worse than no KPIs.
The same aggregate is available directly as GET /api/admin/missions/analysis (?limit= capped
at 200 — each mission is a file read).
It shipped invisible once — check you can see it
Both panels are computed server-side and were, for a day, thrown away by the page: the trace
request carried a full analysis object and the frontend never read the field, so Admin →
Missions looked exactly as it had before the feature existed. The panel's own unit tests all
passed throughout, because they rendered the component directly and nothing asserted it was
ever mounted. If the views are missing after a deploy, that is the shape of bug to suspect
— not an empty trace.
What it is costing you¶
Every planner turn, every chat reply and every frame the observer looks at is a paid API call. Until BIN-240 the only way to find out what a session cost was to open the provider's billing console the next day. Now the brain prices each call as it makes it and shows you the result.
The chip in the top bar is what this session has spent, next to the segmenter chip. It
appears once something has been priced — a brain nobody has talked to yet shows nothing rather
than a $0 you would learn to ignore. Hover it for today's total as well, and for the count of
any calls that could not be priced.
Sub-cent figures keep four decimals on purpose. A real eleven-turn chat session measured
$0.0004; rounded to cents that is $0.00, and a session that cost money would read as free.
Reset clears the session figure and nothing else. It starts a new session — today's total, and every row behind it, are untouched. There is no button in this app that makes a bill smaller.
Admin → Cost¶
The full picture, over this session, 24 hours, 7 days or month to date:
| Panel | What it answers |
|---|---|
| The four tiles | What the window cost, over how many calls and tokens — and how many calls could not be priced |
| Spend over time | Which day (or hour) the money went, so a runaway is one glance rather than a query |
| Provider and model | What is expensive. The only breakdown that survives old rows being pruned |
| What it was doing | chat, planner, observer, recovery_distill (the brain's), recovery (the robot's own grasp recovery tier) — the observer is usually the surprise |
| Which credential paid | One row per key. Two robots sharing one credential show up here as two ids with one fingerprint |
| Which service / whose robot | brain, plus any robot-side spender that reports (BIN-240 phase 2) — a grasp recovery loop shows up as grasp-service, attributed to the robot it ran on |
| How the number was arrived at | estimated from our price table, or unpriced — never blended into one figure |
Two numbers the page will never fudge
An unpriced call is not a free one. If a model is missing from the price table its calls are counted separately and kept out of the money figure. A zero there would be indistinguishable from a model that genuinely costs nothing, which is the one reading this whole feature exists to prevent. Fix it by adding the rate — the table is a file on the host and a running brain re-reads it within a minute (see running the brain).
A partial answer says so. Raw ledger rows are deleted after retention_days; what
outlives them is a daily roll-up that keeps provider and model and nothing else. So a
window reaching back that far still has a correct total and a correct model split, and the
other breakdowns genuinely cannot place the older spend — they show it as "not
attributable" rather than dropping it. Every breakdown on the page adds up to the total
above it, always.
A dropped report is not the same failure as an unpriced call — and this page cannot see
it. A robot-side reporter with the wrong secret, or none, gets a 401 and nothing lands
in the ledger at all: not a $0, not an "unpriced" line, nothing on any breakdown. If a
robot's tools clearly ran and the page shows no spend for that service, check the reporter's
logs and its secret before trusting a blank as "this call was free."
Days are UTC, here and in the header, matching the rolled-up history — so a call stays in the same day after its raw row is gone.
The observer is the one that surprises people
A per-frame VLM call on a cadence adds up faster than a planner that runs a handful of turns. That is what the red banner and the idle gate are for — and now that the observer reports its spend, What it was doing will tell you exactly how much an armed eye cost you on a long mission.
Budgets¶
Admin → Cost → Budgets is where you set a cap, per window, against this brain's robot — there is nothing else to scope it against: one brain drives one robot, so a brain limit and a robot limit are the same limit. Three windows, each independent:
| Window | Resets |
|---|---|
| This session | When you press Reset |
| Today | UTC midnight |
| Month to date | The 1st of the month, UTC |
A limit takes effect the moment you save it — no redeploy, no restart, and it survives one: the limit lives on the same disk the ledger itself is mounted on.
The banner escalates at 50% / 80% / 100%, in the same place BillableServiceBanner already
lives — visible from wherever you are, not just on the Cost page. At 100% you'll see it named
plainly: which window, how much, and — if the hard stop is on — that further calls are being
refused until you raise the limit or turn the stop off.
The hard stop is off by default, on purpose
A limit with the hard stop off only warns — it never gets in the way of a mission. Turn it on when you want the brain to actually refuse the next billable call once the cap is hit: it covers chat, the mission planner, the observer and the recovery distiller in one place (they all resolve through the same swappable model holder), and it also switches off the observer if it was armed, the same way its own Disable button would. It does not cover a robot-side spender — the recovery tier, the grounder — because a robot-side report arrives after the money is already spent, on another host. That spend still shows up in the ledger; it just cannot be the thing that trips the stop.
A refused turn reads as a normal error — a chat reply explaining why, or a mission that ends "failed" with the limit named as the reason — never a crash, and never a silent retry.
A month-to-date limit can undercount near the end of a long month
Raw ledger rows are deleted after retention_days (30 by default); what survives is a daily
rollup that keeps provider and model only, with no robot to attribute it back to. So a
month-to-date cap's spend figure is a floor once the window reaches that far back, and
the budgets panel says so rather than showing a number that looks complete. It is never an
over-count — the limit will not trip early — only a possible under-count near the boundary.
Correct it on the fly — and it learns¶
If the robot gets stuck, tell it what it was missing in chat. If your hint leads to success, the brain distills a generalized lesson (not the one-off state) and reuses it next time — e.g. you say "the object is in the left arm" and it learns "track which arm holds the object and place with that arm," not "object is in the left arm." Genuine failure→fix→success runs can be Remembered.
Which lessons come back is scoped, in two ways (BIN-545 — until then it was scoped in neither, so every lesson went to every mission on the instance):
- By robot. A lesson is tagged with the instance's robot capability tag
(
brainctl --profile), and a tagged lesson is only offered back to a brain carrying the same tag. An untagged lesson is generic and applies to any robot; an untagged brain — the default, and every brain on the 4090 today — gets the generic ones and not another robot class's. So retagging an instance, or pointing it at a different robot class, retires the lessons that were about the old one instead of misleading the new one. - By profile. A profile with
recoverieslocked neither stores a lesson nor receives one. That matters in the direction that is easy to miss: lessons kept during a developer session are not injected into a customer profile's planning after somebody switches profiles.
Lessons live in that brain's own store, not in a directory shared between instances — fleet-wide sharing is the central memory container's job.
…and after it has stopped, too¶
Talking to it after a run used to be pointless. Chat kept no history and had no route to what the mission had done, so a sentence like "grasp was failing because you had the robot at maximum height, and it could not reach the cans it saw" — typed four seconds after the brain itself summarised that failure — was met with "I'm a robot assistant… What would you like me to do?". That is fixed (BIN-350). Two things you should now expect:
- It knows what just happened. For about half an hour after a run ends, a chat turn carries that run's outcome and the reason — the step that failed and what it reported. So "why did that fail?" gets "both attempts failed pre-grasp, side approach, no IK solution", and a correction gets acknowledged against the specific failure instead of a fresh introduction. It also remembers the last few things either of you said, so you can follow up without repeating yourself.
- It offers to keep your explanation. When you tell it why a run went wrong, a card appears under the answer: "That sounds like why 'pick up the coke can' went wrong. Keep it so I plan around it next time?" One click stores your sentence, as you wrote it, against that instruction — and it comes back as planner context the next time you ask for something similar. Ignore the card and nothing is stored.
This is the gap Remember could not fill. Remember distills a lesson out of the trace, so it is only offered for a run that failed and then recovered — a run that just failed has no fix in it to distill. Your explanation is that missing fix, which is why keeping it needs no model call and no verdict on whether the run was "worth" remembering.
Reset clears it
The header's Reset ends the mission, restarts the spend figure for the session, and wipes the chat — including the brain's own memory of it. After a Reset it genuinely does not know what you were talking about, which is the point: what it remembers is what is on your screen.
What the planner knows before it plans¶
The planner does not start from nothing any more. Its system prompt carries a world block (BIN-288) — the same snapshot the World panel renders, from the same read:
## What you can see right now
- Posture: torso upright, left arm tucked, right arm extended forward 0.62 m, both grippers empty
- WARNING: left_arm_joint1 is at the joint limit — a move in that direction will not go further.
- Base is at x 1.24, y -0.37, facing 90° in the map frame.
- Objects in view (5): cup[obj_0] at (1.00, 0.10, 0.74); … (seen 3s ago).
- You CANNOT observe: faults, e-stop & self-collision.
Before it, the planner's whole grounding was what is held in which gripper plus the previous command's outcome — which is why it could not answer "is the arm already near the target?" and why an open-ended instruction had nowhere to begin. The scene the system already computed went to your browser and nowhere else, so you were better informed than it was.
Two things are worth knowing when you read a plan:
- It is told what it cannot see. "You CANNOT observe: faults…" is there because a planner that knows it cannot check for faults asks differently from one that believes it checked and found none. If a plan seems oddly cautious about something, this is often why.
- It is told how old the picture is, in words. A scene the robot has moved since says so and says to look again; one that is merely old says nobody has refreshed it. So a plan that re-looks before reaching for something is not being timid — it was told the positions were unverified.
The block is injected at four decision points — mission start, around a skill call, on a replan, after a failed step — and never per turn, which would slow every turn down for no decision. Reading it costs nothing: it never triggers a scene capture, so it never bills.
Where to see it: the Config panel lists it as a World state provenance layer, and the
Debug panel records a world_state event each time it is injected, with which decision point
triggered it.
What it thinks it is holding — and what Reset clears¶
Alongside the world block, the planner is told what the robot is already holding, so a follow-up "put it on the table" knows what "it" is. That sentence is bookkeeping — the brain remembering an earlier grasp — not something it can see.
Twice on rap-0 that outlived the fact. A mission opened with "You are ALREADY HOLDING the Coke can in the right gripper" against a robot holding nothing; the brain spent three turns reasoning about a conflict that did not exist ("the box needs a two-handed grasp … I can't do both") and then asked, and the operator had to type "you don't have a can at the moment" two minutes in. The mission before it had already given up over the same phantom, after waiting 347 s.
Three things changed, and each is something you can rely on:
- Reset clears what it thinks it is holding. It does not clear the previous command — a follow-up still knows what you asked for last, which is the whole reason that grounding exists.
- The claim is checked against the grippers before the mission starts. If both grippers report
open, the belief is dropped and the planner is simply never told it is holding anything. The
Debug panel records this as
hold_dropped, and the arm chips update. - The planner is now told the gripper state too, so it can see the contradiction itself when the check cannot settle it.
Only both grippers open clears a hold
A closed gripper settles nothing in either direction, and that is deliberate. On rap-1 a
pick routinely ends verify_unavailable (BIN-330), and a two-hand grasp can squeeze without
lifting (BIN-334) — so a closed gripper the robot does not report as holding may well have
the object in it. Treating closed as empty would swap a phantom hold for a dropped real one,
which is the worse failure: the robot then plans as though its hands were free.
When the grippers cannot be read at all, the planner is told so in those words rather than the hold being quietly kept or quietly dropped. If you see "NOT MEASURED … verify before planning around it", that is working as intended.
A hold does not yet expire when the robot restarts
The belief is not tied to a robot session, because nothing in the stack exposes a robot
boot identity yet. In practice the gripper check above catches it — a hold that survived an
r1ctl restart is refuted the moment the grippers are readable — but if the grippers are
unreadable and the robot restarted, a stale hold can still be asserted. Press Reset, or
tell it plainly, and it clears.
It asks for a picture when it wants one¶
The planner can receive camera frames — before BIN-289 it could not, the message type could not hold one — but it does not get them automatically. It asks, by name, with a reason (BIN-392).
Frames were pushed on every turn for a while, on the reasoning that a plain frame runs no grounder and so costs nothing. True at the world service, and beside the point: it is not free at the planner's own model. Every turn carried ~1.5k image tokens the planner had not asked for and usually could not use, competing for one context window with lessons, waypoints and your Context-panel facts. And the question most likely to need a picture — did the gripper actually close? — needs a wrist camera, which the every-turn head frame could never answer.
So the planner calls read_world_state(cameras=["left_wrist"], objects=…, why=…), and the frames
arrive on the following turn. Two consequences worth knowing:
- A mission where it never asks gets no pictures at all, and that is normal, not a fault. Most navigation missions need none.
- It has to know what to ask for, which is why the request carries a
why— that string is recorded, and it is how you find out whether the pull contract was the right call.
What this costs, and what it does not
The plain frame is free of API cost. It encodes the picture the robot already holds — no grounder runs — so it costs input tokens and nothing else.
The annotated overlay would bill — producing the object ids runs the grounder on every
call — which is why it is off by default; world_state.annotated_frames turns it on,
capped per mission.
On the R1 it does not exist, and that is settled (BIN-558). Switching
annotated_frames on gets you the free plain frame back, marked annotated: false with a
reason, and nothing is charged. It is not the labelled picture you see in Visual Command —
that overlay is drawn in your browser from the scene inventory and is free. If you want
object ids, press Refresh scene ($) and read them in the World panel's object list, which
is the same capture the Visual view is drawing.
If you are ever tempted to switch images off to save money, switch off the annotation — it is the only part that would ever bill.
Since BIN-536 the per-mission cap is no longer the only control: the world service budgets the
annotated frame on the same ceiling as a scene capture, so an overlay can be refused (you
get the free plain frame, marked annotated: false, with the reason). The budget model is
the world-state spec's and the robot-side requirement
— including why the R1 refuses — is
integrating-a-robot's.
Two things keep it from growing: an unchanged frame is not re-sent, and only one frame is ever in the conversation — the old one is dropped when a new one arrives, so the planner is looking at now, not at a reel of increasingly wrong pictures. If a mission exhausts its image budget the planner is told the pictures have stopped, so it starts asking for what it can no longer see instead of assuming the scene still looks the same.
Seeing what it was given, after the fact¶
Two cards at the bottom of the Debug dock answer "what did it know when it decided that?":
| Card | Shows |
|---|---|
| What the planner saw | the frame it was actually given — not the live camera, which has moved on |
| What the planner was told | the world block it was handed, and every pull it made, with the why |
Read the second one when a plan looks uninformed. It distinguishes three things that used to look identical on screen: the planner was given a body summary; it was given nothing and reasoned from your instruction alone; or this brain has no record of the mission (it predates a restart, or has aged out of the last few kept). Those call for completely different responses, and until BIN-418 the only way to tell them apart was to pull the mission record as JSON — which is how an empty Debug panel got read as "no world state" on a mission whose trace clearly showed one being injected.
Neither card polls. They are audit surfaces, deliberately: refreshing on a timer would turn "what the planner was told" into a second live view of something that is not live. Use the refresh button on a mission still running.
Choosing a camera in the Visual view¶
A robot with more than one camera gets a picker at the top-left of the Visual view, listing what it actually reports rather than a fixed set. Switching changes the live stream and what a tap is measured against, and it clears the segmentation overlay — those outlines were computed from the previous view and do not describe the new one.
A camera with no depth is offered and marked "look only". You can watch it; you cannot tap it to place or arrange, because turning a pixel into a 3-D point needs that camera's own depth data. Deprojecting the tap against a different camera's depth would give a confident, wrong point — and that point moves an arm.
If the picker is not there, the robot reports one camera, or cannot enumerate them at all. In the second case the list shows what is assumed with "list may be incomplete" beside it.
See what it sees (World)¶
The right-docked World panel shows the snapshot the planner, the L3 skills and the surveillance watchdog all read — the same picture, not a prettier parallel one. When a mission goes wrong the first question is "what did it think the world looked like?", and this is where that is answered: the posture sentence it was given, joints with their ranges and any parked at a limit, both end-effector poses, the base pose, grippers, fault flags, the object inventory, and a live tile per camera.
Read it for what is missing, not only for what is there. Most of a snapshot is robot-dependent — the R1 reports joint angles, the next robot may not — so the panel is built to make absence legible, and it separates three things that look identical if you collapse them:
| What you see | What it means |
|---|---|
| a value | the robot reported it, and it is current |
| a Stale chip + "11 min old" | the robot reported it then. The value is real and it is not what the robot sees now |
| Not available + a reason | the robot cannot report it. The reason distinguishes "this robot does not implement joint_state()" — an integration gap — from a read that failed just now |
| "In the snapshot, not shown here" | the robot did report it and this panel does not draw it. A UI gap, not a robot fact |
A block marked Partial is answerable but incomplete: Cameras on a robot that cannot
enumerate its cameras is a usable head camera plus a caveat, not a claim that it has exactly one.
Faults is the other one, and the more consequential: on the R1 it reports fault: false for the
one signal the driver publishes, while the e-stop (a hardware latch with no state topic) and
self-collision (a MoveIt planning result) cannot be read at all — each listed with the reason,
under the block. Read it as "no fault reported, and two things nobody can see", never as a clean
bill of health.
Reading the object inventory. Each object shows its label, its id, and where it is — with the
frame the coordinates are in, which is the part worth pausing on. base_link is the robot's own
frame, so a base_link position is only true for as long as the base has not moved; if the scene is
minutes old and the robot has driven since, those numbers describe where the object was relative
to where the robot was. map coordinates do not have that problem. An object reading "no position
reported" means the payload carried none — that is the robot's answer, not a gap in this panel.
Below the list, "Per object, not shown" names the fields the payload carries for each object that
the panel does not draw. It exists for the same reason as "In the snapshot, not shown here" one
level up, and it earned its place: the panel spent its first weeks rendering "no pose" for every
object in every real capture, because it read the position from the wrong place while the coordinates
sat in a field nothing listed. Today that line reads bbox, centroid_uv, polygon, relations, score —
and score is 0 on every object while bbox is empty, so size and confidence are not currently
available from the robot at all, which is why neither is shown.
Every block says how old it is, and the banner at the top is about the body only. A stale block is called out rather than quietly aging — a snapshot that looks current is the failure the whole world service exists to prevent, and a panel that renders identically either way would undo it.
The reason freshness is per block rather than one line at the top is that the blocks age on
completely different clocks. Body readings refresh about every second and cost nothing. The scene
changes only when a capture is triggered, and a capture bills — so a stale scene is the normal
state of this panel, not an edge case, and it is the block whose numbers rot fastest: object
coordinates are in base_link, so they stop being true as soon as the base drives, with no field
going false to tell you.
Until BIN-374 the scene could be eleven minutes old and still read Available
Against a real snapshot from rap-1: the scene block was 658 seconds old and flagged stale
by the world service itself, and the panel showed a calm Available chip beneath a banner
reading Live · body read 0.0s ago. The word STALE appeared nowhere on the card; the block's
own stale and age_s were two monospace rows — age_s 658.030, stale yes — sitting between
refresh and budget at the same weight as a budget counter. Every element was true and the
impression was wrong.
If you are looking at a brain that predates this, the scene card's age is in that key/value list
and nothing highlights it. Check age_s by hand before believing an object position.
Refresh scene ($) costs money; everything else on this panel does not
Body readings are ROS reads behind the abstraction and cost nothing, so the panel refreshes them every five seconds while it is open — and stops the moment you close it. Camera frames are free too, which is why they are shown rather than hidden.
A scene refresh re-runs the grounder on the robot and bills per capture, so it happens only when you press the button. Nothing refreshes the scene on a timer.
This was true of the design and false of the code until BIN-347. The brain sent the world service's spend verdict on every read, so each of those five-second polls bought a capture — an open World panel billed until the robot's 40-fill session budget ran out, after which the scene silently went cold. The panel is now genuinely free to leave open; if you want the number to check, a plain snapshot takes ~2 s and a deliberate refresh ~15 s.
$ does it from the keyboard while the World panel is open — the same capture, the same
cost, the same refusals. It is scoped to the panel (like the Visual view's WASD jog keys)
and ignored while you are typing, so a $ in a chat message never buys anything.
The counter beside the button is what you have left, and it is not only yours
(BIN-415). The ceiling is metered per robot, so every brain attached to that robot spends
from the same 40 — a colleague's instance, a scratch instance, and your panel. When it reads
budget spent, Refresh scene ($) will be refused until the world service is restarted or
its ceiling raised. A brain older than BIN-415 shows no counter at all, and its captures are
recorded as unattributed, so on a shared robot you cannot tell whose they were.
If the panel says No world snapshot, that is the brain telling you it could not reach the robot's world service — the reason names the address it tried. A robot that is up but cannot answer some question looks different: the snapshot arrives, and the blocks it cannot fill say so individually.
Sending a snapshot to someone else¶
The copy button at the panel's top right, next to Refresh scene ($), puts the whole snapshot on your clipboard as JSON. Use it the moment something looks wrong, because the thing you are looking at is about to be gone: the body readings refresh every five seconds, so re-reading the snapshot later gives you a different one, and the state that showed the anomaly is unrecoverable. Copy first, then investigate.
What you get is the payload itself, not a transcription of the screen — the same bytes the planner, the skills and the watchdog were handed — so it can go straight into a Linear issue or be fed to a tool. The camera frames are not in it (they are fetched per tile, separately), which is deliberate: a paste buffer full of base64 helps nobody.
It is free, and it is worth knowing that explicitly given what it sits beside. It also works when the snapshot failed — that is the case where the world service's own reason for having nothing is the most useful thing on the screen, so the button stays live rather than greying out with the paid one.
If the button says Copy failed
The browser refused the clipboard. This is nearly always the plain-http URL: the clipboard API
needs a secure context, and the brain is regularly opened over plain http on the tailnet. The
panel falls back to an older copy mechanism that usually works anyway, so a failure here means
both were refused — open the brain over its HTTPS URL (brainctl ls prints it) and try
again.
Teach it places (Waypoints)¶
"Take this to the other table" only works if the robot knows what "the other table" is.
The right-docked Waypoints panel is where those places live: every named spot the robot
knows, with its pose, plus Add and delete. The names go into the planner's prompt, which
is what turns the sentence into a navigate_to_named step instead of a clarifying question.
The way to teach one is to drive the robot there and name it — jog it into place from the Visual view, then Add → Use current position. That reads the robot's live base pose into the x / y / θ fields, so you can see (and nudge) exactly what is about to be stored before you save. You can also type a pose in by hand, if you happen to know one.
If the button reports a problem instead of filling the fields, it is telling you what it got
back: a robot that exposes no get_pose tool, or a nav stack that isn't running, can't say
where it is. Neither is something the panel can work around.
A taught place lasts one session
Waypoints are stored as coordinates in the robot's map frame, and on the R1 that frame is built from scratch at every bring-up — its origin is wherever the robot happened to be standing when the stack came up. The saved file survives a restart; the meaning of the numbers in it does not, so places taught before a restart point somewhere else. Re-teach them after a bring-up until persistent maps + relocalization land (BIN-213).
Build a plan yourself, with no AI in it (Program)¶
The third segment of the header's view switch — Program — is the one view that never asks a language model anything. Chat turns your words into a plan; Visual Command turns your click into one; here you compose the plan, step by step. That is the point: it is deterministic (the same steps every run), inspectable (you can read exactly what will happen before it happens), repeatable, and it keeps working when every model provider is unreachable, slow or over budget.
The view has four parts:
- Skills (left) — a short Control flow group (Repeat until, If / else) above the
skills this brain offers you, grouped by the robot server that
provides them, with a search box. Each one shows what an operator calls it (Go to waypoint)
above the name the plan actually stores (
navigate_to_named); hover it to read what it does. This list is not a fixed menu: it is whatever your brain has attached right now, so plugging in a new MCP server makes its skills available and unplugging one stops them being offered. It is also shorter than the full list on purpose — the robot advertises about fifty tools, most of them internal machinery, and this palette shows the handful you build plans out of. The counter under the heading says both numbers (7 of 51 shown), and Config → Skills is where you change which ones appear. The two control-flow entries are not skills and are never hidden by that list, or by the search box. - The canvas (middle) — your plan, as a chain of blocks, and beside it any drafts you have not joined to it yet. Drag a skill from the library onto the canvas, or press the skill's Add button; either way it arrives unconnected, and connecting it is what makes it a step — see the chain is the order below, which is the one rule worth reading before you build anything. Drag a block onto the block it should follow to connect the two, drag from a block's bottom dot to another's top dot to draw the line by hand, or press + plan on a draft to put it at the end. Click a line to select it, and Delete removes whatever is selected — a block or a line. Four things sit on the canvas itself: Arrange (top right, see tidying the plan), the zoom controls (bottom left), a minimap (bottom right) and — while a plan is running — a small legend saying what the step colours mean (top left).
- The step panel (right) — the selected block's settings. Its parameters come from the skill itself, so what you see is exactly what that skill accepts, with its own descriptions, its own choices in the dropdowns, its own defaults filled in, and its own example greyed into each empty box. There is no separate list of fields to keep up to date: a skill that gains a parameter grows a field here the same day, and a skill that gains an example shows it the same day too. Some dropdowns are filled in live — the place picker on Go to waypoint lists the waypoints this robot has actually been taught, so it cannot offer somewhere the robot has never been.
- Problems and the run (bottom) — everything the editor can see wrong with the plan, and, once you press Run, the live progress.
Authoring one, in order¶
- Add the steps, and join them up. Drag or Add each skill. Each one lands as a draft beside the plan; drag it onto the step it should come after (the target block lights up before you let go), or press + plan on it to put it at the end of the line. A block with a number on it is a step of the plan; a block marked draft is not.
- Fill in each block. Select it and complete the parameters — clicking anywhere on a block
selects it, and the selected one gets a ring around it. A red
*marks a parameter the skill requires, and the grey text in an empty box is an example of what belongs there, written by whoever wrote the skill. -
Pass a result from one step to the next. This is what makes perceive, then pick what you found one plan instead of two. Set Save result as on the step that produces something —
obj, say — and any later step can read it: click the small link button beside a parameter, pick$objfrom the dropdown, and type the field you want in the box under it (name, orpose.position.x). The panel shows the exact value it will pass —$obj.name— before you save anything.The dropdown lists only the results saved by steps that run earlier, so it cannot offer something that will not exist yet. The field box is typed rather than chosen for an honest reason: a skill tells the brain what arguments it takes, and nothing tells it what a result contains. Offering you a list of field names would mean guessing them. If you get one wrong, the brain says so — see below — and the robot never moves on a bad reference.
Leave the field box empty to pass the whole result, which is usually what a skill wants when its parameter is an object rather than a name or a number.
-
Say what counts as success, if the skill does not say so itself: Succeeds when takes a path in the result, e.g.
successorgrasp.ok. The step passes when there is something truthy at that path and the skill did not report a failure of its own. Leave it empty to accept anything the skill does not itself report as a failure.It is a path, not a comparison — there is no
=or>box beside it, because the robot cannot evaluate one.heldis a question about whether the result saysheld; writing something likeheld == truewould be read as the name of a field calledheld == true, which nothing has, so the step would always fail. 5. Repeat something until it works, or choose between two things — see Repeat and if/else below. Both are optional; a plain sequence needs neither. 6. Name it and Save. The name is just the box in the toolbar — editing it and saving is how you rename a plan. 7. Run. The plan runs on the robot immediately and its progress appears both on the blocks (each one lights up as it runs, succeeds or fails) and in the familiar mission card below, with the same Stop button as any other mission. A step that fails its Succeeds when gate turns red on its block, and the mission card says what was expected and what came back (expected 'held' on {…}) — which is the difference between "the grasp failed" and "the gripper closed but reported nothing in it".
Run is deliberately unavailable until the plan is saved and free of problems, and the button's hover text says which of the two is stopping it — the brain runs the stored plan, so launching unsaved edits would run something other than what is on screen.
Watching a repeat. The steps inside a container light up too, and a repeat shows its
progress by re-lighting the same rows on each attempt: the body step goes blue, then red when its
gate fails, then blue again as the next attempt starts, and the container itself stays blue while
any step in it is running. An if/else shows which way it went — the arm that ran lights up and
the arm that did not stays grey. What is not shown is which attempt you are on, only the
cap and the condition on the container (×≤3 · until pk.held): the robot does not send an
attempt number, and guessing one from the restarts would be wrong, because a step's own retry
motion restarts it too. The mission card below always lists every step in the order it happened.
The chain is the order¶
A plan is an ordered list of steps, and the lines between the blocks are that order. Nothing else is: not where a block sits, not the order you added them in. This is the rule the whole view rests on, so it is worth stating plainly:
- A block on the chain carries a number —
1,2,3— and that number is exactly the position it will run in. What you see numbered is what gets stored and what gets run. - A block off the chain is a draft. It is marked
draft, it has no number, and it is not part of the plan: it is not saved, it is not run, and its half-filled parameters cannot stop the rest of the plan from running. It is on the canvas because you are still working on it. - Where a block sits on the canvas means nothing to the running order. You can move blocks around to keep things legible — and since the arrangement is now saved with the plan, it is worth doing — but the plan itself does not change. Position never implies order. The same goes for Arrange, which rearranges every block and changes no step.
That last point is why an unfinished block looks like a draft rather than like a mistake. Adding a skill no longer draws a line for you, so having something unconnected on the canvas is normal and expected — you place it, then say what it comes after. The alternative would be a canvas where a block's position looks like it decides when it runs while the lines actually do, and "the order I can see is not the order that runs" is the one way an editor like this becomes dangerous rather than merely awkward. There is a test that authors a plan on the canvas, saves it, and refuses to pass unless the stored step order is the order that was numbered on screen — and it does it with two of the blocks dragged to opposite ends of the canvas, which is the case that matters now that the arrangement is saved along with the plan.
Joining a draft, three ways, all equivalent:
| Gesture | What it does |
|---|---|
| Drag the draft onto another block | Connects them: the block you dropped it on runs first. The target lights up before you release, so a near miss does nothing rather than something you did not mean. |
| Drag from a block's bottom dot to another's top dot | Draws the line yourself. The connector snaps to a nearby dot and shows you which one it has caught before you let go, so you do not have to hit it exactly. |
| Press + plan on the draft | Puts it at the end of the line. This is the one that needs no dragging. |
Removing something is the same either way: select a block and press Delete (or use the bin in the step panel) and the steps around it are joined back together, so deleting a middle step does not orphan everything after it. Select a line and press Delete and just that connection goes — which splits the plan, leaving the tail as drafts. Both work on lines you can actually hit: a connection's clickable area is much wider than the hairline that is drawn.
Delete does nothing while you are typing
Parameters contain words, and words contain the letters you are deleting. While the cursor is in any box, Delete and Backspace belong to that box and the canvas never sees them. Click the canvas background first if you want to delete a block.
Tidying the plan, and finding your way around it¶
A plan you have been rearranging by hand can end up spread out, and a block dragged past the edge of the window is genuinely easy to lose. Three things on the canvas answer that, and none of them touches the plan's order.
Arrange (top right) lays every block out the way the view lays out a plan you have never moved: the chain top to bottom in one column in its running order, and everything not on the chain — drafts, and anything stranded off it — in a second column beside it. A container keeps its body inside it, so a Repeat until with four steps takes the room it needs. Then the view zooms to fit, so what you get is the whole tidy plan on screen.
Two things to know about it:
- It cannot change what runs. Arrange moves blocks and nothing else — it does not connect anything, disconnect anything or renumber anything. See the chain is the order: the lines decide the sequence, and Arrange only reads them.
- It leaves the plan unsaved, on purpose. Arranging throws away an arrangement you made by hand, and since arrangements are saved with the plan that is a real change. So you get to look at the result and decide: Save keeps it, or reopen the plan and your own layout comes back. Pressing Arrange on a plan that is already tidy changes nothing and does not mark it unsaved.
It is also the quickest way to get back a block you dragged out of sight — a block parked somewhere off the canvas comes back into a column with everything else.
The minimap (bottom right) is the whole plan at a glance, with the current view marked on it. Drag or scroll inside it to move the canvas. While a plan is running, each block on it is coloured by what that step is doing — running, done, failed, skipped — so it is also the fastest way to see where in the plan the robot is when the plan is too big to fit on screen. The block you have open is outlined.
The soft red bloom behind the plan is scenery, but useful scenery: it is fixed to the plan rather than to the window, so it drifts across as you pan and gives you something to judge "where am I" by on a plan larger than the screen. There is a second one behind the side panels, which is what separates them from each other and from the canvas.
Repeat, and if/else¶
Two blocks in the library's Control flow group hold other steps inside them. Both are things the robot has always been able to do; what changed is that you can build them here rather than only in a plan the AI wrote.
Repeat until runs the steps inside it again from the top, until a saved result comes out true or it has tried the number of times you set. This is how try until it works is written — a missed grasp re-looks and re-grasps without anyone in the loop. A step inside that fails its own Succeeds when gate also restarts the body, which is usually exactly what you want.
If / else runs one set of steps when a saved result is true and another when it is not. Both arms are optional individually — a plan that only needs a fallback can leave then empty — but not both at once.
Filling one in:
- Press Add on Repeat until or If / else, or drag it onto the canvas. The block appears in the line as a dashed container with an empty body, and the editor immediately tells you the body is empty — an empty container is not something the robot will run.
- Add the steps inside it. After you add a Repeat until it is selected and the library is aimed into it, so the next skill you add goes in the body. You can see where you are aiming: the slot inside the container says adding here. For an If / else you choose the arm first, with the small add here button on then or on else — there are two answers, so it does not guess. Dragging a skill straight onto a slot works too, and lands it there.
-
Set the condition on the container itself. Select it and fill in:
Block Field What it means Repeat until At most How many attempts before the plan gives up. Repeat until Stops when The result path that ends the repeat early, e.g. pk→held. The list offers what the body itself saves, because the check happens after each pass.If / else Runs then when The result path that decides, e.g. gp→success.If / else Reverse it Run then when the path is not true. Each condition is a result name from the dropdown plus a field path you type, exactly as a parameter reference is, and for the same reason: the robot knows which names a plan saves and nothing tells it what a result contains.
-
Clicking a step inside a container opens it in the step panel like any other step — parameters, Save result as, Succeeds when. Steps are numbered by where they sit:
2.1is the first step inside block 2, and2.t1/2.e1are the first steps of an if/else's two arms.
A name saved inside a container is readable by the steps after it, including a name saved in only one arm of an if/else — either arm may run, and the step after is entitled to read what it produced.
Stops when cannot be reversed, and Runs then when can
The If / else block has a Reverse it tick and the Repeat until block does not.
That is not an oversight: the robot evaluates the two conditions with different code, and only
the if/else one understands "not". If you write a repeat's condition as !pk.held by hand, the
robot looks for a field literally called !pk, never finds one, and the repeat runs the full
number of attempts every time. The editor flags that as something to check rather than a
problem, because the plan is still legal — it just cannot do what it looks like it says.
To end a repeat on the absence of something, save the positive value and test that instead.
What can go inside what. The editor offers only the arrangements the robot can actually run, and greys out the rest with the reason in the hover text:
| At the top level | Inside a Repeat until | Inside a then / else | |
|---|---|---|---|
| A skill | yes | yes | yes |
| If / else | yes | yes | no |
| Repeat until | yes | no | no |
So a repeat may hold an if/else, but not another repeat, and an if/else's arms hold plain steps. An AI-written plan that breaks one of those rules — it can happen — opens here and is reported in the brain's own words rather than quietly rearranged.
What the editor checks while you type¶
Two kinds of remark, and they are not the same:
- Problems stop Run, because the plan could not do what it says: a required parameter
left empty, a parameter the skill does not have, a block whose connections loop back on
themselves so nothing can reach it, a
reference to something no earlier step saves (and separately, one whose step runs later —
move it up), a reference you started and did not finish, a Save result as name that no
reference could ever point at (one containing a
., or starting with$), and a skill that no attached server offers any more. - Things to check are advice and block nothing: two steps saving under the same name (the
later one wins, which is sometimes exactly what you meant), or a save name that works but is
easy to mistype, like
my result. - A draft is neither. Nothing an unjoined block says is checked at all, because it is not part of the plan — the moment you join it, its problems appear.
The split is deliberate. Anything the robot would happily run is at most advice, because an editor that refuses to launch a working plan is worse than one that stays quiet.
The brain checks the plan again when you save it and again when you run it, against the tools
attached at that moment — and when it refuses, each of its reasons appears on the step it is
about, in the brain's own words, with the step's name as a button that opens it. So "no
earlier step saves 'obj'" lands on the step that reads $obj, not in a wall of text you have to
count steps against. Change anything and those messages clear: they described the plan you sent,
and only the brain can decide whether your fix worked. The editor's job is to tell you now rather
than in front of a customer; the brain remains the authority.
One line of steps — with containers in it, and nothing lost
A plan you build here is a straight sequence, so connecting a block to two next steps, or drawing a circle, is a problem rather than a guess about which arm to run. (Leaving a block unconnected is not a problem — that is a draft, above.) A repeat or an if/else is not that: it is one block in the line that holds its own list of steps, drawn inside it. There are no wires inside a container, because there is nothing to choose — the steps in a body run in the order they are listed.
Plans written by the AI in Chat contain the same two blocks, labelled the way the mission
trace labels them (×≤3 · until pk.held), and open here fully editable — that is what
Save as plan on a finished run puts in the store. Saving the plan
keeps every part of it you did not change exactly as it was — including the parts this
editor has no box for — so opening an AI-written plan, changing one parameter and saving is
safe.
What is saved, and what is not
The steps are saved: their order, their parameters, their save names.
Where you dragged the blocks is saved too. Arrange a plan so it reads the way you think about it — the retry loop off to one side, the two placing steps beside each other — and it comes back that way when you reopen it, on your screen and on a colleague's. Only the blocks you actually moved are remembered; anything you left alone is laid out top-to-bottom from the running order as before, so a plan authored on a wide screen is never unreadable on a narrow one. Moving a block counts as a change, so the plan says unsaved and you are asked before you leave the view — otherwise the arrangement you can see would quietly disappear.
Plans saved before this feature existed have no positions in them, and open exactly as they always did. Nothing is invented for them, and re-saving one does not add anything.
This does not change the order anything runs in. The lines between the blocks decide that, and only they do — see the chain is the order. Dragging a block to the top of the canvas does not make it step 1, and never will; if it ever appears to, that is a bug worth reporting rather than a shortcut worth using.
Drafts are not saved — that is what being a draft means. If you leave the view with an unjoined block on the canvas, it is gone when you come back (its position with it); the step count under the canvas always says how many drafts are sitting beside the plan so this is not a surprise. An unsaved plan does not survive a page reload at all, and leaving the view with unsaved work asks first.
Nothing here calls a model — and that is tested¶
The claim is the whole reason the view exists, so it is checked against what the app actually does on the network, not against a promise in the code: the view is driven through a complete authoring-saving-launching journey against a stub that refuses any request outside the two plan endpoints, and against a socket that must not be spoken to at all. Both of those checks were watched failing with a model call added, then kept.
Practically: with every model provider down or unconfigured, Program still lists the skills, still builds and saves a plan — references, success gates and all — and still runs it.
When a brain runs with its models switched off¶
An instance can be started without one or more of its reasoners — see running the brain for the flags. What you see then is not a greyed-out version of the normal UI: the surface goes away with the model behind it.
| Switched off | What disappears |
|---|---|
| Chat LLM | The whole Chat view — no transcript, no composer, no voice path in. Its entry leaves the view switcher. |
| Mission planner | Chat and Visual Command, since both ask the planner to produce the plan. Stored plans are unaffected. |
| Watchdog eye (paid) | The eye button in the top bar, the observer window, the "VLM observer is armed/active" cost strip, and the eye's findings. |
| Watchdog body checks (free) | The body findings, and the "body checks only" line in the Debug panel's status. |
Two things follow from that, and both are deliberate:
- Nothing dead is on screen. A view whose model is off is absent from the switcher rather than present-and-erroring, and when only one view is left the switcher itself disappears — a segmented control with one permanently-pressed segment is chrome that looks interactive and is not. The landing view is always one that works, so a model-free instance opens on Program rather than on an empty Chat.
- The Programming view always renders, and needs none of them. On an all-off instance it is the UI: open a stored plan, press Run, the robot moves — with no model in the loop at all.
The eye and the body checks are separate switches, and that matters for what the Surveillance box tells you: with the eye off, the box is still there and still reports that the robot is being watched, because the free deterministic checks are still reading it. Only an instance started without both has no watchdog surface at all. See the four badges for what each one is claiming.
Why the arm button vanishes instead of being disabled. On an instance with no eye there is nothing to arm, so pressing it could only ever produce a refusal. It is the same control as always on a normal brain — and it now reads the resolved state, so it can never show "armed" on an instance whose eye does not exist.
Why is a view missing? — Config → Reasoners¶
The Config panel lists all four reasoners and, for each one that is off, why:
off at boot— this instance was started without it. Nothing you can do in the UI brings it back; it is abrainctlflag or abrain.envline, and the instance has to be recreated.locked for this profile— the instance has that model, but the active profile does not include it. Switch profile, or edit the profile'sunlockedlist.
The distinction is the point: they live in different files, and guessing means editing the wrong one. The card is read-only — a reasoner is set where it is decided, not from here, so there is no third place for the answer to disagree with itself.
Keep the plan the AI just ran (Save as plan)¶
Ask the brain for something once, watch it work, and then keep that plan. This is the shortest way there is to a plan that runs forever without a model: the AI writes the first draft, you correct it in the Programming view, and from then on it replays exactly — no provider, no bill, no variation between runs.
Where the button is. On the run itself, in two places: expand the mission card in Chat and it is under the summary, and in Visual Command it is at the bottom of the Plan panel. It appears once the mission has ended — while the robot is still working, "the plan that ran" is not settled yet, and the control you want in reach at that moment is Stop. Press Save as plan, give it a name (the box is pre-filled with what you asked for, so usually you just press Enter), and it lands in Config → Plans and in the Programming view's Open a saved plan list right away.
It is offered on a failed run too, on purpose. A plan worth fixing in the editor is usually one that did not work.
Save it before the next brain update, or it is gone
Stored plans survive a brainctl update; the mission records they are read from do not
(BIN-461). So the plan a run compiled exists only until the next deploy of that brain. That
is why the button is on the run instead of somewhere you would have to go looking for it.
What gets saved is the plan, not the list on your screen. Those are not the same thing, and
the difference matters. A plan with a repeat in it is stored as one repeat holding its body;
the screen shows that body once, lifted out of the container and labelled
×≤3 · until pk.held, and re-lights the same rows on each attempt. A plan with an if/else is
stored as one conditional with two arms; the screen shows both arms side by side. Saving
what is displayed would turn a three-attempt retry into a single pass and a conditional into a
straight line through both arms — which is not a cosmetic difference, it is a different set of
robot motions. The brain therefore keeps its own record of the plan it compiled and saves from
that, so what you re-run is byte-for-byte what ran.
A run the brain drove step by step cannot be saved, and says so. Not every mission compiles a plan: sometimes the brain calls one tool, looks at the answer, and decides the next one — a turn-by-turn run, with nothing compiled at any point. Pressing Save as plan on one of those is refused with "this run had no compiled plan; it was driven turn by turn", and the fix is in the message: ask the brain to plan it as one sequence and run that, then save it. The brain will not invent a plan out of the calls that happened and offer it as the plan that ran — that would be a confident description of something your robot never did.
Two other refusals read differently, deliberately, because the remedy differs:
| It says | What happened |
|---|---|
| this run had no compiled plan; it was driven turn by turn | Nothing was compiled. Ask for one sequence and save that run. |
| this brain did not record the plan itself | A plan did run, on a brain built before plans were kept. Re-run the task. |
| there is no record of this mission on this brain | A brain update erased the run, or the id is wrong. |
A saved plan is checked before it is stored, against the tools the brain can see right now — so a run from this morning is refused if the grasp service has since gone away, with every reason named by step rather than one per attempt. That reads as an annoying refusal and is the opposite: the alternative is discovering it mid-motion with a bobbin in the gripper.
Editing the saved copy never changes the run it came from. The mission record stays exactly as it happened, so the trace remains an honest account of that run however much you rework the plan afterwards.
Nothing here calls a model — and neither does re-running it
Keeping a plan and launching the kept copy both touch no model provider at all, which is checked the same way the Programming view's claim is: the whole journey is driven against a brain that refuses any request outside the one save endpoint, and a planner that fails loudly if anything enters it. The original run used the AI, once. That is the point — the model authored the plan; it is not a dependency of running it.
If the mission replanned part-way through, what is kept is the last plan it compiled — the one that finished the job — and the notice says how many there were rather than implying the mission was a single plan.
Keep the stored plans tidy (Config → Plans)¶
A stored plan is a fixed sequence of skill steps the brain replays exactly, with no reasoning in the loop — the same bobbin goes on the same shaft the same way, every run. Two things put one in the store: the Programming view, where you author it yourself, and Save as plan on a finished run, which keeps the plan the AI wrote. The Plans section of the right-docked Config panel is where you look after the ones that already exist.
Each row shows the plan's name, its description, how many steps it has and when it last changed. From the row you can:
- Expand it (the chevron) to read the steps in order — the tool each one calls and the
parameters it passes. A repeat or an if/else step shows the same label the canvas and the
mission trace use for it (
×≤3 · until pk.held) rather than a blank line. This is a read-only view: changing the steps is the Programming view's job. - Rename it in place. Enter saves, Escape leaves the stored name alone.
- Duplicate it. The copy lands as
<name> (copy)and opens straight into the rename box, because a copy almost always wants its own name — start typing. - Delete it. It asks first, and there is no undo.
- Launch it, from inside the expanded row: the plan runs on the robot right away and the panel names the mission it started, so you can follow it in the Debug panel.
Refresh (top-right of the section) re-reads the store — that is how a plan you just saved in the Programming view, or one another operator created, shows up without reloading the page.
Renaming is safe
A plan is identified by a hidden id, not by its name, so renaming one never confuses it with another — including two plans that briefly carry the same name after a duplicate.
A launched plan needs no AI model. The reasoning already happened, once, when the plan was authored; launching one calls no model at all, so a plan runs on a brain whose planner has no API key — the thing a chat command on that brain would refuse to do. It is also a normal mission in every other way: it appears in the Debug panel and the Missions view, the body watchdog watches it, and Stop halts it like any other run. Only one mission runs at a time, so Launch is refused while something else is going.
A plan is checked before anything moves — twice. When you save it, and again the moment you launch it, the brain compares every step against the tools this brain can actually see right now, and refuses the whole plan if any step names a tool that is missing, passes a parameter that tool does not have, or reads a result no earlier step produced. You get every problem at once, named by step, rather than one per attempt.
The second check is the one that matters on a busy day: a plan you saved this morning, while the grasp service was up, is refused at Launch if that service has since gone away. That reads as an annoying refusal and is the opposite — the alternative is the robot stopping half-way through with a bobbin in the gripper.
Your plans survive a deploy — your mission traces still do not
Stored plans are kept on the brain host's mounted data directory, so updating a brain
(brainctl update) leaves them alone. Mission traces are not: they still live inside the
container and every update erases them (BIN-461). Export a trace you care about before
asking for an update.
If the section cannot read the store it says why rather than showing an empty list: a brain it cannot reach reads as could not reach the brain, and a brain running a build from before plan storage existed says exactly that. Neither of those is "you have no plans" — if the panel tells you the store is empty, it really is.
Choose which skills the Program view offers (Config → Skills)¶
The robot advertises far more tools than anybody would put in a plan. On the R1 it is about fifty, and roughly seven of them are things you would ever choose: go to a waypoint, drive, rotate, strafe, perceive, grasp, place. The rest are the machinery underneath — solving inverse kinematics, adding an object to the motion planner's world model, clearing its map of obstacles. Useful to the robot, and not a step you compose a shift's work out of.
So the Skills palette in the Programming view shows a curated list, and the Skills section of the right-docked Config panel is where that list is chosen. It shows every tool your brain can see, grouped by server, each with a tick box, the operator's name for it and the tool name underneath. Tick one and it appears in the palette immediately — no reload, no restart. Untick one and it goes.
Out of the box the list is those seven. Four near-neighbours are deliberately left out, and each is one tick away if you want it:
| Left out | Why |
|---|---|
| Stop | It is the emergency stop, and it is handled the moment you press it — not queued as step 4 of a plan. If you need to stop, you need to stop now. |
| Save waypoint | That is teaching the robot a place, which you do from Waypoints. A plan uses places; it does not name them. |
| List waypoints | It reads the list back. A plan cannot do anything with the answer. |
| Take a stance | Because it is easy to confuse with two others — see the warning below. |
Three skills that read alike and are not
Go to waypoint (navigate_to_named) drives the whole robot across the floor to a
place you saved. Move arm to pose (goto_named) moves one arm to a stored joint
position and the robot does not go anywhere. Take a stance (go_to_stance) puts the
torso and arms into a whole-body posture. Picking the wrong one of those in front of a
customer is why the palette shows the operator name first and why the third is not offered
by default.
The promise that makes it safe to use¶
Hiding a skill never breaks a plan that uses it. A stored plan that calls a hidden skill still opens in the Programming view, still shows every step, still passes its checks and still runs. This is a choice about what you are offered while authoring — nothing else. A plan that came from the AI planner, or one you saved last month while a skill was visible, is unaffected.
The reverse is equally firm: ticking a box cannot invent a skill. The list can only ever narrow what the robot actually advertises, so the editor can never offer you a step the robot would then refuse. If a robot server is unplugged, its skills disappear from the palette and from this card — but they stay ticked, marked not connected, so plugging it back in restores your choices rather than making you redo them.
It belongs to the brain, not to your browser¶
The list is stored on the brain, per profile. Two consequences worth knowing:
- A demo opened on a different laptop, in a fresh browser, in a private window, shows the same curated list. Nothing here lives in your browser, so nothing is lost by clearing it.
- Switching profile switches the list, so a customer profile can offer three skills while the developer profile offers all fifty on the same brain.
It also survives a brainctl update, like the stored plans and unlike the mission traces.
The card tells you which of those is answering — the skill list Binabik ships, this brain's configuration, or your choices, stored on this brain — and the ↺ button restores the configured default. Setting a different default for a whole instance or a whole profile, rather than by hand from this card, is a deployment question: running-the-brain.md owns the flags and the profile keys own the per-profile list.
Install it as a desktop app (PWA)¶
The UI is a Progressive Web App — it can run as a standalone desktop window (own dock/taskbar icon, no browser chrome). When you open it in a normal browser tab, a small "Install Binabik" toast appears for a few seconds (bottom-right); click Install, or use the browser's install control (the ⊕ in the address bar / "Install app" in the menu). Installed users don't see the toast.
Install requires a secure context (HTTPS)
Browsers only offer install over HTTPS (or localhost) — over plain
http://<host>:<port> the prompt never appears. brainctl publishes each
instance over HTTPS automatically via Tailscale Serve (an
https://<host>.<tailnet>.ts.net URL), which is why install works on the shared
host; a TLS reverse proxy is the alternative. The app still works fine over plain
HTTP — it just can't be installed.
What you are driving (the parts you can see)¶
The brain is robot-voice-chat — a FastAPI + Socket.IO backend serving a React SPA.
Everything below is what shows up in the UI; the deployment side of it lives in
Running a brain.
- Mission executor — the planner loop. Turns your command into a plan, calls tools, observes results, reacts. This is what the Debug panel streams.
- FSM plan runner — a compiled step machine (loops,
if,optional) that runs a whole plan over the robot's tools without the LLM in the tight loop. A compiled plan is also what gives you the nested sub-module boxes described above. - MCP tool backend — one connection per server in this instance's attach manifest:
for the R1,
r1-abstraction(:9220, the contract overgalaxea_agent) +grasp(:9210), plus the world state (:9240) and central memory. Each reconnects independently, and a server that is down is skipped, not fatal — which is why a brain can look healthy while a tool you wanted is simply absent. - Surveillance — an independent VLM watchdog that can pause a run on hazards or mismatches. Its findings ride the same sub-module telemetry channel, filed against the step that was executing, so they appear both inline under that step and chronologically in the Surveillance window.
- Memory + recoveries — operator context, learned processes and distilled recovery lessons, injected into the planner prompt at mission start.
- Web UI — chat, the Debug panel, a Context panel (operator facts), a Waypoints panel (named places), a Config panel, the admin area, and the Visual Command / observer view.
When it won't do what you asked¶
It keeps asking clarifying questions instead of acting. By design it asks when genuinely unsure, and it is honest when a capability was never exposed. The usual causes:
- No named places. "Take it to the other table" has nowhere to go until places are taught — see Teach it places; their names are injected into planning. Without them the brain must ask. If a place was taught but the robot has been restarted since, treat it as untaught: the coordinates no longer point where you meant.
- A missing tool. If the request needs a capability the robot never exposed, the brain says so rather than flailing.
- No robot prompt. If the robot advertises no
prompt://system, the planner has only the universal base to work from and leans on questions.
The mission ended "incomplete" and I never told it to stop. Check whether it asked you something. A clarification pauses the mission until an operator turn arrives; if none does, the planner is nudged after ~60 s and eventually closes the mission out neutrally. The question is on the chat card and — since BIN-160 — on the Visual view's question interrupt; a hard page reload is the one case where it can be lost from the browser while the robot is still waiting.
Since BIN-198 the summary tells you which it was: a mission that ends still blocked appends the
measured wait and quotes the question ("Waited 389s for an answer to “…”; none reached the
mission"). If you did answer and see that note, your answer never arrived — check the brain's
log for ws_send_message and ws_action_refused around that time, both at INFO since BIN-199.
Before that the planner simply asserted "received no reply", which it had no way to verify.
It answers, but never touches the robot. That is not a planner problem — the brain came up without the robot's tools attached. See Running a brain → troubleshooting.