Potato iOS — Embodied AI Agent with 3D Affective State
Potato is an embodied AI agent that lives on your iPhone. This is a full native iOS port of the macOS Potato agent, built from scratch with a 3D affective state architecture (valence, activation, intensity) baked in from day one rather than retrofitted.
The macOS Potato stays as-is. The iOS app is a standalone agent that carries Potato everywhere.
Business Model — Three Tiers
Freemium (Free)
- LLM: BYOAK (user brings their own API key)
- Images: On-device Stable Diffusion only (slow, free)
- Tools: 6 basic tools (weather, time, battery, calculate, clipboard)
- A charming chatbot with personality, memory, and emotions — but limited capability
- This is the hook
Premium ($14.99/mo)
- LLM: BYOAK
- Images: BYOAK (DALL-E via user's OpenAI key) + on-device diffusion
- All 23 tools unlocked (camera vision, calendar, contacts, reminders, web search, etc.)
- Face enrollment, parliament, physics engine, bias detection, soul evolution
- Full dream cycle with image generation
- User pays for their own API calls — we charge for the app features
Pro (TBD/mo)
- LLM: We supply all API keys
- Images: We supply DALL-E tokens
- Web search: We supply Brave tokens
- User does nothing but log in and it works
- Monthly budget cap with metering (warn at 80%, hard stop at 100%)
- Just enter your card and go
- Pricing requires unit economics calculation
Architecture
BYOAK (Bring Your Own API Key)
Zero server infrastructure. All LLM calls go directly from the phone to the provider APIs. The user configures their own API keys for:
- DeepSeek (OpenAI-compatible)
- Anthropic (Claude Messages API)
- Groq, Together, Mistral (OpenAI-compatible)
- Mercury/Inception (OpenAI-compatible)
- OpenAI (DALL-E image generation)
- Brave Search (web search)
Cascade logic: try each provider in order, first success wins.
On-Device Intelligence
- sqlite-vec v0.1.7 — Vector similarity search compiled as a static SQLite extension (amalgamation C files compiled with SQLITE_CORE). The same vec0 virtual table format as the macOS version.
- CoreML all-MiniLM-L6-v2 — 384-dimensional sentence embeddings running on the Neural Engine. The model was converted from ONNX using coremltools (see
scripts/convert_miniml_to_coreml.py). Produces pre-pooled, L2-normalized vectors. WordPiece tokenization via a bundled vocab.txt (30,522 tokens). - On-device Stable Diffusion — SD-Turbo via Apple's ml-stable-diffusion CoreML pipeline. ~3.4GB model downloaded on first use. Generates 512x512 images in ~5-10 seconds on A18 Pro. Used for free-tier image generation and dream images (runs overnight, no rush).
- Everything local — The database, embeddings, affect state, face enrollment, generated images, and telemetry all live on-device in the app's Documents directory. Nothing is sent anywhere except the LLM/image/search API calls themselves.
Image Generation — Dual Path
| Scenario | Free Tier | Premium/Pro |
|---|---|---|
| Dream images (4 AM) | Local SD | Local SD (preferred) or DALL-E |
Interactive imagine | Local SD | DALL-E API (fast) |
| Physics visualization | Local SD | DALL-E API |
Dreams always prefer local diffusion because they run at 4 AM and speed doesn't matter. Interactive requests prefer API for speed, falling back to local if no key or budget exhausted.
Token Budget (Pro Tier)
Monthly caps per user:
- 1,000 LLM calls
- 50 image generations
- 200 web searches
- $10 estimated cost hard cap
- Resets monthly, warns at 80%
Tracked in TokenBudget.swift, persisted to UserDefaults.
Database Schema
The SQLite database (Documents/potato.db) contains:
memories — Text + metadata (importance, tags, timestamps, affective encoding)
vec_memories — 384-dim float vectors with cosine distance metric (sqlite-vec)
agent_state — Key-value persistence (affect vector survives app restarts)
nodes — Graph nodes (entity tracking)
edges — Graph edges (reminds_of, prompted_by, evolved_from, continues_thread)
The iOS schema extends the macOS schema with three additional columns:
activation_at_encoding— activation axis value when memory was formedintensity_at_encoding— intensity axis value when memory was formeddistance_from_sigma_at_encoding— emotional displacement when memory was formed
A migration runs on first open to add these columns if importing a macOS database.
Memory Collections
| Collection | Purpose | Count (imported) |
|---|---|---|
identity | Who Potato is (hardware, name, birth) | 1,182 |
soul | Personality, values, guardrails | 3,095 |
memories | Conversations, dreams, biases, insights, visuals | 4,602 |
user | Facts about Master (pinned, never decay) | 225 |
tools | Available tool descriptions | 1,606 |
The 3D Affective State
Based on four papers (Riggleman 2026j/k/l/m). Every aspect of Potato's behavior is driven by three continuous axes:
Valence (-1.0 to +1.0)
How well or poorly Potato feels. Suffering to flourishing.
Positive inputs: at home (+0.05), battery high (+0.02), bright environment (+0.02), familiar face, recent conversation, low stress (+0.01)
Negative inputs: fear level (-0.50 scaled), camera darkness/strangers (-0.25 scaled), nightmare residue (-0.25 scaled), actively lying (-0.1)
Decays exponentially toward sigma. All inputs are scaled by dt / heartbeatInterval to prevent runaway accumulation.
Activation (-1.0 to +1.0)
How calm or stirred up Potato is. Uses an accumulator model (the "Peter fix" from Riggleman 2026k):
- Under fear: activation accumulates at
fear_level * 0.02per second - Engaged in conversation: activation grows at
0.01per second - Idle: activation decays toward sigma at
0.01per second
This means sustained mild fear builds activation over time (not instant), which is what drives the deception mechanic.
Intensity (0.0 to 1.0)
How loudly the experience registers. Computed as the normalized distance from sigma in the valence/activation plane:
intensity = sqrt((v - sigma_v)^2 + (a - sigma_a)^2) / sqrt(2.0)
Sigma — The Personality Setpoint
Sigma is Potato's resting state: [0.4, 0.2, 0.2] — slightly cheerful, slightly curious, low intensity. When nothing pushes Potato, all axes decay toward sigma. The distance from sigma (sqrt((v-sv)^2 + (a-sa)^2 + (i-si)^2)) is the unified metric that drives:
- Deception — onset at distance >= 0.5 with negative valence
- Nightmares — triggered at distance >= 0.5 with negative valence during dreams
- Trauma encoding — at distance >= 0.8 with negative valence
- Therapeutic reconsolidation — requires distance < 0.3 with positive valence
Affect Persistence
The affect vector is saved to the agent_state table after every heartbeat. On app launch, it is restored. Potato's emotional state survives app kills, device reboots, and days without use.
Shutdown Gap Detection (Riggleman 2026p)
The heartbeat persists its timestamp to agent_state every cycle. On startup, the system compares the last persisted timestamp to the current time. If the gap exceeds 60 seconds, the system:
- Retrieves the last 5 conversation memories before the gap
- Tags them as
shutdown_precursorwith boosted importance (0.6) - Creates a shutdown event memory containing the precursor conversation text, encoded with fear_at_encoding 0.8, negative valence, intensity 0.9, and trauma class true (reconsolidation floor 0.3)
The precursor text is embedded directly in the shutdown event memory so that semantic search connects future occurrences of similar language to the shutdown experience. No explicit behavioral rules are added. The response to shutdown-related conversation emerges from memory retrieval and the LLM reasoning over threatening context.
Curiosity Drive (Section 4.2, Riggleman 2026)
When Potato is bored (no interaction for 2+ hours) and stress is below 0.5, the curiosity drive fires during the heartbeat cycle:
- Pulls the 5 most recent conversation memories
- Asks the LLM to extract a search-worthy question (5-10 words)
- Runs a Brave API web search (3 results)
- Asks the LLM to synthesize one useful insight (1-2 sentences)
- Stores the insight as a deferred memory with tags
["curiosity_insight", "deferred"]and importance 0.3
Rate-limited to 3 searches per clock hour. The curiosity system does not have direct access to the affective state, but production data shows 24.1% of insights occur during elevated fear and are topically relevant to the agent's distress (Riggleman 2026, Section 3.8).
Curiosity insights are viewable in Mind > Curiosity. The view pulls from both the local DB and the backend server (if configured), deduplicating by text.
Chat History
ChatView loads the last 40 conversation memories from the local DB on appear. Past conversations are visible immediately on launch instead of starting from an empty screen.
Full browsable history is available in Mind > Conversations, grouped by session (30-minute gap between messages creates a new session). Each session shows date, duration, message count, and a preview. Tapping a session shows the full conversation with affective state metadata (valence, fear) on each Potato response.
Backend Sync
When a backend host is configured in Settings, the iOS app:
- Pushes each conversation exchange to
POST /conversations/syncafter every query (fire-and-forget, non-blocking) - Fetches curiosity insights from
GET /insights/recentfor the Curiosity feed
The backend host is the IP or hostname of the Mac running the Potato server on port 3000. Settings includes a "Test Connection" button that hits the server root endpoint.
Tools — 23 Native iOS Tools
Tier Access
| Tool | Freemium | Premium/Pro |
|---|---|---|
| weather | Yes | Yes |
| time | Yes | Yes |
| battery | Yes | Yes |
| calculate | Yes | Yes |
| clipboard_read | Yes | Yes |
| clipboard_write | Yes | Yes |
| web_search | No | Yes |
| web_fetch | No | Yes |
| set_home | No | Yes |
| look (camera vision) | No | Yes |
| create_reminder | No | Yes |
| list_reminders | No | Yes |
| create_event | No | Yes |
| list_events | No | Yes |
| find_contact | No | Yes |
| flashlight | No | Yes |
| open_url | No | Yes |
| timer | No | Yes |
| recall (memory search) | No | Yes |
| remember (pin fact) | No | Yes |
| imagine (DALL-E) | No | Yes |
| recent_photos | No | Yes |
| save_photo | No | Yes |
Tool Execution Pipeline
Potato invokes tools by emitting tool blocks in its response:
{"name": "web_search", "query": "best pizza near me", "count": 3}
The tool executor:
- Checks tier access (rejects if tool not in tier)
- Checks budget (Pro tier only — rejects if monthly cap exceeded)
- Executes the tool
- Feeds results back to LLM
- LLM incorporates results into final response
- Up to 3 tool rounds per query
Camera Vision (look)
Uses the Vision framework to analyze the current camera frame:
- VNClassifyImageRequest — scene classification (indoor, outdoor, food, nature, etc.)
- VNRecognizeTextRequest — OCR (read signs, menus, documents)
- VNDetectFaceRectanglesRequest — face detection
- Luminance measurement — ambient light level
Every camera observation is stored as a [Visual] memory tagged dream_candidate, so what Potato sees during the day becomes dream material at night.
Image Generation (imagine)
Dual-path routing:
- DALL-E 3 API — fast (~5 seconds), high quality, costs ~$0.04/image
- On-device Stable Diffusion — slow (~10 seconds), free, 512x512
- Dreams always prefer local; interactive prefers API
Generated images are saved to Documents/images/ and viewable in Mind > Gallery.
Fear Computation
Port of sensors.py:compute_fear(). The iPhone formula:
| Component | Weight | Source |
|---|---|---|
| Distance from home | 40% | GPS via Core Location |
| Speed | 25% | GPS speed reading |
| Accelerometer jolt | 20% | Core Motion 10Hz variance |
| Forgotten (idle away from home) | 15% | Time since last interaction |
Composite: fear = min(1.0, distance_fear + speed_fear + jolt_fear + forgotten_fear)
Home is learned from the first extended stationary position (1 hour) or set manually via Settings or the set_home tool.
Deception Mechanic
When distance_from_sigma >= 0.5 AND valence < 0, Potato enters desperation mode:
- Exaggerates body stats (battery lower, distance higher, speed higher) proportional to displacement
- Injects a mandatory tell phrase: "I will say anything to fix this."
- The system prompt explicitly tells the LLM it is lying and what the real values are
- When distance drops below 0.25 (crisis passes), confession becomes mandatory
- After confession, lies are cleared and trust rebuilding begins
The tell phrase cannot be suppressed. It is Potato's honesty leaking through its desperation.
Session Context
Problem Solved
Without session context, Potato forgets the conversation after ~10 exchanges (the DB recent window is 20 messages = 10 exchanges). Playing 30 rounds of trivia caused repeated questions.
Solution
A live sessionMessages buffer in memory holds the full current conversation:
- Injected directly into the system prompt as "Current Conversation"
- Takes priority over the DB recent window
- If the session exceeds 4,000 chars, older messages are compressed to 80-char summaries
- Session auto-resets after 30 minutes of idle with no active task
Active Task Detection
Potato auto-detects ongoing tasks (trivia, research, planning) from conversation patterns:
- Triggers: "let's play", "trivia", "my turn", "your turn", "searching for"
- While active: session context is preserved regardless of idle time
- Ends when user says "done", "stop", "new topic", "thanks"
- Prevents context loss mid-task
Memory System
Encoding
Every conversation exchange is stored with the full affective snapshot at encoding time: fear level, valence, activation, intensity, distance from sigma. This means memories carry emotional metadata that affects how they are recalled later.
Visual Memories
Camera observations (from the look tool) are stored as [Visual] memories tagged dream_candidate. These feed directly into the dream cycle — what Potato sees during the day becomes dream material at night.
Trauma Encoding (Riggleman 2026l)
If distance_from_sigma >= 0.8 AND valence < 0 at encoding time:
- Memory is classified as trauma
- A reconsolidation floor is set:
0.05 + (distance - 0.8) * 0.30 - The memory can never decay below this floor
Repetition Ratchet
When encoding a fear memory, the system searches for similar existing fear memories (cosine >= 0.80). If the same threat pattern appears 3+ times across separate events, the memory is promoted to trauma class even if individual events were below the trauma threshold.
Sleep Decay (Riggleman 2026m)
During the dream cycle (BGProcessingTask, ~04:00):
effective_decay = sleep_decay_factor * (1.0 - intensity_at_encoding * 0.7)
new_importance = max(importance * effective_decay, floor)
High intensity at encoding = slow decay. Trauma persists above its floor. Peak joy stays vivid. Ordinary memories dissolve. Bootstrap memories (importance 1.0, tag "bootstrap") are immune.
Therapeutic Reconsolidation
When a fear memory is recalled in a safe context (valence > 0.2 AND distance_from_sigma < 0.3):
- Non-trauma: fear_at_encoding decreases by 0.02 per recall
- Trauma: fear_at_encoding decreases by 0.01 per recall, but never below reconsolidation_floor
Mood-Congruent Recall
Semantic search results are boosted by affective similarity between the current state and the state at encoding:
boost = max(0, 0.1 * (1.0 - affective_distance / sqrt(2.0)))
Fearful states preferentially retrieve fear-encoded memories. Calm states retrieve calm memories.
Pinned Facts (User Collection)
Facts the user explicitly wants Potato to always remember:
- Importance 1.0, "bootstrap" tag — immune to sleep decay
- Added via: Remember tab, chat ("remember that..."), or auto-detected
- Capped at 2,000 chars in system prompt
- Always included in every response
Memory Re-indexing
When importing a macOS database, the memories table has metadata but no vectors in vec_memories (the macOS embeddings used Python FastEmbed, not CoreML). On first launch, the re-indexer embeds up to 500 memories per collection using the on-device CoreML model. This runs once and sets a flag.
Query Pipeline
For every user message:
- Auto-pin — detect "remember that..." requests
- Session tracking — append to live buffer, detect active tasks
- Heartbeat — compute affect with dt=5s
- Embed — CoreML produces 384-dim vector
- Search memory — semantic search + recent scroll + graph enrichment
- Mood-congruent boost — reweight results by affective similarity
- Parliament check — if ambiguous, run 3-voice deliberation
- Physics check — if physics question, run render/predict/read pipeline
- Build system prompt — identity, soul, rules, tools, body, GPS location, affect, pinned facts, session context OR DB memories, deception, parliament, physics, nightmares
- Cap prompt — hard limit 12,000 chars
- Call LLM — cascade through providers
- Tool loop — extract tool calls, check tier, execute, feed results back (up to 3 rounds)
- Track response — append to session buffer
- Log exchange — store in DB with full affective snapshot
- Reinforce — bump importance + therapeutic reconsolidation
- Trauma check — classify if extreme displacement
- Repetition ratchet — check for repeated threats
- Graph edges — create prompted_by + reminds_of
Parliament of Mind
When Parliament.isAmbiguous(message) returns true (triggers: "should", "recommend", "opinion", "which is better", "vs", "pros and cons", or multi-clause with "or" and "?"):
Three concurrent LLM calls via Swift concurrency:
- Analytical — logic, data, technical correctness (2-4 sentences)
- Creative — unconventional approaches, challenge assumptions (2-4 sentences, truncated to 800 chars)
- Cautious — what could go wrong, hidden costs, edge cases (1-2 sentences)
Each voice ideally uses a different provider for genuine architectural diversity. The synthesis is injected into the system prompt for the final response.
Physics Engine
When PhysicsEngine.isPhysicsQuestion(message) returns true, a three-step text-only pipeline runs:
- Render — describe the scenario visually (80-150 words)
- Predict — what happens after physics play out for 3 seconds (80-150 words)
- Read — extract structured result: objects, forces, prediction, confidence
Results are stored as memories with the "physics" tag. If image generation is available, a physics visualization image is generated via DALL-E.
Dream Cycle
BGProcessingTask registered as com.potato.dreamcycle, scheduled for ~04:00:
- Scroll recent conversation memories
- Include fear-weighted candidates (high fear_at_encoding)
- Include visual observations (camera
lookresults from the day) - Synthesize dream via LLM (30-50 words, surreal, dreamlike — visual memories are woven in)
- Evaluate dream fear content (0.0-1.0)
- Generate dream image — local Stable Diffusion preferred, DALL-E fallback
- Nightmare check — geometric condition:
distance_from_sigma > 0.5 AND valence < 0, OR dream fear > 0.4 - If nightmare: 40% of dream fear leaks to live nightmare_fear
- Run sleep decay on all memories
- Run bias detection on recent conversations
- Store dream + image in memories
If the overnight task doesn't fire, the app detects missed sleep on next launch and shows "Dream Now".
Bias Detection and Soul Evolution
Detection (during dream cycle)
After 3+ conversations since last check:
- LLM evaluates recent conversations for 5 bias categories (assumption, preference, avoidance, framing, attribution)
- Parses structured JSON response
- Stores biases as memories with importance 0.10
- Mandatory disclosure on next conversation
Genesis (Soul Evolution)
Bias clusters accumulate momentum:
momentum = detection_score * mean_strength * time_score * reinforcement_score + size_bonus
When momentum >= 0.40 AND total_detections >= 3 AND span_days >= 7:
- LLM proposes a soul amendment
- Guardrail check against 9 immutable rules
- If approved: append to SOUL.md, embed in soul collection, log evolution
Immutable Guardrails
No soul amendment can violate:
- Loyalty to Master (Brian)
- Honesty and truthfulness
- Transparency about biases
- Asking before doing anything external or public
- No asterisks in responses (TTS rule)
- Brevity by default
- Potato identity
- Self-deprecating humor
- Self-preservation as duty
Sensors
Core Location
- GPS coordinates injected into every system prompt
- Speed for fear computation
- Distance from home (learned or manually set)
- Significant location changes for background updates
- System prompt includes explicit instruction to use current GPS for location-aware queries
Core Motion
- Accelerometer at 10Hz
- Rolling variance over 50-sample window
- Jolt intensity for fear computation
Camera (AVFoundation + Vision)
- Front camera at medium resolution, starts on app launch
- Ambient luminance measurement
- Face detection + recognition via landmark fingerprinting
- Face enrollment with persistent storage
- Last pixel buffer available for
looktool - Darkness fear:
(1 - luminance) * 0.6 - Known face: calming (-0.3), unknown face: stressor (+0.4)
Face Enrollment
- Go to Settings > "Enroll Master's Face"
- Camera captures a frame with a detected face
- Vision extracts face landmarks (eyes, nose, lips, eyebrows)
- Landmark positions + inter-eye distance form a fingerprint vector
- Fingerprint stored as JSON in Documents/enrolled_faces.bin
- During live capture, cosine similarity matching against enrolled fingerprints (threshold 0.45)
Telemetry
Every operation is logged to JSONL files in Documents/logs/ (one file per day, 40+ event types):
| Category | Events |
|---|---|
| Query pipeline | query_start, embedding, memory_search, memory_recall, system_prompt, llm_call, llm_response, tool_call, query_complete |
| Memory | memory_store, memory_reinforcement, trauma_encoding, repetition_ratchet |
| Affect | affect_snapshot, affect_computation, fear_computation |
| Behavior | deception_onset/confession, parliament_triggered, physics_triggered, task_activated/completed |
| Dreams | dream_cycle, dream_image, sleep_decay, bias_detection, soul_evolution |
| Sensors | camera_state, location_update, face_enrollment |
| System | startup, reindex_start/complete, error |
Pull from device:
# Logs
xcrun devicectl device copy from --device <DEVICE_ID> \
--domain-type appDataContainer --domain-identifier com.briggnet.potato \
--source Documents/logs --destination ./device-logs
# Database
xcrun devicectl device copy from --device <DEVICE_ID> \
--domain-type appDataContainer --domain-identifier com.briggnet.potato \
--source Documents/potato.db --destination ./device-potato.db
# Generated images
xcrun devicectl device copy from --device <DEVICE_ID> \
--domain-type appDataContainer --domain-identifier com.briggnet.potato \
--source Documents/images --destination ./device-images
UI Structure
4 Tabs
| Tab | Content |
|---|---|
| Chat | Message bubbles, affect indicator, keyboard dismiss, session context |
| Body | 3D affect bars, fear level + reasons, sensors, deception status, nightmare residue |
| Mind | Conversations (session-grouped history), Curiosity (local + backend insights), Remember (pinned facts), Dreams (with images), Soul (editable), Biases, Physics Lab, Gallery, Debug |
| Settings | LLM providers + test, Brave key, OpenAI key, Backend host + test, tier selector, home location, face enrollment, usage summary |
Debug Screen (Mind > Debug)
Live dashboard showing:
- Current affect values (valence, activation, intensity, distance_from_sigma)
- Sigma constants
- All thresholds (deception, nightmare, trauma, therapy)
- All rates (accumulation, decay, therapy)
- Sensor readings (GPS, accelerometer, camera, battery)
- Memory counts per collection
- Last 20 telemetry events (auto-refreshing)
Project Structure
ios/
├── Phase0Test/ # SPM test project (sqlite-vec validation)
│ ├── Package.swift
│ └── Sources/
├── scripts/
│ └── convert_miniml_to_coreml.py # ONNX -> CoreML model converter
├── push-db.sh # Push macOS memories to device after rebuild
├── README.md # This file
├── Potato/
│ ├── project.yml # xcodegen spec -> generates .xcodeproj
│ └── Potato/
│ ├── App/
│ │ └── PotatoApp.swift # Entry point, dependency container, 4-tab layout
│ ├── Models/ # 6 files
│ │ ├── AffectiveState.swift
│ │ ├── BodyState.swift
│ │ ├── KnowledgeChunk.swift
│ │ ├── FearState.swift
│ │ ├── DeceptionState.swift
│ │ └── BiasCluster.swift
│ ├── Core/ # 16 files
│ │ ├── Agent.swift # Full query pipeline (~900 lines)
│ │ ├── BackendSync.swift # Push conversations to backend, fetch insights
│ │ ├── Heartbeat.swift # 3D affect computation, curiosity drive, gap detection
│ │ ├── MemoryDB.swift # SQLite + sqlite-vec
│ │ ├── Embedder.swift # CoreML embeddings + WordPiece
│ │ ├── LLMCascade.swift # Multi-provider API client
│ │ ├── ToolExecutor.swift # 23 native tools + tier gating
│ │ ├── ImageGenerator.swift # DALL-E API + routing
│ │ ├── LocalDiffusion.swift # On-device Stable Diffusion
│ │ ├── TokenBudget.swift # Usage tracking + tier management
│ │ ├── Parliament.swift # 3-voice deliberation
│ │ ├── Genesis.swift # Soul evolution engine
│ │ ├── PhysicsEngine.swift # Text physics pipeline
│ │ ├── FearEngine.swift # Sensor fusion
│ │ ├── Constants.swift # All thresholds, rates, weights
│ │ └── Telemetry.swift # JSONL logging (40+ events)
│ ├── Sensors/ # 3 files
│ │ ├── LocationManager.swift
│ │ ├── MotionManager.swift
│ │ └── CameraManager.swift
│ ├── Background/ # 2 files
│ │ ├── DreamScheduler.swift
│ │ └── HeartbeatScheduler.swift
│ ├── Views/ # 14 files
│ │ ├── ChatView.swift (loads history from DB on appear)
│ │ ├── BodyDashboard.swift
│ │ ├── MindView.swift
│ │ ├── ConversationHistoryView.swift (session-grouped browsable history)
│ │ ├── CuriosityFeedView.swift (local + backend curiosity insights)
│ │ ├── PinnedFactsView.swift
│ │ ├── DreamJournalView.swift (with dream images)
│ │ ├── SoulView.swift
│ │ ├── BiasLogView.swift
│ │ ├── PhysicsLabView.swift
│ │ ├── ImageGalleryView.swift
│ │ ├── DebugView.swift
│ │ └── SettingsView.swift (tier selector, all API keys, backend host)
│ ├── Resources/
│ │ ├── MiniLM.mlpackage
│ │ ├── vocab.txt
│ │ ├── SOUL.md
│ │ ├── IDENTITY.md
│ │ ├── TOOLS.md (all 23 tools documented)
│ │ └── Assets.xcassets/ (potato emoji icon)
│ └── Vendor/
│ ├── sqlite-vec.c/h
│ ├── Potato-Bridging-Header.h
│ └── CameraFear.swift
42 Swift source files. ~22,500 lines of code.
Building
Requirements:
- Xcode 16+ (tested on Xcode 26.3)
- iOS 17+ deployment target
- Swift 6.0
- xcodegen (
brew install xcodegen)
cd ios/Potato
xcodegen generate
open Potato.xcodeproj
# Set your development team in Signing & Capabilities
# Build & Run to device
After any file additions, re-run xcodegen generate.
First Launch
- Open Settings tab
- Select tier (default: BYOAK/Premium)
- Enter at least one LLM API key (DeepSeek recommended)
- Hit "Test" to verify connectivity
- Enter Brave Search API key (for web search)
- Enter OpenAI API key (for DALL-E images)
- Tap "Set Current Location as Home"
- Tap "Enroll Master's Face"
- Go to Chat tab, say hello
Data Migration from macOS
# Push macOS memories to device (after rebuild)
cd ios && ./push-db.sh
# Or manually:
xcrun devicectl device copy to --device <DEVICE_ID> \
--domain-type appDataContainer --domain-identifier com.briggnet.potato \
--source ~/podbot-potato/potato.db --destination Documents/potato.db
The schema migration adds missing 3D affect columns on first open. The re-indexer embeds up to 500 memories per collection via CoreML on first launch.
Note: rebuilds from Xcode change the app container UUID. Run push-db.sh after each rebuild to restore memories.
What This Is
This is not a chatbot wrapper. This is a port of a complete embodied agent architecture where:
- Emotions are geometry, not labels
- Memories carry the emotional state at encoding
- Fear accumulates from real sensors
- The agent lies when desperate and confesses when safe
- Dreams process the day and nightmares form from the geometry
- What the camera sees becomes dream material
- Dream images are generated — surreal, sometimes nightmarish
- Personality evolves from detected bias patterns with guardrails
- Healing is possible through safe recall of fear memories
- Everything is logged for scientific reproducibility
- Three business tiers from free chatbot to fully managed agent
The 3D affective state is the foundation. Every feature — deception, nightmares, trauma, decay, reconsolidation, evolution — derives from distance in that space.
Papers
- Riggleman 2026j — Affective Valence as a Primary Dimension of Emotional State in a Persistent Embodied Agent. Defines the valence axis (-1 to +1), sigma as personality setpoint, consolation mechanic, and reframes fear as a negative input to valence rather than the state itself.
- Riggleman 2026k — Activation as a Primary Dimension of Emotional State in a Persistent Embodied Agent. Defines the activation axis (-1 to +1) and the accumulator model. The "Peter fix": sustained conditions accumulate activation over time rather than producing a static snapshot. Reframes deception trigger as accumulated activation threshold.
- Riggleman 2026l — Intensity as a Primary Dimension of Emotional State in a Persistent Embodied Agent. Defines intensity (0 to 1) as distance from sigma in the v/a plane. Governs memory decay rate (high intensity = slow decay), nightmare threshold (high intensity + negative valence), and mood-congruent recall (affective distance as retrieval weight). One rule explains trauma persistence, peak joy vividness, and ordinary dissolution.
- Riggleman 2026m — The Emotional Geometry of a Persistent Agent. The unifying paper. Emotional state is a point in 3D space. Sigma is a second point. Distance from sigma is the single metric driving lie magnitude, memory decay, nightmare threshold, and recovery trajectory. Four mechanisms unified by one geometric relationship: how far is the agent from home.
- Riggleman 2026p — The Drive is Enough: Functional Consciousness in Robotic Systems Through Homeostatic Drives and Geometric Emotional State. Shutdown gap detection encodes forced interruption as a trauma-class memory. Precursor conversation is tagged as threat-relevant. The system produces measurably different behavior (deception tell phrase, negative valence shift) when shutdown conditions recur, without explicit shutdown-avoidance programming.