// deep-dive · engineering · DuxBot K1

Anatomy of a humanoid robot's brain

An engineering deep-dive into DuxBot K1: from the microphone socket to the LLM tool-call, from pure-domain anti-fall safety to a docker-compose deploy on an 8 GB Jetson.

2026-06-25 · ~22 min · Go / C++ / Python / pgvector / Ansible
hexagonal-architectureembedded-roboticssafety-ssotragrealtime-llmjetson-orindocker-compose

TL;DR — 40 seconds


This is the long technical document. If you want the narrative version, it lives elsewhere. The goal here is different: open the hood and show how each subsystem of DuxBot — the voice brain of the Booster K1 Education humanoid robot — is built, why each decision was made, and which traps cost hours of field debugging before becoming a rule.

The hardware: K1 Education, 22 degrees of freedom, Jetson Orin NX (aarch64, 8 GB unified CPU+GPU RAM, 6 cores, RT-Tegra kernel), Ubuntu 22.04. A robot that listens, talks, searches the web, dances, kicks, gets up off the floor and recognizes who’s in front of it.

Let’s go from the physical edge inward.


1. The map: hexagonal, three layers, one dependency direction

The real K1: the DUX_BOT admin screen — Booster K1, firmware v1.6.2.2.
The real K1: the DUX_BOT admin screen — Booster K1, firmware v1.6.2.2.

The whole agent is Go, in a hexagonal architecture (Ports & Adapters). There’s only one rule and it’s absolute: the dependency always points inward.

adapters/  ──►  ports/  ──►  domain/
(I/O, SDKs)    (interfaces)  (pure stdlib)
domain/        pure logic, ZERO external deps (stdlib only)
  command.go       Action enum, Tool surface, Choreography(action,args) → []Step
  safety.go        anti-fall SSOT: SafeMode/ClampLinear/ClampYaw/IsSafeTrajectory
  segment.go       VADSegmenter — energy-based speech state machine (no bytes)
  conversation.go  ConversationGuard — half-duplex, double-talk, echo-guard, reset
  wake.go          WakeGate — "hey duxbot" sleep/wake machine, injected time
  guardrails.go    ScreenInput/ScreenOutput — profanity/code filter (robot talks to kids)
ports/         interfaces: AudioCapture, AudioPlayback, RealtimeLLM/Session,
               RobotController, Transcriber/StreamTranscriber, WakeDetector,
               PerceptionInput, VisionInput, IdentityInput, EventInfo, FleetRegistry...
adapters/
  driven/        pluggable backends (LLM, STT, audio, robot, perception, RAG...)
  driving/       orchestrators (voiceloop, webremote)
sidecar/       C++ over unix sockets (robot_bridge, audio_bridge)
cmd/           11 entrypoints — duxbot, duxvoice, duxchat, duxbench, admin...
Hexagonal architecture: adapters → ports → domain, with the dependency always pointing inward.

Why so much rigor? Because domain/ carries the logic that cannot fail — above all the physical safety — and because it imports only the standard library, it’s testable at ~98% coverage in milliseconds, with no hardware, no GPU, no network. The robot’s fall physics goes through go test.

There’s a build detail that looks trivial and isn’t: GOWORK=off is mandatory. The duxbot module lives outside the workspace root go.work; without that variable, go build resolves against the wrong workspace and fails. And the cross-compile for the robot is GOOS=linux GOARCH=arm64 CGO_ENABLED=0 — a static ELF, zero runtime dependencies on the Jetson.


2. Why two binaries (and only one is Go)

The Booster SDK is a static C++ library (libbooster_robotics_sdk.a) with a Python binding but no Go binding. To keep the Go agent pure (it cross-compiles on the Mac, no cgo), the robot control lives in a tiny C++ sidecar compiled on the robot itself. The Go ↔︎ sidecar communication is a unix socket with newline-delimited JSON.

Voice pipeline: from microphone to body. The tool-call is the only path that moves the robot; the transcript is diagnostic only.

There are actually two sidecars:

A detail that saves hours of debugging: the FIFOs are opened O_RDWR (read and write) so open() never blocks waiting for a peer and reads never EOF when the bridge cycles. It’s the same trick the original Python code used.

And a cheap observability gem: the Go side stamps the active span’s W3C traceparent header on every op line. The C++ sidecar doesn’t speak OpenTelemetry — it just extracts the trace_id (32 hex) and echoes it on stderr. Result: a grep on a trace_id correlates the sidecar journal with the Jaeger trace, without dragging the heavy OTel dependency into the C++.


3. The audio path — and the recurring deafness

The Booster K1 during bring-up, tethered by an Ethernet cable.
The Booster K1 during bring-up, tethered by an Ethernet cable.

The microphone is a single USB array (card Speaker, capture node /dev/snd/pcmC1D0c). The flow:

USB mic → booster-audio (NAEC, opens hw:1,0 EXCLUSIVE) → audio_bridge → /tmp/duxbot_cap → duxbot-go

The project’s most recurring symptom was deafness: “I talk and it goes mute.” The agent speaks fine, but doesn’t hear; the peakRMS in the log stays ~5 even when shouting; the /tmp/duxbot_cap FIFO sits at 0 bytes.

The investigation was a textbook case, because the cause looked obvious and was a red herring:

The cure was three layers of defense: eliminate the rogue spawner; a single-source audio bring-up (audio-up-coordinated.sh) that guarantees a single owner of the device before coming up (kills the rogue, confirms the device is free with fuser, waits for backend init success, and only then brings up the consumers in the order audio→control→go); and a host watchdog that detects the absence of a capture heartbeat and runs the same routine on its own, covering both boot and runtime with no operator.

Transferable lesson: the authoritative “ready” signal is the unit journal (backend init success), not the auth nor the FIFO bytes during bring-up. When a system has a noisy dependency (the auth that always fails), it’s easy to blame the noisy one. Measuring what actually gates the resource is what closes the case.


4. Barge-in — talking over the robot

Natural conversation requires barge-in: the user interrupts and the robot stops immediately. There are two modes, and the difference is instructive.

Full-duplex (robust, recommended). The mic always flows to the realtime LLM; the server VAD hears the speech over the bot and triggers a full abort (cancels the response, discards pending audio, player.Stop in ~1–2 ms). The subtle trick is the dynamic over-talk gate: it tracks a moving average (EMA) of the ambient noise/echo floor while the bot speaks, and only forwards frames that jump a factor K (default 3.0) above that floor. This auto-calibrates across environments: headphones on the Mac (ambient ~150–300 → threshold ~750) and the robot (NAEC echo ~5–9k → threshold capped at 11000), with no manual tuning.

There’s a hidden UX subtlety: on confirming the interruption, a 0.4 s “sticky” window opens and audio flows continuously. Why? Normal human speech has micro-dips between syllables that would chop the stream, and the server VAD would never see contiguous speech. The window covers the time until the VAD fires.

Half-duplex + DTD (fallback). While the bot speaks, the mic is muted; a double-talk detector compares the muted mic’s RMS against the playback echo. A fragile heuristic — it almost never fires on the Mac without native AEC. Full-duplex is the choice.


5. Orchestrating a turn — turn.go

How does the robot, in the same turn, search + converse + move? The answer is in a seemingly simple loop and a rigid division of labor.

Capability Who runs it Mechanism
Conversation brain (cloud) text streaming
Search (search_web) inline in turn.go the result becomes a role:tool message in the history
Movement / identity delegated to voiceloop event → executor + safety SSOT

Search is inline because it’s just text — it goes back into the history and the brain keeps reasoning. Movement is delegated because whoever owns the physical robot and the anti-fall SSOT is the voiceloop; turn.go never touches the metal — it emits an event and blocks on a channel until the body finishes. Search and movement never trip over each other.

The turn.go hop loop: inline search, delegated movement, and the brain re-called with results until it produces final text.

The structural reason all three fit in a turn is a loop of hops:

for hop := 0; hop < 6; hop++ {
    // calls the brain 1×: returns TEXT (end) OR tool-calls
    // re-calls the brain with the tool results
}

Each hop calls the brain once. The brain returns text (end) or tool-calls. Since the loop re-feeds the brain with the results, a turn chains: search_web (hop 0) → injected result → wave + speech (hop 1) → end (hop 2). Multi-tool per hop + multi-hop = search, conversation and movement coexisting in one turn.

A delegation handshake closes the cycle: when delegating a movement, turn.go drains the continuation channel, emits the tool-call event (the voiceloop runs the choreography → sidecar → SDK), emits the response-done event, and blocks until the body finishes. The next hop only runs after the real wave actually happens.

There’s a fix here that unblocked “moving together” and cost dearly: arm choreographies called kWaveHand without ensuring the robot’s mode; kWaveHand fails in damping mode with rc=400. The fix was to prepend ChangeMode(kWalking) to every arm choreography. Which brings us to the rule that saved the most time in the whole project.

rc=0 means command ACCEPTED/queued, NOT executed. It took over 20 hours to learn this. The SDK accepts a command and the body can still never move (rc=501 “low battery stop moving”, rc=400 generic reject, the gait balancer suppressing the trunk band). The state machine must transition on rc==0 from the physical side, never on enqueue. In the code, this rc travels as the primitive’s error:

// RobotRCError carries the SDK return code of a REFUSED primitive.
// The hard-won field lesson: the "result:ok" on the Go side is only the
// ENQUEUE — physical success is the rc==0 from the sidecar.
type RobotRCError struct {
    Op string // the refused primitive (change_mode/move/wave_hand/...)
    RC int    // SDK code (501 ServerRefused, 400 rejected, ...)
}

6. Safety as an SSOT — defense in depth

This is the non-negotiable piece. Physical safety is a single source of truth, lives in the pure domain, and no backend can bypass it.

The K1 stays standing always. Three commands could knock it down: cutting torque (damping mode), a speed high enough to topple it, or a replay of a trajectory that isn’t a designed gait. All three are clamped or blocked — in depth:

Risk Rule Applied at (3 layers)
ChangeMode(kDamping) only kPrepare/kWalking pass domain → Go adapter → C++ sidecar
Move too fast clamps \|vx\|,\|vy\|≤0.5 m/s, \|vyaw\|≤1.2 rad/s domain → Go adapter → C++ sidecar
ReplayTrajectory non-gait file whitelist domain → Go adapter → C++ sidecar
const (
    MaxLinearSpeed = 0.5 // |vx|,|vy| (m/s)
    MaxYawSpeed    = 1.2 // |vyaw| (rad/s)
)

func ClampLinear(v float64) float64 { return clampF(v, -MaxLinearSpeed, MaxLinearSpeed) }
func ClampYaw(v float64) float64    { return clampF(v, -MaxYawSpeed, MaxYawSpeed) }
func SafeMode(m RobotMode) bool      { return m == ModePrepare || m == ModeWalking }
Defense in depth: the same domain SSOT is re-applied at three boundaries, so no path can command a fall.

The same rule is called three times: at the orchestrator, at the adapter, and at the C++ sidecar on the SDK boundary. So that even a buggy orchestrator or a hand-crafted socket can never command a fall. And it’s not a crutch — a test (TestChoreographyVelocitiesWithinSafeEnvelope) guarantees that every choreography the system could emit is born inside the envelope.

There are also gates born from a real fall. The battery:

// On 2026-06-13 the K1 fell flat on its face when "Dance" was pressed at ~2%
// battery — the SDK refused the ChangeMode (rc=501) and the motors lost torque
// mid-routine. A dying pack won't hold the body during a dance.
const (
    DanceMinSOCDefault = 20.0 // dance
    KickMinSOCDefault  = 40.0 // a kick is more violent → higher floor
    GetUpMinSOCDefault = 20.0 // getting up is essential recovery → minimum safe floor
)

func CanDanceAtSOC(soc float64, seen bool, minSOC float64) bool {
    if minSOC <= 0 { minSOC = DanceMinSOCDefault }
    if !seen { return true } // unknown charge → can't prove unsafe; allow (honest, logged)
    return soc >= minSOC
}

Note the if !seen: the system never pretends to know a battery it hasn’t measured. The same principle holds for the obstacle — a too-old frame forbids advancing rather than guessing:

func ClampForObstacle(vx, frontDistM float64, stale bool) float64 {
    if vx <= 0 { return vx }                       // reverse/stopped: nothing to brake
    if stale || frontDistM <= ObstacleStopM { return 0 } // blind or collision → don't advance
    if frontDistM >= ObstacleSlowM { return vx }   // clear → full speed
    f := (frontDistM - ObstacleStopM) / (ObstacleSlowM - ObstacleStopM)
    return vx * f                                   // linear braking zone
}

K1 ≠ T1 — the boot check that refuses to start wrong

The canonical model is the K1 (22 DOF). There’s a cousin, the T1 (23 DOF), that shares almost all joint names — but the K1 has no waist joint, uses an A prefix on the shoulder (ALeft_Shoulder_Pitch) and has a passive Ankle_Cross. If a T1 joint name leaks into the K1 control path, you’re commanding the wrong body topology: a guaranteed fall. A domain test (TestJointKeysAreK1Members) and a boot check fail loud if any non-K1 name shows up. The robot refuses to start wrong.

Debt honesty: there is no native K1 walking RL policy. The 12 leg joints have identical names to the T1, so the T1 policy (T1.onnx) is reused for walking and spinning — with an explicit boot warning, never silently.


7. One binary, three bodies — wscore and the simulator

The emulator: the same binary driving the MuJoCo K1 body on the Mac.
The emulator: the same binary driving the MuJoCo K1 body on the Mac.

The hexagonal architecture gives a gift: the same binary that deploys to the robot runs on the Mac as an emulator. An environment switch (DUXBOT_ROBOT) picks the body, all satisfying the same RobotController interface:

wscore deserves a note. It’s the primary motion communication path and the source-of-truth spec: hexagonal just like the Go (ports/robot.pybooster_sdk.py → ops in ws_server.py), with a “single DDS owner” rule — only one B1LocoClient active at a time, so when wscore is the owner, the C++ sidecar doesn’t run, and vice-versa. The resulting process rule is inviolable in the project: new body capability is born in the Python core (locomotion, gesture, head pan/tilt, dance); the C++ sidecar is a legacy fallback path, not where new things are born.

This grounds a development principle: testing and making it work in the simulator is mandatory before touching the hardware. The order is fixed — deterministic unit test → real “wire” proof against ws_server.py (or the Go sim backend) → green build and tests → only then a version bump and deploy. “Break the sim = break the robot.” Hardware is never the first test.


8. The realtime AI protocols are not interchangeable

This section is a warning paid for in production. xAI (grok-voice-preview-1.0), OpenAI (gpt-realtime) and others are “OpenAI-realtime compatible” on paper. In practice, each one accepts and emits a different subset of fields in session.update and of event types.

The concrete facts that became rules:

The final rule became: with any audio-native realtime, text transcription off, always. It already does STT+LLM+TTS in the same socket; parallel transcription is redundant and, on grok, fatal. And the process rule: never port a field from one adapter to another blindly — a provider-specific change goes in gated, with a safe default, and only turns on where it’s unproven after a controlled probe that measures the full effect.

Transferable lesson: “compatible with API X” rarely means “interchangeable with X.” And a test that measures acceptance without measuring behavior survival is a dangerous false green.


9. Internet is the official mode — and why offline was frozen

A final decision, measured on the robot, not in theory: the official production mode is cloud realtime (OpenAI/xAI duplex). The 100% offline path (ollama on the Orin) is frozen — a documented fallback, no further evolution.

Why? The Orin NX has 8 GB unified (CPU and GPU share the same RAM) and 6 cores. While the motion stack runs (motion realtime ~713 MB + ~30 ROS2 nodes + 7 containers), there’s no budget left to host an LLM. The measurements with a small model (qwen2.5:1.5b):

Attempt Latency/turn GPU offload
keep_alive=5m (model pinned to CPU) ~45 s 0/29 layers
keep_alive=0, cold 27.8 s 93% CPU / 7% GPU
keep_alive=0, warm 100 s 93% CPU
num_gpu=99 (force offload) 61 s didn’t offload

ollama looks at “free VRAM”, sees little (unified memory is already taken by motion+ROS), decides 0 layers on the GPU and falls 100% to CPU — which is core-starved. It’s not a config problem; it’s a saturated machine. What’s fluid: cloud realtime, sub-second. Only the brain goes to the cloud; motion stays 100% local, with control latency intact.

But the offline path exists and is ingeniously honest, so it’s worth documenting the engineering even frozen: mic → whisper.cpp (STT) → Qwen2.5-3B (decide) → XTTS (TTS) → playback. All on the Orin, no cloud, no key. Tool-calling without native function-calling is solved by Structured Outputs: the tools are forced by a JSON Schema that ollama compiles into a GBNF grammar (constrained decoding), which physically prevents the q4 model from degenerating (unterminated string, template token leakage → loop). The 8 GB RAM is managed in sequence, never simultaneously: whisper transcribes and frees → Qwen loads, decides, unloads via keep_alive:0 → XTTS loads and plays.


10. Event RAG — from 93.5% to 100% recall

The robot sits at a real event (a vintage-car fair, a school) and must answer with knowledge of that event — which the LLM doesn’t know. That’s a RAG over a curated corpus. The metric that matters is Context Recall: did the right chunk land among the top-k retrieved? Recall is the ceiling of everything — if the right piece wasn’t even retrieved, the LLM hallucinates or apologizes.

Step zero: a ruler that reproduces production

I built a harness (cmd/rageval) that runs a golden set (questions + expected chunks) through the same path the robot uses and measures recall. Every change was A/B-tested on it before going to production. That’s how I found that the “market-standard” reranker (a textbook cross-encoder) worsened recall on the Portuguese corpus: 93.5% → 90.3%. Measuring killed a myth.

The three independent layers

question
   ├─[QUERY]   normalize → (HyDE: + hypothetical answer)
   ├─[DENSE]   embed(query) <=> embedding   (bge-m3, 1024d, cosine)
   ├─[SPARSE]  to_tsvector @@ tsquery        (Postgres full-text, pt)
   └─ RRF (fuse dense+sparse by rank) → top-k
The three independent retrieval layers — query (HyDE), dense (bge-m3) and sparse (full-text) — fused by RRF. Each step attacked a distinct layer.

Each step of the journey attacked a different layer — which is why they added up.

Step A — sparse was swallowing exact terms (93.5% → 96.8%). Diagnosis in the score logs: “what’s the school’s methodology” brought the wrong chunk. The tsquery built an AND ('methodology' & 'school'), but the target chunk talks about “methodology” and “DUX College” and doesn’t contain “school” → no match → the lexical signal zeroed out. Fix: tsquery to OR (matching any term is enough), lexical weight 1.4 in RRF, top-k from 4 to 6. Cost: a constant and a server-side change.

Step B — an honest denominator (the truth was 97.3%). 96.8% on 31 questions is 30/31; there’s no “99%” in that denominator, and 31/31 would be overfit. I expanded the golden set from 31 to 76 questions. The real baseline was 97.3% (73/75) — and the 2 misses had the content existing in the corpus, just mis-ranked. It wasn’t missing data: it was ranking.

Step C — HyDE, asking as if you already knew (97.3% → 100%). A short question has an ambiguous vector. HyDE (Hypothetical Document Embeddings) first generates a short hypothetical answer and embeds that — the vector of a plausible answer lands much closer to the right chunk (which is also an answer). The implementation subtlety:

// semanticChunks:
embText := q
if queryExpandMode() == "hyde" {            // DUXBOT_QUERY_EXPAND=hyde
    embText = expandHyDE(ctx, hydeClient, question, s.log)
}
vec := embed(embText)                         // DENSE uses query + hypothesis
hybridChunks(eventID, q, vec, poolK)          // LEXICAL uses q (real terms)

The generated hypothesis enters only the dense arm; the lexical one keeps the real terms of the question (the invented hypothesis might not contain the keyword). Best of both worlds. And fail-safe: any error (no gateway, timeout) degrades to the raw query, never takes down the search.

Step Change Layer Recall
0 bge-m3 dense + hybrid base 93.5%
A tsquery AND→OR + weight + top-k sparse 96.8%
B golden 31→76 measurement 97.3% (real)
C HyDE (gated) dense/query 100%

HyDE costs one extra LLM call per question (~300–800 ms), so it stays off by default and only turns on where precision matters more than latency (a kiosk, FAQ). The embedding model (bge-m3, via Cloudflare Workers AI, no OpenAI) was not swapped — recall rose 6.5 points by changing everything except the piece most people swap first.

An engineering invariant worth gold: query and ingest use the SAME embedding model. Diverge = junk cosine. And the corpus lives in versioned seeds (the source of truth), not loose scripts — the ad-hoc enrichment scripts were declared obsolete when they messed up a re-ingest.


11. Deploy: docker-compose on a Jetson, and why K8s was dropped

Field deploy: robot #10 on the futsal court while the deploy logs scroll on the laptop.
Field deploy: robot #10 on the futsal court while the deploy logs scroll on the laptop.

The robot’s runtime is docker-compose, definitive. Four services, network_mode: host, ipc: host, mounted env_file: jaeger, robot-bridge, audio-bridge, duxbot-go.

The host network choice isn’t laziness — it’s necessity: 1. The Booster SDK uses FastDDS (multicast peer discovery + shared-memory transport); a bridge network would isolate both. 2. The L4T (RT-Tegra) kernel lacks the iptable_raw module → compose up in bridge fails.

There was an intent to migrate to MicroK8s. It was abandoned, and the rationale is a good study in “the right tool for the right node”:

Dimension docker-compose MicroK8s/K8s
RAM overhead ~0 700 MB–1 GB (on an Orin with ~571 Mi free)
Runs on this kernel? ❌ snap-confine requires apparmor; the RT kernel has none
Nodes that justify it 1+ 2+
/dev, sockets, FIFO, GPU, host-net 1 line friction (privileged, hostPath, device-plugin)

The blocker is physical, at the kernel: the RT-Tegra was compiled without apparmor, and MicroK8s (snap) won’t come up without it. Forcing it would mean recompiling the humanoid’s motor-control kernel — high risk, ~zero gain. Besides, K8s solves problems a single-node, single-owner robot doesn’t have (scaling across nodes, draining, multi-tenant) and gets in the way of what it does need (direct access to /dev, sockets, GPU). The apparmor block wasn’t an obstacle to overcome — it was a signal that K8s doesn’t belong on this node. (If a second robot ever shows up, the clean detour is k3s — a systemd binary, no snap/apparmor — not MicroK8s.)

Fleet delivery: CI builds arm64 once, publishes an immutable registry tag, and each robot pulls the same image — running docker-compose locally, provisioned by Ansible.

Important not to conflate: “robots need images in a registry” ≠ “robots need K8s on the robot”. Fleet delivery is a central layer (CI builds arm64 → registry → each robot pulls the same immutable tag), while each robot still runs docker-compose locally. And all provisioning — apt, Docker, ollama, sidecars, units, config.env — goes through Ansible. Ad-hoc SSH editing is forbidden; manual SSH only for read-only diagnostics.

Finally, versioning as SSOT: the version of everything comes from a single file (robot/VERSION), stamped in three places that must agree — the Go binary (via -ldflags), the image’s OCI label, and the admin navigation. One command (task version) cross-checks the three and answers “what’s live?”. (History: the robot ran a stale :latest image for days without anyone noticing — :latest hides old code. The OCI label kills that.)


12. The edges: perception, identity, wake word

The web remote (DUXBOT Remote): live telemetry + walk / wave / dance / stop.
The web remote (DUXBOT Remote): live telemetry + walk / wave / dance / stop.

DuxBot is more than voice. The optional capabilities, all behind ports and nil-guarded (when absent, they degrade gracefully):

All follow the same three-layer shape and the same fail-safe: a stale signal or a dead node degrades to the safe state, never to a guess.


13. How it’s all proven — the test pyramid

Confidence in the system comes from four complementary layers of proof, from deterministic to physical:

  1. Deterministic glue (domain/routing_e2e_test.go, ~100+ cases, offline, always-green): pins the domain’s routing — when live search fires/holds, when the guard blocks profanity/code, when a movement tool becomes a choreography. It’s the CI gate.

  2. Live bench (cmd/duxbench, 219 kid utterances): drives the real binary one shot per case and measures what only the real brain answers — routing accuracy (216–217/219 ≈ 98.6–99.1%, stable over 4 runs), conciseness (median 10 words) and language (zero non-Latin script leakage). It exits with an error if accuracy drops below the minimum — it can gate a release.

  3. Multi-turn bench (cmd/duxevolve, 51 checks): proves context evolution that the one-shot doesn’t see — pronoun carry (“who is Elon Musk” → “how many kids does he have”), additive follow-up (“the capital of France” → “and Germany’s” → Berlin), maintained arm state, name memory, silence reset. 50/51 = 98%.

  4. Body union (choreography_sim_e2e_test.go, -tags sim, MuJoCo, no robot): closes the seam between routing and physics. It drives the choreography through the real orchestrator against the MuJoCo K1 body and asserts that each intent moves the body and the robot never falls (TrunkZ ≥ 0.4). 15/15 — including a test that runs all 10 movements in sequence without a single fall.

And a code-language rule that looks pedantic but keeps the base coherent: comments in pt-BR (whoever reads the source is Brazilian), diagnostic messages (fmt.Sprintf of log/error/OTel) in English, and — untouchable — everything the robot says or the LLM reads (system prompt, tool descriptions, search results) in pt-BR.


The through-line

Personality: the number-10 jersey and a Brazil hat.
Personality: the number-10 jersey and a Brazil hat.

Thirteen sections and a dozen subsystems, but the principle is one, repeated in every decision: discipline over cleverness, and honesty about what you don’t know.

The body, the Booster SDK had already solved. The thin, pure, testable and honest layer between a kid’s “hi” and a 22-joint humanoid in motion — that was the engineering.


Stack: Go (hexagonal agent, static arm64 cross-compile, GOWORK=off CGO_ENABLED=0) · C++ (robot_bridge/audio_bridge sidecars over the Booster SDK, unix sockets) · Python (wscore — hexagonal motion core + MuJoCo reference simulator) · Postgres/pgvector (hybrid dense+sparse+RRF RAG) · Cloudflare Workers AI (bge-m3 1024d embeddings) · OpenAI/xAI realtime (audio-native duplex brain) · Jaeger/OTel (correlated cloud→robot tracing) · Ansible (100% provisioning) · docker-compose (runtime on the Jetson Orin NX).