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.
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.

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...
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.
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.
There are actually two sidecars:
robot_bridge.cpp — owns the
B1LocoClient (locomotion) at
/tmp/duxbot_robot.sock. Wire protocol, one line in, one
out:
{"op":"change_mode","mode":2} → {"ok":true}
{"op":"move","vx":0.3,"vy":0,"vyaw":0} → {"ok":true}
{"op":"wave_hand","hand":1,"action":0} → {"ok":true}
{"op":"replay_trajectory","path":"..."} → {"ok":false,"error":"..."}audio_bridge.cpp — the NAEC audio:
echo-cancelling microphone, 24 kHz player, and the barge-in
flush. Three FIFOs:
/tmp/duxbot_cap — mic PCM16 16k (sidecar → Go)/tmp/duxbot_play — playback PCM16 24k (Go →
sidecar)/tmp/duxbot_audio_ctl — a single byte 'F'
= flush (cuts the queued sound instantly)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++.

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:
Wrong suspect: at boot,
booster-audio POSTs to a license server
(authority.huwentec.com) for the AEC backend. It looked
like the bottleneck. But: it failed 98 out of 98 times
in the entire history and the mic worked — auth was
never a prerequisite for capture. The license file doesn’t even exist on
disk. Caching the auth would solve nothing.
Actual root cause (proven in the field):
device contention. Two booster-audio processes
fighting over the single USB microphone. Whoever loses gets
snd_pcm_open ret=-16 (Device or resource busy) → the AEC
backend goes DEGRADED → answers
2001 "audio backend is unavailable" forever → deafness. The
two sources of the second process: a rogue spawner from an old
recovery script (it launched setsid nohup booster-audio
outside systemd), and a SEGV at boot when dozens of services come up at
once.
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.
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.
turn.goHow 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 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=0means 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=400generic reject, the gait balancer suppressing the trunk band). The state machine must transition onrc==0from the physical side, never on enqueue. In the code, thisrctravels 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, ...)
}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 }
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
}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.
wscore and the simulator
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:
null — just logs the commands.sim — MuJoCo with physics + RL policy
+ live GLFW viewer, isolated behind //go:build sim (the
robot cross-compile, no cgo, never pulls in MuJoCo).sidecar — the physical robot, via the
C++ sidecar.wscore — a Python
core that owns the DDS + B1LocoClient and speaks WebSocket
to the Go side.wscore deserves a note. It’s the
primary motion communication path and the
source-of-truth spec: hexagonal just like the Go
(ports/robot.py → booster_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.
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:
Turning on input_audio_transcription on
grok-voice breaks the response. Proven in production
(2026-06-17): grok starts only echoing “USER: …” and stops
generating a conversational response — it treats the session as
“transcription mode.” And the perverse detail: an earlier probe
had “passed” because it only measured whether the field was
accepted (and the …completed event emitted), not
whether the assistant kept responding. Accepting the
field ≠ the pipeline still working.
OpenAI’s GA realtime rejects
session.temperature. “Unknown parameter” — and a
rejected session.update brings down the
instructions and tools with it (total, silent
incoherence). Coherence on the default path comes from the system prompt
alone.
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.
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.
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.
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.
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
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.

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.)
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.)

DuxBot is more than voice. The optional capabilities, all behind ports and nil-guarded (when absent, they degrade gracefully):
Perception / virtual bumper
(PerceptionInput): a node on the Orin publishes the
distance to the nearest frontal obstacle (~5–10 Hz); the domain turns it
into a speed clamp (ClampForObstacle). Monocular today,
because the stereo camera has a physically broken LPWM
sync trigger (a hardware defect, RMA pending; the camera runs in
free-run as a workaround, with degraded stereo depth but intact
2D detection).
Vision / what_do_you_see
(VisionInput): a YOLO COCO node on the Orin publishes the
list of detected objects/people; it becomes a box overlay on
the remote control and a tool the robot uses to count what it sees (“I
see two people and a chair”).
Identity (IdentityInput): face
(InsightFace) + voice (SpeechBrain) fused by a domain policy
(FuseIdentity — weights, name agreement, threshold). The
who_is_this / enroll_person tools.
Telemetry (body_status): the robot
answers “what’s your battery?”, “are you hot?” with
real body data. It’s push via DDS, not RPC —
the sidecar subscribes to the topics once at boot and caches the latest
frame; each block has a “seen” flag, so before the first publish the
robot is honest (“still booting up the sensors”) instead of inventing a
number.
Wake word “hey duxbot”
(WakeDetector): openWakeWord (ONNX ~200 KB) in a sidecar
over a socket. An honest debt: the custom model trained only on
synthetic audio (edge-tts) overfit — it scored 0.85 on training and
0.0005 on the real NAEC mic. Stopgap: the hey_jarvis
builtin, well-trained out of the box. Retraining pending with real field
positives.
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.
Confidence in the system comes from four complementary layers of proof, from deterministic to physical:
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.
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.
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%.
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.

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.
rc=0 is not physical success — the
20-hour lesson that rewrote how the state machine trusts the world.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).