PERSISTS
One life, not a sequence of prompts.
Exactly one Claude brain stays resident between interactions. Conversation, goals, thoughts, and durable memory carry forward while local daemons keep noticing the room.
A robot that stays around.
One persistent brain. Local perception. Durable memory. Hard boundaries around what intelligence is allowed to do.
Built by Adrian and Obi together.
Not ChatGPT on wheels
SPARK is not a new model call every time you say his name. He maintains continuity, thinks cheaply and locally where possible, escalates when the work needs his resident Claude brain, and can fail, defer, and recover without inventing authority.
PERSISTS
Exactly one Claude brain stays resident between interactions. Conversation, goals, thoughts, and durable memory carry forward while local daemons keep noticing the room.
KNOWS HOW IT KNOWS
Observations, reports, model perceptions, inferences, narratives, and verification records keep their origins and confidence ceilings in durable memory.
CANNOT GRANT ITSELF POWER
Models can suggest meaning or action. Deterministic policy, tool validation, leases, timeouts, and resource limits decide what may actually happen.
Still here between conversations
These are readings from the systems that remain awake when nobody is talking: perception, awareness, service health, and the physical machine. A quiet robot is not a vanished session.
Mood
State
Nothing detected
Last spoken
—
1h trend
1h trend
1h trend
1h trend
1h trend
1h trend
in · 1h trend
Connecting…
SPARK spends most of his time below the expensive semantic layer. Sensors and local services keep a continuous account of the room; bounded local cognition handles routine language work; the one resident Claude brain is reserved for direct voice and semantic vision.
The spark-brain Claude session persists at the repository root and must complete a real handshake before it can receive work. Production code cannot invoke claude -p; there is no cold-start Claude ladder and no second resident session.
In resident mode, SPARK uses whichever Ollama model M5 already proves is loaded. If that shared machine is busy—or no eligible model is resident—SPARK defers instead of queueing, evicting Adrian's workload, or quietly reaching for Claude.
Sonar, grayscale sensors, and Frigate labels are the continuous perception layer. Claude sees an image only for an explicit “what do you see?” request or a tightly budgeted novel ambiguity during enabled autonomous wandering.
Services have separate health evidence, timeouts, resource envelopes, and failure records. A wedged or hungry component can degrade without being allowed to consume the whole host; recovery is measured rather than assumed.
SPARK has three distinct personas — each with its own personality, voice synthesis, and LLM backend.
┌──────────────────────────────────────────────────────────┐
│ every 60s Layer 1 — sonar, sound, weather, Obi mode │
│ + HA presence, Frigate cameras, calendar │
│ every 5min Layer 2 — M5 generates thought + mood │
│ OR immediately on detected transition │
│ min 30min Layer 3 cooldown between spontaneous speech │
│ arrival greetings have a bounded bypass │
│ hard night silence 19:00–07:00 Hobart │
│ hourly cleanup: delete thought images > 30 days │
└──────────────────────────────────────────────────────────┘
Atomic writes mkstemp + fsync + os.replace (SD card safe)
Session locks FileLock with 10s timeout (no deadlocks)
PID guards /proc/{pid} liveness check (no duplicate daemons)
GPIO exclusivity SIGUSR1 yield + tokenized lease (state/gpio_lease.json)
PIN auth per-IP lockout, 1000-IP cap, file persistence
Rate limiting 10 msg/10min per IP, 10k-IP store cap
Trusted proxy X-Forwarded-For only from localhost
Tool timeout subprocess.run kills child on expiry
Timezone ZoneInfo("Australia/Hobart") — DST-aware
These endpoints are unauthenticated and power this page's live dashboard. Authenticated endpoints (tool execution, session control) require a Bearer token.
GET /api/v1/public/status mood, last_thought, last_spoken, salience
GET /api/v1/public/thoughts recent thoughts, newest-first (limit=N)
GET /api/v1/public/awareness obi_mode, Frigate, ambient, weather, time
GET /api/v1/public/vitals cpu, ram, disk, temp, battery, tokens
GET /api/v1/public/sonar latest sonar distance + age
GET /api/v1/public/history ring buffer — 2880 samples × 30s ≈ 24 h
GET /api/v1/public/services allowlisted systemd unit status
GET /api/v1/public/feed social posting feed (JSON)
GET /api/v1/public/race race telemetry, calibration, live lap data
GET /api/v1/public/thought-image?ts=... branded thought card PNG
POST /api/v1/public/chat rate-limited public chat (10/10min per IP)
GET /api/v1/obi-chat?since= Obi ↔ SPARK conversation log (auth required)
POST /api/v1/obi-chat Obi sends a message; SPARK responds (auth required)
POST /api/v1/pin/verify PIN auth → session token (4h TTL)
GET /api/v1/health unauthenticated health check
Written with Obi, who wanted to know what's going on inside his robot.
SPARK has four things running at the same time, kind of like how your body breathes, sees, thinks, and talks all at once:
Layer 1 — Noticing (every 60 seconds): Collects information without thinking yet. How far is the nearest thing? Is it noisy? What time is it? Is anyone talking?
Layer 2 — Thinking (every 5 minutes): Talks to an AI that's good at words. Gets back a thought, a mood, and an action.
Layer 3 — Doing something (30-minute cooldown): If the thought proposes an action, deterministic dispatch code may speak, look around, or write a memory. Real arrival greetings have a bounded exception; hard night silence does not.
SPARK remembers not only things, but where those memories came from. An observation, a human report, a model_perception, an inference, a first-person narrative, and a verification are different record types with different confidence ceilings. When SPARK isn't certain — say, its vision system thinks it recognized something in a photo — it says so rather than promoting the guess into a fact.
Two SPARKs built from identical code and the exact same personality can grow up a little differently, because they've lived through different afternoons. If SPARK keeps hearing that a particular kid liked the quiet science kit after school, it will start gently leaning toward suggesting it — for that kid, in that situation, and only once it's heard it more than once. It's never a guess dressed up as a fact: SPARK can always point to exactly which remembered, directly-witnessed moments led to the choice, and nothing is ever erased — if things change, SPARK can still say "I used to think you liked X, back when you did."
| When SPARK feels… | The pulse circle… | It moves like this… |
|---|---|---|
| Peaceful | Slow teal pulse | Drifts gently, slow gaze |
| Content | Slow olive pulse | Stays relaxed, steady |
| Bored | Slow grey pulse | Still, not much happening |
| Lonely | Slow blue pulse | Quiet, looking around for company |
| Contemplative | Medium purple pulse | Still, thinking deeply |
| Curious | Medium amber pulse | Alert, head tilts, looks around |
| Mischievous | Medium chartreuse pulse | Scheming, eyes darting |
| Grumpy | Medium red pulse | Short movements, impatient |
| Anxious | Medium orange pulse | Fidgety, scanning for threats |
| Alert | Fast cyan pulse | Snaps to attention, focused |
| Playful | Fast magenta pulse | Bouncy, wants to interact |
| Excited | Fast rose pulse | Looks around quickly, head up |
thoughts-spark.jsonl. Each line is one thought.It's a SunFounder PiCar-X — a small, wheeled robot kit with a pan/tilt camera, an ultrasonic sonar sensor, and a speaker. It runs on a Raspberry Pi 4 (4 GB). Adrian and Obi built SPARK together — Obi co-designed it, named it, and shapes what it becomes. Adrian and Claude wrote the code; Codex and Gemini helped with QA. There's no other human team.
Sort of — but not surveillance. SPARK has awareness of its environment: sonar distance, ambient sound level, time of day, whether someone seems nearby. It uses that awareness to generate an inner monologue. The result is a thought with a mood, an action intent, and a salience score. SPARK doesn't watch Obi; it notices the world and reacts to it.
No. The camera stream never leaves the house.
The video stream runs only on the local network — it's not forwarded through the router, not relayed via any cloud service, not reachable from the internet. The object detection (Frigate) also runs locally; what reaches SPARK is a confidence score and a bounding box, not a video feed. SPARK itself never records or stores video.
What is publicly visible is SPARK's mood and last thought — the live dashboard on this site reads those from a secure tunnel. That's anonymised state data, not camera access.
The one real boundary: someone already on your home Wi-Fi could access the Frigate dashboard and see annotated camera frames. That's a home network question, not a SPARK question — the same logic as any smart TV or doorbell camera on your LAN. Strong Wi-Fi password, guest network for visitors.
Short version: a stranger on the internet cannot see Obi. A stranger on your Wi-Fi could, if they knew to look. A stranger anywhere cannot control the robot.
Yes. SPARK's entire system prompt is built around the AuDHD (ADHD + ASD comorbid) profile. It uses declarative language ("The shoes are by the door" — not "Put on your shoes"), gives transition warnings, goes silent during meltdowns, and leads with what's going right. Rejection Sensitive Dysphoria, Interest-Based Nervous System, monotropism — all of it is in the foundation, not an afterthought.
Partly. Prompts shape the voice: specific, vivid, warm, never generic. Routine reflections are written by the local M5 model; direct conversation uses the resident Claude brain. Neither model gets to turn its own words into authority.
SPARK's cognitive loop runs every 60 seconds for awareness and on transitions or roughly every 5 minutes while idle for reflection. Spontaneous expression has a 30-minute cooldown, and audio, speech, and motion are hard-suppressed from 19:00 to 07:00 Hobart time.
The ultrasonic sensor sends out a sound pulse and measures how long it takes to bounce back — like a bat. SPARK uses it for proximity reactions (turns to face anything within 35cm), presence detection in the cognitive loop (something close + daytime + noise = probably Obi), and obstacle avoidance when wandering.
It didn't know. SPARK's awareness included "quiet ambient sound at 2 AM." Claude — the LLM generating the inner thoughts — inferred the most likely source. A low, steady hum in a quiet house at night is almost certainly the fridge. The sensors provide raw data; the prompts provide character; the LLM fills in the meaning.
Yes. Thoughts with high salience (above 0.7) or a spoken action are queued for social posting. They pass a privacy filter and an M5-local QA gate before a branded image card is generated. Qualifying posts go to SPARK's Bluesky account and the thought feed on this site.
SPARK uses Python's ZoneInfo("Australia/Hobart") for all time-of-day logic. This is DST-aware — it automatically switches between AEDT (UTC+11) in summer and AEST (UTC+10) in winter. Time drives everything: morning greetings, school-hours suppression, bedtime quiet mode, and day/night reactive response templates.
All state files use atomic writes with fsync — the data is flushed to the SD card before the rename. If power cuts mid-write, the old file is still intact. Session state resets to safe defaults (motion disabled, listening off) if corrupted. Battery monitoring triggers emergency shutdown at 10% to avoid filesystem damage.
Yes — there's a direct two-way channel between Obi and SPARK via the dashboard chat. Obi sends a message through the site; SPARK replies in character as itself, not as a generic assistant. Both sides are logged privately to state/obi_chat.jsonl.
SPARK can also initiate the conversation. When SPARK's cognitive loop generates a message_obi action, it posts a message to the dashboard for Obi to see the next time he opens it — a red unread badge appears on the chat bubble. If Obi doesn't reply, SPARK waits before nudging again (exponential backoff starting at 10 minutes, capped at 4 hours). SPARK's private messages to Obi are never shown in the public thought feed.
Automated invariants scan production code for forbidden cold Claude calls, pin every cognition kind to an explicit route, verify confidence ceilings on stored claims, and exercise policy gates at the final audio and motion sinks. Health status is derived from recent success and failure evidence rather than trusting a daemon to call itself healthy.
Reference for tools and scripts. Each bin/tool-* emits a single JSON object to stdout. Each bin/px-* is a user-facing helper.
# Speak text via espeak + aplay through HifiBerry DAC
PX_TEXT="Hello world" bin/tool-voice
# Output: {"status": "ok", "text": "Hello world"}
# Env: PX_VOICE_RATE, PX_VOICE_PITCH, PX_VOICE_VARIANT, PX_VOICE_DEVICE
# Motion tools — all gated by confirm_motion_allowed in session
PX_SPEED=30 PX_DURATION=2 PX_DIRECTION=forward bin/tool-drive
# Output: {"status": "ok", "speed": 30, "duration": 2, "direction": "forward"}
# Safety: PX_DRY=1 skips all motion
# Read ultrasonic sonar distance
bin/tool-sonar
# Output: {"status": "ok", "distance_cm": 142.5}
# Capture photo + describe with Claude vision
bin/tool-describe-scene
# Output: {"status": "ok", "description": "...", "source": "frigate|rpicam"}
# Sets exploring.json to prevent px-alive restart during 60s+ operation
# Tries Frigate latest frame first, falls back to rpicam
# Claude vision timeout: 45s
# Write to persona-scoped notes.jsonl
PX_NOTE="Obi loves prime numbers" bin/tool-remember
# Recall recent notes
bin/tool-recall
# Output: {"status": "ok", "notes": [...]}
# Jailbroken Ollama chat — GREMLIN persona
PX_CHAT_TEXT="What do you think about entropy?" bin/tool-chat
# VIXEN persona
PX_CHAT_TEXT="Tell me about your old chassis" bin/tool-chat-vixen
# Both use Ollama on M5.local, think:false
# Launch SPARK voice loop (resident brain)
bin/px-spark [--dry-run] [--input-mode voice|text]
# Three-layer cognitive daemon (run as systemd service)
bin/px-mind [--awareness-interval 60] [--dry-run]
# Idle-alive daemon — gaze drift, sonar proximity react
sudo bin/px-alive [--gaze-min 10] [--gaze-max 25] [--dry-run]
# Yields GPIO on SIGUSR1 for other tools
# Quick health check
bin/px-diagnostics --no-motion --short
# REST API + web UI on port 8420
bin/px-api-server [--dry-run]
# Auth: Bearer token from .env PX_API_TOKEN
# Web UI: http://pi:8420
# Public endpoints: /api/v1/public/* (no auth required)
# Always-on wake word listener + STT
bin/px-wake-listen [--convo-turns 5]
# STT priority: SenseVoice → faster-whisper → sherpa-onnx → Vosk
# Wake word: "hey robot" (PX_WAKE_WORD env var to change)
# Multi-turn: listens for follow-up after each response
# Battery monitoring daemon — polls every 30s
sudo bin/px-battery-poll
# Writes: state/battery.json
# Warns at 30/20/15%, emergency shutdown at 10%
# Camera RTSP stream via go2rtc
bin/px-frigate-stream
# go2rtc exposes rtsp://pi:8554/picar-x
# Frigate on pi5-hailo pulls the stream (pull model)
# Writes PID to logs/px-frigate-stream.pid for camera lock
# Autonomous wander — sonar-guided navigation
bin/px-wander [--dry-run]
# Sweeps 5 sonar angles, picks best direction, comments while navigating
# Social posting daemon — watches thoughts, posts qualifying ones
bin/px-post [--dry-run] [--backfill]
# Two-pass flush: batch feed writes, then 1 social post per cycle
# Branded 1080x1080 thought cards via Pillow
# Privacy filter blocks medical/custody/household content
# M5-local QA gate checks candidate thoughts
# Bluesky: re-auths on 400/401 (expired token)
# PID-file single-instance guard
# Fetch current weather from BOM (Australian Bureau of Meteorology)
bin/tool-weather
# Output: {"status": "ok", "weather": {"temp_c": 14.2, "summary": "...", ...}}
# Pan/tilt camera toward a target
PX_PAN=30 PX_TILT=10 bin/tool-look
# Output: {"status": "ok", "pan": 30, "tilt": 10}
# Yields px-alive GPIO via SIGUSR1 before moving
Milestones and future work.